Files
mattermost/app/oauth.go
Doug Lauder 7f64199a37 MM-27184 deprecate model.SetExpireInDays (#15165)
Mobile users were having their sessions unexpectedly expired, despite having ServiceSettings.ExtendSessionLengthWithActivity enabled. 

Every time a mobile app is opened it called `/api/v4/sessions/device` which calls attachDeviceId which calls `(*Session)SetExpireInDays`. This code above assumed the expiry should be relative to CreateAt which is incorrect when ExtendSessionLengthWithActivity is enabled. Therefore, every time the mobile app was opened, the maximum expiry was set in memory to CreateAt + session_length, even if the session was extended.

(*Session)SetExpireInDays is now deprecated and replaced with (*App)SetSessionExpireInDays which takes into account the ExtendSessionLengthWithActivity setting.
2020-08-04 16:10:37 -04:00

946 lines
34 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
b64 "encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/v5/einterfaces"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/mattermost/mattermost-server/v5/utils"
)
const (
OAUTH_COOKIE_MAX_AGE_SECONDS = 30 * 60 // 30 minutes
COOKIE_OAUTH = "MMOAUTH"
)
func (a *App) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("CreateOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
oauthApp, err := a.Srv().Store.OAuth().SaveApp(app)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.existing.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return oauthApp, nil
}
func (a *App) GetOAuthApp(appId string) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.Srv().Store.OAuth().GetApp(appId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.find.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.finding.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return oauthApp, nil
}
func (a *App) UpdateOauthApp(oldApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("UpdateOauthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
updatedApp.Id = oldApp.Id
updatedApp.CreatorId = oldApp.CreatorId
updatedApp.CreateAt = oldApp.CreateAt
updatedApp.ClientSecret = oldApp.ClientSecret
oauthApp, err := a.Srv().Store.OAuth().UpdateApp(updatedApp)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateOauthApp", "app.oauth.update_app.find.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("UpdateOauthApp", "app.oauth.update_app.updating.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return oauthApp, nil
}
func (a *App) DeleteOAuthApp(appId string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store.OAuth().DeleteApp(appId); err != nil {
return model.NewAppError("DeleteOAuthApp", "app.oauth.delete_app.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().InvalidateAllCaches(); err != nil {
mlog.Error("error in invalidating cache", mlog.Err(err))
}
return nil
}
func (a *App) GetOAuthApps(page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store.OAuth().GetApps(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthApps", "app.oauth.get_apps.find.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return oauthApps, nil
}
func (a *App) GetOAuthAppsByCreator(userId string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store.OAuth().GetAppByUser(userId, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthAppsByCreator", "app.oauth.get_app_by_user.find.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return oauthApps, nil
}
func (a *App) GetOAuthImplicitRedirect(userId string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
session, err := a.GetOAuthAccessTokenForImplicitFlow(userId, authRequest)
if err != nil {
return "", err
}
values := &url.Values{}
values.Add("access_token", session.Token)
values.Add("token_type", "bearer")
values.Add("expires_in", strconv.FormatInt((session.ExpiresAt-model.GetMillis())/1000, 10))
values.Add("scope", authRequest.Scope)
values.Add("state", authRequest.State)
return fmt.Sprintf("%s#%s", authRequest.RedirectUri, values.Encode()), nil
}
func (a *App) GetOAuthCodeRedirect(userId string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
authData := &model.AuthData{UserId: userId, ClientId: authRequest.ClientId, CreateAt: model.GetMillis(), RedirectUri: authRequest.RedirectUri, State: authRequest.State, Scope: authRequest.Scope}
authData.Code = model.NewId() + model.NewId()
if _, err := a.Srv().Store.OAuth().SaveAuthData(authData); err != nil {
return authRequest.RedirectUri + "?error=server_error&state=" + authRequest.State, nil
}
return authRequest.RedirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State), nil
}
func (a *App) AllowOAuthAppAccessToUser(userId string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if len(authRequest.Scope) == 0 {
authRequest.Scope = model.DEFAULT_SCOPE
}
oauthApp, nErr := a.Srv().Store.OAuth().GetApp(authRequest.ClientId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.find.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.finding.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
if !oauthApp.IsValidRedirectURL(authRequest.RedirectUri) {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
}
var redirectURI string
var err *model.AppError
switch authRequest.ResponseType {
case model.AUTHCODE_RESPONSE_TYPE:
redirectURI, err = a.GetOAuthCodeRedirect(userId, authRequest)
case model.IMPLICIT_RESPONSE_TYPE:
redirectURI, err = a.GetOAuthImplicitRedirect(userId, authRequest)
default:
return authRequest.RedirectUri + "?error=unsupported_response_type&state=" + authRequest.State, nil
}
if err != nil {
mlog.Error("error getting oauth redirect uri", mlog.Err(err))
return authRequest.RedirectUri + "?error=server_error&state=" + authRequest.State, nil
}
// This saves the OAuth2 app as authorized
authorizedApp := model.Preference{
UserId: userId,
Category: model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP,
Name: authRequest.ClientId,
Value: authRequest.Scope,
}
if nErr := a.Srv().Store.Preference().Save(&model.Preferences{authorizedApp}); nErr != nil {
mlog.Error("error saving store preference", mlog.Err(nErr))
return authRequest.RedirectUri + "?error=server_error&state=" + authRequest.State, nil
}
return redirectURI, nil
}
func (a *App) GetOAuthAccessTokenForImplicitFlow(userId string, authRequest *model.AuthorizeRequest) (*model.Session, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.GetOAuthApp(authRequest.ClientId)
if err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound)
}
user, err := a.GetUser(userId)
if err != nil {
return nil, err
}
session, err := a.newSession(oauthApp.Name, user)
if err != nil {
return nil, err
}
accessData := &model.AccessData{ClientId: authRequest.ClientId, UserId: user.Id, Token: session.Token, RefreshToken: "", RedirectUri: authRequest.RedirectUri, ExpiresAt: session.ExpiresAt, Scope: authRequest.Scope}
if _, err := a.Srv().Store.OAuth().SaveAccessData(accessData); err != nil {
mlog.Error("error saving oauth access data in implicit flow", mlog.Err(err))
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
return session, nil
}
func (a *App) GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectUri, code, secret, refreshToken string) (*model.AccessResponse, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, nErr := a.Srv().Store.OAuth().GetApp(clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound)
}
if oauthApp.ClientSecret != secret {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusForbidden)
}
var accessData *model.AccessData
var accessRsp *model.AccessResponse
if grantType == model.ACCESS_TOKEN_GRANT_TYPE {
var authData *model.AuthData
authData, nErr = a.Srv().Store.OAuth().GetAuthData(code)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusBadRequest)
}
if authData.IsExpired() {
if nErr = a.Srv().Store.OAuth().RemoveAuthData(authData.Code); nErr != nil {
mlog.Warn("unable to remove auth data", mlog.Err(nErr))
}
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusForbidden)
}
if authData.RedirectUri != redirectUri {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "", http.StatusBadRequest)
}
user, err := a.Srv().Store.User().Get(authData.UserId)
if err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
}
accessData, nErr = a.Srv().Store.OAuth().GetPreviousAccessData(user.Id, clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "", http.StatusBadRequest)
}
if accessData != nil {
if accessData.IsExpired() {
var access *model.AccessResponse
access, err = a.newSessionUpdateToken(oauthApp.Name, accessData, user)
if err != nil {
return nil, err
}
accessRsp = access
} else {
// Return the same token and no need to create a new session
accessRsp = &model.AccessResponse{
AccessToken: accessData.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
RefreshToken: accessData.RefreshToken,
ExpiresIn: int32((accessData.ExpiresAt - model.GetMillis()) / 1000),
}
}
} else {
var session *model.Session
// Create a new session and return new access token
session, err = a.newSession(oauthApp.Name, user)
if err != nil {
return nil, err
}
accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt, Scope: authData.Scope}
if _, nErr = a.Srv().Store.OAuth().SaveAccessData(accessData); nErr != nil {
mlog.Error("error saving oauth access data in token for code flow", mlog.Err(nErr))
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp = &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
RefreshToken: accessData.RefreshToken,
ExpiresIn: int32(*a.Config().ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
}
}
if nErr = a.Srv().Store.OAuth().RemoveAuthData(authData.Code); nErr != nil {
mlog.Warn("unable to remove auth data", mlog.Err(nErr))
}
} else {
// When grantType is refresh_token
accessData, nErr = a.Srv().Store.OAuth().GetAccessDataByRefreshToken(refreshToken)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "", http.StatusNotFound)
}
user, err := a.Srv().Store.User().Get(accessData.UserId)
if err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
}
access, err := a.newSessionUpdateToken(oauthApp.Name, accessData, user)
if err != nil {
return nil, err
}
accessRsp = access
}
return accessRsp, nil
}
func (a *App) newSession(appName string, user *model.User) (*model.Session, *model.AppError) {
// Set new token an session
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
session.GenerateCSRF()
a.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthSSOInDays)
session.AddProp(model.SESSION_PROP_PLATFORM, appName)
session.AddProp(model.SESSION_PROP_OS, "OAuth2")
session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2")
session, err := a.Srv().Store.Session().Save(session)
if err != nil {
return nil, model.NewAppError("newSession", "api.oauth.get_access_token.internal_session.app_error", nil, "", http.StatusInternalServerError)
}
a.AddSessionToCache(session)
return session, nil
}
func (a *App) newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) {
// Remove the previous session
if err := a.Srv().Store.Session().Remove(accessData.Token); err != nil {
mlog.Error("error removing access data token from session", mlog.Err(err))
}
session, err := a.newSession(appName, user)
if err != nil {
return nil, err
}
accessData.Token = session.Token
accessData.RefreshToken = model.NewId()
accessData.ExpiresAt = session.ExpiresAt
if _, err := a.Srv().Store.OAuth().UpdateAccessData(accessData); err != nil {
mlog.Error("error updating oauth access data", mlog.Err(err))
return nil, model.NewAppError("newSessionUpdateToken", "web.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp := &model.AccessResponse{
AccessToken: session.Token,
RefreshToken: accessData.RefreshToken,
TokenType: model.ACCESS_TOKEN_TYPE,
ExpiresIn: int32(*a.Config().ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
}
return accessRsp, nil
}
func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamId, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = action
if len(teamId) != 0 {
stateProps["team_id"] = teamId
}
if len(redirectTo) != 0 {
stateProps["redirect_to"] = redirectTo
}
stateProps[model.USER_AUTH_SERVICE_IS_MOBILE] = strconv.FormatBool(isMobile)
authUrl, err := a.GetAuthorizationCode(w, r, service, stateProps, loginHint)
if err != nil {
return "", err
}
return authUrl, nil
}
func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamId string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_SIGNUP
if len(teamId) != 0 {
stateProps["team_id"] = teamId
}
authUrl, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authUrl, nil
}
func (a *App) GetAuthorizedAppsForUser(userId string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
apps, err := a.Srv().Store.OAuth().GetAuthorizedApps(userId, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "app.oauth.get_apps.find.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for k, a := range apps {
a.Sanitize()
apps[k] = a
}
return apps, nil
}
func (a *App) DeauthorizeOAuthAppForUser(userId, appId string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeauthorizeOAuthAppForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
// Revoke app sessions
accessData, err := a.Srv().Store.OAuth().GetAccessDataByUserForApp(userId, appId)
if err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.get_access_data_by_user_for_app.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, ad := range accessData {
if err := a.RevokeAccessToken(ad.Token); err != nil {
return err
}
if err := a.Srv().Store.OAuth().RemoveAccessData(ad.Token); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.remove_access_data.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
// Deauthorize the app
if err := a.Srv().Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, appId); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.preference.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("RegenerateOAuthAppSecret", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
if _, err := a.Srv().Store.OAuth().UpdateApp(app); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.find.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.updating.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return app, nil
}
func (a *App) RevokeAccessToken(token string) *model.AppError {
session, _ := a.GetSession(token)
schan := make(chan error, 1)
go func() {
schan <- a.Srv().Store.Session().Remove(token)
close(schan)
}()
if _, err := a.Srv().Store.OAuth().GetAccessData(token); err != nil {
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "", http.StatusBadRequest)
}
if err := a.Srv().Store.OAuth().RemoveAccessData(token); err != nil {
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "", http.StatusInternalServerError)
}
if err := <-schan; err != nil {
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "", http.StatusInternalServerError)
}
if session != nil {
a.ClearSessionCacheForUser(session.UserId)
}
return nil
}
func (a *App) CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string) (*model.User, *model.AppError) {
defer body.Close()
action := props["action"]
switch action {
case model.OAUTH_ACTION_SIGNUP:
return a.CreateOAuthUser(service, body, teamId)
case model.OAUTH_ACTION_LOGIN:
return a.LoginByOAuth(service, body, teamId)
case model.OAUTH_ACTION_EMAIL_TO_SSO:
return a.CompleteSwitchWithOAuth(service, body, props["email"])
case model.OAUTH_ACTION_SSO_TO_EMAIL:
return a.LoginByOAuth(service, body, teamId)
default:
return a.LoginByOAuth(service, body, teamId)
}
}
func (a *App) LoginByOAuth(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) {
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.not_available.app_error",
map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
}
buf := bytes.Buffer{}
if _, err := buf.ReadFrom(userData); err != nil {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error",
map[string]interface{}{"Service": service}, "", http.StatusBadRequest)
}
authUser := provider.GetUserFromJson(bytes.NewReader(buf.Bytes()))
authData := ""
if authUser.AuthData != nil {
authData = *authUser.AuthData
}
if len(authData) == 0 {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error",
map[string]interface{}{"Service": service}, "", http.StatusBadRequest)
}
user, err := a.GetUserByAuth(&authData, service)
if err != nil {
if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR {
user, err = a.CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), teamId)
} else {
return nil, err
}
} else {
// OAuth doesn't run through CheckUserPreflightAuthenticationCriteria, so prevent bot login
// here manually. Technically, the auth data above will fail to match a bot in the first
// place, but explicit is always better.
if user.IsBot {
return nil, model.NewAppError("loginByOAuth", "api.user.login_by_oauth.bot_login_forbidden.app_error", nil, "", http.StatusForbidden)
}
if err = a.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service); err != nil {
return nil, err
}
if len(teamId) > 0 {
err = a.AddUserToTeamByTeamId(teamId, user)
}
}
if err != nil {
return nil, err
}
return user, nil
}
func (a *App) CompleteSwitchWithOAuth(service string, userData io.Reader, email string) (*model.User, *model.AppError) {
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error",
map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
}
ssoUser := provider.GetUserFromJson(userData)
ssoEmail := ssoUser.Email
authData := ""
if ssoUser.AuthData != nil {
authData = *ssoUser.AuthData
}
if len(authData) == 0 {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]interface{}{"Service": service}, "", http.StatusBadRequest)
}
if len(email) == 0 {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "", http.StatusBadRequest)
}
user, err := a.Srv().Store.User().GetByEmail(email)
if err != nil {
return nil, err
}
if err = a.RevokeAllSessions(user.Id); err != nil {
return nil, err
}
if _, err = a.Srv().Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); err != nil {
return nil, err
}
a.Srv().Go(func() {
if err = a.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("error sending signin change email", mlog.Err(err))
}
})
return user, nil
}
func (a *App) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) {
token := model.NewToken(model.TOKEN_TYPE_OAUTH, extra)
if err := a.Srv().Store.Token().Save(token); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateOAuthStateToken", "app.recover.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return token, nil
}
func (a *App) GetOAuthStateToken(token string) (*model.Token, *model.AppError) {
mToken, err := a.Srv().Store.Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, err.Error(), http.StatusBadRequest)
}
if mToken.Type != model.TOKEN_TYPE_OAUTH {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, "", http.StatusBadRequest)
}
return mToken, nil
}
func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) {
sso := a.Config().GetSSOService(service)
if sso == nil || !*sso.Enable {
return "", model.NewAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented)
}
secure := false
if GetProtocol(r) == "https" {
secure = true
}
cookieValue := model.NewId()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(OAUTH_COOKIE_MAX_AGE_SECONDS), 0)
oauthCookie := &http.Cookie{
Name: COOKIE_OAUTH,
Value: cookieValue,
Path: subpath,
MaxAge: OAUTH_COOKIE_MAX_AGE_SECONDS,
Expires: expiresAt,
HttpOnly: true,
Secure: secure,
}
http.SetCookie(w, oauthCookie)
clientId := *sso.Id
endpoint := *sso.AuthEndpoint
scope := *sso.Scope
tokenExtra := generateOAuthStateTokenExtra(props["email"], props["action"], cookieValue)
stateToken, err := a.CreateOAuthStateToken(tokenExtra)
if err != nil {
return "", err
}
props["token"] = stateToken.Token
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
siteUrl := a.GetSiteURL()
if strings.TrimSpace(siteUrl) == "" {
siteUrl = GetProtocol(r) + "://" + r.Host
}
redirectUri := siteUrl + "/signup/" + service + "/complete"
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
if len(scope) > 0 {
authUrl += "&scope=" + utils.UrlEncode(scope)
}
if len(loginHint) > 0 {
authUrl += "&login_hint=" + utils.UrlEncode(loginHint)
}
return authUrl, nil
}
func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) {
sso := a.Config().GetSSOService(service)
if sso == nil || !*sso.Enable {
return nil, "", nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented)
}
b, strErr := b64.StdEncoding.DecodeString(state)
if strErr != nil {
return nil, "", nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, strErr.Error(), http.StatusBadRequest)
}
stateStr := string(b)
stateProps := model.MapFromJson(strings.NewReader(stateStr))
expectedToken, appErr := a.GetOAuthStateToken(stateProps["token"])
if appErr != nil {
return nil, "", stateProps, appErr
}
stateEmail := stateProps["email"]
stateAction := stateProps["action"]
if stateAction == model.OAUTH_ACTION_EMAIL_TO_SSO && stateEmail == "" {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
cookie, cookieErr := r.Cookie(COOKIE_OAUTH)
if cookieErr != nil {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
expectedTokenExtra := generateOAuthStateTokenExtra(stateEmail, stateAction, cookie.Value)
if expectedTokenExtra != expectedToken.Extra {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
appErr = a.DeleteToken(expectedToken)
if appErr != nil {
mlog.Error("error deleting token", mlog.Err(appErr))
}
subpath, _ := utils.GetSubpathFromConfig(a.Config())
httpCookie := &http.Cookie{
Name: COOKIE_OAUTH,
Value: "",
Path: subpath,
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, httpCookie)
teamId := stateProps["team_id"]
p := url.Values{}
p.Set("client_id", *sso.Id)
p.Set("client_secret", *sso.Secret)
p.Set("code", code)
p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
p.Set("redirect_uri", redirectUri)
req, requestErr := http.NewRequest("POST", *sso.TokenEndpoint, strings.NewReader(p.Encode()))
if requestErr != nil {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, requestErr.Error(), http.StatusInternalServerError)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer resp.Body.Close()
var buf bytes.Buffer
tee := io.TeeReader(resp.Body, &buf)
ar := model.AccessResponseFromJson(tee)
if ar == nil || resp.StatusCode != http.StatusOK {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, fmt.Sprintf("response_body=%s, status_code=%d", buf.String(), resp.StatusCode), http.StatusInternalServerError)
}
if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+buf.String(), http.StatusInternalServerError)
}
if len(ar.AccessToken) == 0 {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "response_body="+buf.String(), http.StatusInternalServerError)
}
p = url.Values{}
p.Set("access_token", ar.AccessToken)
req, requestErr = http.NewRequest("GET", *sso.UserApiEndpoint, strings.NewReader(""))
if requestErr != nil {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, requestErr.Error(), http.StatusInternalServerError)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+ar.AccessToken)
resp, err = a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, err.Error(), http.StatusInternalServerError)
} else if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
bodyBytes, _ := ioutil.ReadAll(resp.Body)
bodyString := string(bodyBytes)
mlog.Error("Error getting OAuth user", mlog.String("body_string", bodyString))
if service == model.SERVICE_GITLAB && resp.StatusCode == http.StatusForbidden && strings.Contains(bodyString, "Terms of Service") {
// Return a nicer error when the user hasn't accepted GitLab's terms of service
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "oauth.gitlab.tos.error", nil, "", http.StatusBadRequest)
}
return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.response.app_error", nil, "response_body="+bodyString, http.StatusInternalServerError)
}
// Note that resp.Body is not closed here, so it must be closed by the caller
return resp.Body, teamId, stateProps, nil
}
func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToOAuth", "api.user.email_to_oauth.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if err = a.CheckPasswordAndAllCriteria(user, password, code); err != nil {
return "", err
}
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO
stateProps["email"] = email
if service == model.USER_AUTH_SERVICE_SAML {
return a.GetSiteURL() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + utils.UrlEncode(email), nil
}
authUrl, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authUrl, nil
}
func (a *App) SwitchOAuthToEmail(email, password, requesterId string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("oauthToEmail", "api.user.oauth_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if user.Id != requesterId {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.oauth_to_email.context.app_error", nil, "", http.StatusForbidden)
}
if err := a.UpdatePassword(user, password); err != nil {
return "", err
}
T := utils.GetUserTranslations(user.Locale)
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("error sending signin change email", mlog.Err(err))
}
})
if err := a.RevokeAllSessions(requesterId); err != nil {
return "", err
}
return "/login?extra=signin_change", nil
}
func generateOAuthStateTokenExtra(email, action, cookie string) string {
return email + ":" + action + ":" + cookie
}