From 8b8aa2ca3c803b26fb4a1ba5f249111739376494 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Wed, 12 Apr 2017 16:29:42 -0400 Subject: Refactor OAuth 2.0 code into app layer (#6037) --- api/oauth.go | 676 +++++------------------------- api/oauth_test.go | 2 +- api/user.go | 50 +-- app/oauth.go | 477 ++++++++++++++++++++- app/team.go | 31 ++ model/access.go | 1 + store/sql_oauth_store.go | 24 +- store/sql_oauth_store_test.go | 12 +- store/sql_upgrade.go | 1 + store/sql_user_store.go | 3 +- store/store.go | 6 +- webapp/tests/client/client_oauth.test.jsx | 4 +- 12 files changed, 638 insertions(+), 649 deletions(-) diff --git a/api/oauth.go b/api/oauth.go index 8bd4a9f7b..fa076c56e 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -4,20 +4,14 @@ package api import ( - "fmt" - "io" - "io/ioutil" "net/http" "net/url" - "strconv" "strings" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/app" - "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) @@ -46,15 +40,8 @@ func InitOAuth() { } func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("registerOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) { - c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + c.Err = model.NewAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "", http.StatusForbidden) return } @@ -65,72 +52,50 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { return } - secret := model.NewId() - - oauthApp.ClientSecret = secret oauthApp.CreatorId = c.Session.UserId - if result := <-app.Srv.Store.OAuth().SaveApp(oauthApp); result.Err != nil { - c.Err = result.Err - return - } else { - oauthApp = result.Data.(*model.OAuthApp) - - c.LogAudit("client_id=" + oauthApp.Id) + rapp, err := app.CreateOAuthApp(oauthApp) - w.Write([]byte(oauthApp.ToJson())) + if err != nil { + c.Err = err return } + c.LogAudit("client_id=" + rapp.Id) + w.Write([]byte(rapp.ToJson())) } func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) { - c.Err = model.NewLocAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + c.Err = model.NewAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "", http.StatusForbidden) return } - var ochan store.StoreChannel + var apps []*model.OAuthApp + var err *model.AppError if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { - ochan = app.Srv.Store.OAuth().GetApps() + apps, err = app.GetOAuthApps(0, 100000) } else { - c.Err = nil - ochan = app.Srv.Store.OAuth().GetAppByUser(c.Session.UserId) + apps, err = app.GetOAuthAppsByCreator(c.Session.UserId, 0, 100000) } - if result := <-ochan; result.Err != nil { - c.Err = result.Err + if err != nil { + c.Err = err return - } else { - apps := result.Data.([]*model.OAuthApp) - w.Write([]byte(model.OAuthAppListToJson(apps))) } + + w.Write([]byte(model.OAuthAppListToJson(apps))) } func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - params := mux.Vars(r) - clientId := params["client_id"] - var oauthApp *model.OAuthApp - if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { - c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.database.app_error", nil, "") + oauthApp, err := app.GetOAuthApp(clientId) + + if err != nil { + c.Err = err return - } else { - oauthApp = result.Data.(*model.OAuthApp) } oauthApp.Sanitize() @@ -138,123 +103,49 @@ func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) { } func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - c.LogAudit("attempt") - - responseData := map[string]string{} - responseType := r.URL.Query().Get("response_type") if len(responseType) == 0 { - c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.bad_response.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest + c.Err = model.NewAppError("allowOAuth", "api.oauth.allow_oauth.bad_response.app_error", nil, "", http.StatusBadRequest) return } clientId := r.URL.Query().Get("client_id") if len(clientId) != 26 { - c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.bad_client.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest + c.Err = model.NewAppError("allowOAuth", "api.oauth.allow_oauth.bad_client.app_error", nil, "", http.StatusBadRequest) return } redirectUri := r.URL.Query().Get("redirect_uri") if len(redirectUri) == 0 { - c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.bad_redirect.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest + c.Err = model.NewAppError("allowOAuth", "api.oauth.allow_oauth.bad_redirect.app_error", nil, "", http.StatusBadRequest) return } scope := r.URL.Query().Get("scope") state := r.URL.Query().Get("state") - if len(scope) == 0 { - scope = model.DEFAULT_SCOPE - } - - var oauthApp *model.OAuthApp - if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { - c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.database.app_error", nil, "") - return - } else { - oauthApp = result.Data.(*model.OAuthApp) - } - - if !oauthApp.IsValidRedirectURL(redirectUri) { - c.LogAudit("fail - redirect_uri did not match registered callback") - c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest - return - } - - if responseType != model.AUTHCODE_RESPONSE_TYPE { - responseData["redirect"] = redirectUri + "?error=unsupported_response_type&state=" + state - w.Write([]byte(model.MapToJson(responseData))) - return - } - - authData := &model.AuthData{UserId: c.Session.UserId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope} - authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId)) - - // this saves the OAuth2 app as authorized - authorizedApp := model.Preference{ - UserId: c.Session.UserId, - Category: model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, - Name: clientId, - Value: scope, - } + c.LogAudit("attempt") - if result := <-app.Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil { - responseData["redirect"] = redirectUri + "?error=server_error&state=" + state - w.Write([]byte(model.MapToJson(responseData))) - return - } + redirectUrl, err := app.AllowOAuthAppAccessToUser(c.Session.UserId, responseType, clientId, redirectUri, scope, state) - if result := <-app.Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil { - responseData["redirect"] = redirectUri + "?error=server_error&state=" + state - w.Write([]byte(model.MapToJson(responseData))) + if err != nil { + c.Err = err return } - c.LogAudit("success") + c.LogAudit("") - responseData["redirect"] = redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State) - w.Write([]byte(model.MapToJson(responseData))) + w.Write([]byte(model.MapToJson(map[string]string{"redirect": redirectUrl}))) } func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getAuthorizedApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - ochan := app.Srv.Store.OAuth().GetAuthorizedApps(c.Session.UserId) - if result := <-ochan; result.Err != nil { - c.Err = result.Err + apps, err := app.GetAuthorizedAppsForUser(c.Session.UserId, 0, 10000) + if err != nil { + c.Err = err return - } else { - apps := result.Data.([]*model.OAuthApp) - for k, a := range apps { - a.Sanitize() - apps[k] = a - } - - w.Write([]byte(model.OAuthAppListToJson(apps))) } -} -func GetAuthData(code string) *model.AuthData { - if result := <-app.Srv.Store.OAuth().GetAuthData(code); result.Err != nil { - l4g.Error(utils.T("api.oauth.get_auth_data.find.error"), code) - return nil - } else { - return result.Data.(*model.AuthData) - } + w.Write([]byte(model.OAuthAppListToJson(apps))) } func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { @@ -271,60 +162,36 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete" - if body, teamId, props, err := app.AuthorizeOAuthUser(service, code, state, uri); err != nil { + body, teamId, props, err := app.AuthorizeOAuthUser(service, code, state, uri) + if err != nil { + c.Err = err + return + } + + user, err := app.CompleteOAuth(service, body, teamId, props) + if err != nil { c.Err = err return + } + + action := props["action"] + + var redirectUrl string + if action == model.OAUTH_ACTION_EMAIL_TO_SSO { + redirectUrl = c.GetSiteURLHeader() + "/login?extra=signin_change" + } else if action == model.OAUTH_ACTION_SSO_TO_EMAIL { + + redirectUrl = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"]) } else { - defer func() { - ioutil.ReadAll(body) - body.Close() - }() - - action := props["action"] - switch action { - case model.OAUTH_ACTION_SIGNUP: - if user, err := app.CreateOAuthUser(service, body, teamId); err != nil { - c.Err = err - } else { - doLogin(c, w, r, user, "") - } - if c.Err == nil { - http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_LOGIN: - user := LoginByOAuth(c, w, r, service, body) - if len(teamId) > 0 { - c.Err = app.AddUserToTeamByTeamId(teamId, user) - } - if c.Err == nil { - if val, ok := props["redirect_to"]; ok { - http.Redirect(w, r, c.GetSiteURLHeader()+val, http.StatusTemporaryRedirect) - return - } - http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_EMAIL_TO_SSO: - CompleteSwitchWithOAuth(c, w, r, service, body, props["email"]) - if c.Err == nil { - http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_SSO_TO_EMAIL: - LoginByOAuth(c, w, r, service, body) - if c.Err == nil { - http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) - } - break - default: - LoginByOAuth(c, w, r, service, body) - if c.Err == nil { - http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - } - break + doLogin(c, w, r, user, "") + if c.Err != nil { + return } + + redirectUrl = c.GetSiteURLHeader() } + + http.Redirect(w, r, redirectUrl, http.StatusTemporaryRedirect) } func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { @@ -371,42 +238,15 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { // Automatically allow if the app is trusted if oauthApp.IsTrusted || isAuthorized { - closeBody := func(r *http.Response) { - if r.Body != nil { - ioutil.ReadAll(r.Body) - r.Body.Close() - } - } - - doAllow := func() (*http.Response, *model.AppError) { - HttpClient := &http.Client{} - url := c.GetSiteURLHeader() + "/api/v3/oauth/allow?response_type=" + model.AUTHCODE_RESPONSE_TYPE + "&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirect) + "&scope=" + scope + "&state=" + url.QueryEscape(state) - rq, _ := http.NewRequest("GET", url, strings.NewReader("")) - - rq.Header.Set(model.HEADER_AUTH, model.HEADER_BEARER+" "+c.Session.Token) - - if rp, err := HttpClient.Do(rq); err != nil { - return nil, model.NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) - } else if rp.StatusCode == 304 { - return rp, nil - } else if rp.StatusCode >= 300 { - defer closeBody(rp) - return rp, model.AppErrorFromJson(rp.Body) - } else { - return rp, nil - } - } + redirectUrl, err := app.AllowOAuthAppAccessToUser(c.Session.UserId, model.AUTHCODE_RESPONSE_TYPE, clientId, redirect, scope, state) - if result, err := doAllow(); err != nil { + if err != nil { c.Err = err return - } else { - //defer closeBody(result) - data := model.MapFromJson(result.Body) - redirectTo := data["redirect"] - http.Redirect(w, r, redirectTo, http.StatusFound) - return } + + http.Redirect(w, r, redirectUrl, http.StatusFound) + return } w.Header().Set("Content-Type", "text/html") @@ -416,14 +256,6 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - c.LogAudit("attempt") - r.ParseForm() code := r.FormValue("code") @@ -458,140 +290,21 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } - var oauthApp *model.OAuthApp - achan := app.Srv.Store.OAuth().GetApp(clientId) - if result := <-achan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "") - return - } else { - oauthApp = result.Data.(*model.OAuthApp) - } - - if oauthApp.ClientSecret != secret { - c.LogAudit("fail - invalid client credentials") - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "") - return - } - - var user *model.User - var accessData *model.AccessData - var accessRsp *model.AccessResponse - if grantType == model.ACCESS_TOKEN_GRANT_TYPE { - redirectUri := r.FormValue("redirect_uri") - authData := GetAuthData(code) - - if authData == nil { - c.LogAudit("fail - invalid auth code") - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "") - return - } - - if authData.IsExpired() { - <-app.Srv.Store.OAuth().RemoveAuthData(authData.Code) - c.LogAudit("fail - auth code expired") - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "") - return - } - - if authData.RedirectUri != redirectUri { - c.LogAudit("fail - redirect uri provided did not match previous redirect uri") - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "") - return - } - - if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { - c.LogAudit("fail - auth code is invalid") - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "") - return - } - - uchan := app.Srv.Store.User().Get(authData.UserId) - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "") - return - } else { - user = result.Data.(*model.User) - } - - tchan := app.Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId) - if result := <-tchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "") - return - } else if result.Data != nil { - accessData := result.Data.(*model.AccessData) - if accessData.IsExpired() { - if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil { - c.Err = err - return - } else { - 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, - ExpiresIn: int32((accessData.ExpiresAt - model.GetMillis()) / 1000), - } - } - } else { - // create a new session and return new access token - var session *model.Session - if result, err := newSession(oauthApp.Name, user); err != nil { - c.Err = err - return - } else { - session = result - } - - accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt} - - if result := <-app.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { - l4g.Error(result.Err) - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "") - return - } - - accessRsp = &model.AccessResponse{ - AccessToken: session.Token, - TokenType: model.ACCESS_TOKEN_TYPE, - RefreshToken: accessData.RefreshToken, - ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24), - } - } - - <-app.Srv.Store.OAuth().RemoveAuthData(authData.Code) - } else { - // when grantType is refresh_token - if result := <-app.Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil { - c.LogAudit("fail - refresh token is invalid") - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "") - return - } else { - accessData = result.Data.(*model.AccessData) - } + redirectUri := r.FormValue("redirect_uri") - uchan := app.Srv.Store.User().Get(accessData.UserId) - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "") - return - } else { - user = result.Data.(*model.User) - } + c.LogAudit("attempt") - if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil { - c.Err = err - return - } else { - accessRsp = access - } + accessRsp, err := app.GetOAuthAccessToken(clientId, grantType, redirectUri, code, secret, refreshToken) + if err != nil { + c.Err = err + return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Pragma", "no-cache") - c.LogAuditWithUserId(user.Id, "success") + c.LogAudit("success") w.Write([]byte(accessRsp.ToJson())) } @@ -602,23 +315,13 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { loginHint := r.URL.Query().Get("login_hint") redirectTo := r.URL.Query().Get("redirect_to") - teamId, err := getTeamIdFromQuery(r.URL.Query()) + teamId, err := app.GetTeamIdFromQuery(r.URL.Query()) if err != nil { c.Err = err return } - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_LOGIN - if len(teamId) != 0 { - stateProps["team_id"] = teamId - } - - if len(redirectTo) != 0 { - stateProps["redirect_to"] = redirectTo - } - - if authUrl, err := app.GetAuthorizationCode(service, stateProps, loginHint); err != nil { + if authUrl, err := app.GetOAuthLoginEndpoint(service, teamId, redirectTo, loginHint); err != nil { c.Err = err return } else { @@ -626,59 +329,22 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } } -func getTeamIdFromQuery(query url.Values) (string, *model.AppError) { - hash := query.Get("h") - inviteId := query.Get("id") - - if len(hash) > 0 { - data := query.Get("d") - props := model.MapFromJson(strings.NewReader(data)) - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "") - } - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "") - } - - return props["id"], nil - } else if len(inviteId) > 0 { - if result := <-app.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { - // soft fail, so we still create user but don't auto-join team - l4g.Error("%v", result.Err) - } else { - return result.Data.(*model.Team).Id, nil - } - } - - return "", nil -} - func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupWithOAuth", "api.oauth.singup_with_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented + c.Err = model.NewAppError("signupWithOAuth", "api.oauth.singup_with_oauth.disabled.app_error", nil, "", http.StatusNotImplemented) return } - teamId, err := getTeamIdFromQuery(r.URL.Query()) + teamId, err := app.GetTeamIdFromQuery(r.URL.Query()) if err != nil { c.Err = err return } - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SIGNUP - if len(teamId) != 0 { - stateProps["team_id"] = teamId - } - - if authUrl, err := app.GetAuthorizationCode(service, stateProps, ""); err != nil { + if authUrl, err := app.GetOAuthSignupEndpoint(service, teamId); err != nil { c.Err = err return } else { @@ -686,95 +352,36 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } } -func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, email string) { - authData := "" - ssoEmail := "" - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error", - map[string]interface{}{"Service": strings.Title(service)}, "") - return - } else { - ssoUser := provider.GetUserFromJson(userData) - ssoEmail = ssoUser.Email - - if ssoUser.AuthData != nil { - authData = *ssoUser.AuthData - } - } - - if len(authData) == 0 { - c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error", - map[string]interface{}{"Service": service}, "") - return - } - - if len(email) == 0 { - c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "") - return - } - - var user *model.User - if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { - c.Err = result.Err - return - } else { - user = result.Data.(*model.User) - } - - if err := app.RevokeAllSessions(user.Id); err != nil { - c.Err = err - return - } - c.LogAuditWithUserId(user.Id, "Revoked all sessions for user") +func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) - if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil { - c.Err = result.Err + id := props["id"] + if len(id) == 0 { + c.SetInvalidParam("deleteOAuthApp", "id") return } - go func() { - if err := app.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, utils.GetSiteURL()); err != nil { - l4g.Error(err.Error()) - } - }() -} - -func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } + c.LogAudit("attempt") if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) { - c.Err = model.NewLocAppError("deleteOAuthApp", "api.command.admin_only.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + c.Err = model.NewAppError("deleteOAuthApp", "api.command.admin_only.app_error", nil, "", http.StatusForbidden) return } - c.LogAudit("attempt") - - props := model.MapFromJson(r.Body) - - id := props["id"] - if len(id) == 0 { - c.SetInvalidParam("deleteOAuthApp", "id") + oauthApp, err := app.GetOAuthApp(id) + if err != nil { + c.Err = err return } - if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil { - c.Err = result.Err + if c.Session.UserId != oauthApp.CreatorId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { + c.LogAudit("fail - inappropriate permissions") + c.Err = model.NewAppError("deleteOAuthApp", "api.oauth.delete.permissions.app_error", nil, "user_id="+c.Session.UserId, http.StatusForbidden) return - } else { - if c.Session.UserId != result.Data.(*model.OAuthApp).CreatorId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { - c.LogAudit("fail - inappropriate permissions") - c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.delete.permissions.app_error", nil, "user_id="+c.Session.UserId) - return - } } - if err := (<-app.Srv.Store.OAuth().DeleteApp(id)).Err; err != nil { + err = app.DeleteOAuthApp(id) + if err != nil { c.Err = err return } @@ -784,37 +391,11 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { } func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - params := mux.Vars(r) id := params["id"] - // revoke app sessions - if result := <-app.Srv.Store.OAuth().GetAccessDataByUserForApp(c.Session.UserId, id); result.Err != nil { - c.Err = result.Err - return - } else { - accessData := result.Data.([]*model.AccessData) - - for _, a := range accessData { - if err := app.RevokeAccessToken(a.Token); err != nil { - c.Err = err - return - } - - if rad := <-app.Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil { - c.Err = rad.Err - return - } - } - } - - // Deauthorize the app - if err := (<-app.Srv.Store.Preference().Delete(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, id)).Err; err != nil { + err := app.DeauthorizeOAuthAppForUser(c.Session.UserId, id) + if err != nil { c.Err = err return } @@ -824,78 +405,25 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { } func regenerateOAuthSecret(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("registerOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - params := mux.Vars(r) id := params["id"] - var oauthApp *model.OAuthApp - if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil { - c.Err = model.NewLocAppError("regenerateOAuthSecret", "api.oauth.allow_oauth.database.app_error", nil, "") - return - } else { - oauthApp = result.Data.(*model.OAuthApp) - - if oauthApp.CreatorId != c.Session.UserId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { - c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return - } - - oauthApp.ClientSecret = model.NewId() - if update := <-app.Srv.Store.OAuth().UpdateApp(oauthApp); update.Err != nil { - c.Err = update.Err - return - } - - w.Write([]byte(oauthApp.ToJson())) + oauthApp, err := app.GetOAuthApp(id) + if err != nil { + c.Err = err return } -} - -func 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.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays) - session.AddProp(model.SESSION_PROP_PLATFORM, appName) - session.AddProp(model.SESSION_PROP_OS, "OAuth2") - session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2") - - if result := <-app.Srv.Store.Session().Save(session); result.Err != nil { - return nil, model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_session.app_error", nil, "") - } else { - session = result.Data.(*model.Session) - app.AddSessionToCache(session) - } - - return session, nil -} -func newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) { - var session *model.Session - <-app.Srv.Store.Session().Remove(accessData.Token) //remove the previous session - - if result, err := newSession(appName, user); err != nil { - return nil, err - } else { - session = result + if oauthApp.CreatorId != c.Session.UserId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) { + c.Err = model.NewAppError("regenerateOAuthSecret", "api.command.admin_only.app_error", nil, "", http.StatusForbidden) + return } - accessData.Token = session.Token - accessData.ExpiresAt = session.ExpiresAt - if result := <-app.Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil { - l4g.Error(result.Err) - return nil, model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") - } - accessRsp := &model.AccessResponse{ - AccessToken: session.Token, - TokenType: model.ACCESS_TOKEN_TYPE, - ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24), + oauthApp, err = app.RegenerateOAuthAppSecret(oauthApp) + if err != nil { + c.Err = err + return } - return accessRsp, nil + w.Write([]byte(oauthApp.ToJson())) } diff --git a/api/oauth_test.go b/api/oauth_test.go index 7231c777d..18938b902 100644 --- a/api/oauth_test.go +++ b/api/oauth_test.go @@ -491,7 +491,7 @@ func TestOAuthAuthorize(t *testing.T) { } authToken := Client.AuthType + " " + Client.AuthToken - if r, err := HttpGet(Client.Url+"/oauth/authorize?client_id="+oauthApp.Id+"&&redirect_uri=http://example.com&response_type="+model.AUTHCODE_RESPONSE_TYPE, Client.HttpClient, authToken, true); err != nil { + if r, err := HttpGet(Client.Url+"/oauth/authorize?client_id="+oauthApp.Id+"&redirect_uri=http://example.com&response_type="+model.AUTHCODE_RESPONSE_TYPE, Client.HttpClient, authToken, true); err != nil { t.Fatal(err) closeBody(r) } diff --git a/api/user.go b/api/user.go index 466f12873..8b32dff36 100644 --- a/api/user.go +++ b/api/user.go @@ -4,10 +4,8 @@ package api import ( - "bytes" b64 "encoding/base64" "fmt" - "io" "net/http" "strconv" "strings" @@ -132,52 +130,6 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(user.ToJson())) } -func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader) *model.User { - buf := bytes.Buffer{} - buf.ReadFrom(userData) - - authData := "" - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewLocAppError("LoginByOAuth", "api.user.login_by_oauth.not_available.app_error", - map[string]interface{}{"Service": strings.Title(service)}, "") - return nil - } else { - authData = provider.GetAuthDataFromJson(bytes.NewReader(buf.Bytes())) - } - - if len(authData) == 0 { - c.Err = model.NewLocAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error", - map[string]interface{}{"Service": service}, "") - return nil - } - - var user *model.User - var err *model.AppError - if user, err = app.GetUserByAuth(&authData, service); err != nil { - if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { - if user, err = app.CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), ""); err != nil { - c.Err = err - return nil - } - } - c.Err = err - return nil - } - - if err = app.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service); err != nil { - c.Err = err - return nil - } - - doLogin(c, w, r, user, "") - if c.Err != nil { - return nil - } - - return user -} - // User MUST be authenticated completely before calling Login func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) { session, err := app.DoLogin(w, r, user, deviceId) @@ -1188,7 +1140,7 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) { return } - teamId, err := getTeamIdFromQuery(r.URL.Query()) + teamId, err := app.GetTeamIdFromQuery(r.URL.Query()) if err != nil { c.Err = err return diff --git a/app/oauth.go b/app/oauth.go index e79d240fe..260e4ac00 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -4,8 +4,10 @@ package app import ( + "bytes" "crypto/tls" b64 "encoding/base64" + "fmt" "io" "io/ioutil" "net/http" @@ -13,10 +15,371 @@ import ( "strings" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) +func CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return nil, model.NewAppError("CreateOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + secret := model.NewId() + app.ClientSecret = secret + + if result := <-Srv.Store.OAuth().SaveApp(app); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.OAuthApp), nil + } +} + +func GetOAuthApp(appId string) (*model.OAuthApp, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return nil, model.NewAppError("GetOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.OAuth().GetApp(appId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.OAuthApp), nil + } +} + +func DeleteOAuthApp(appId string) *model.AppError { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return model.NewAppError("DeleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + if err := (<-Srv.Store.OAuth().DeleteApp(appId)).Err; err != nil { + return err + } + + return nil +} + +func GetOAuthApps(page, perPage int) ([]*model.OAuthApp, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return nil, model.NewAppError("GetOAuthApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.OAuth().GetApps(page*perPage, perPage); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.OAuthApp), nil + } +} + +func GetOAuthAppsByCreator(userId string, page, perPage int) ([]*model.OAuthApp, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return nil, model.NewAppError("GetOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.OAuth().GetAppByUser(userId, page*perPage, perPage); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.OAuthApp), nil + } +} + +func AllowOAuthAppAccessToUser(userId, responseType, clientId, redirectUri, scope, state string) (string, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + if len(scope) == 0 { + scope = model.DEFAULT_SCOPE + } + + var oauthApp *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + return "", result.Err + } else { + oauthApp = result.Data.(*model.OAuthApp) + } + + if !oauthApp.IsValidRedirectURL(redirectUri) { + return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest) + } + + if responseType != model.AUTHCODE_RESPONSE_TYPE { + return redirectUri + "?error=unsupported_response_type&state=" + state, nil + } + + authData := &model.AuthData{UserId: userId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope} + authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, userId)) + + // this saves the OAuth2 app as authorized + authorizedApp := model.Preference{ + UserId: userId, + Category: model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, + Name: clientId, + Value: scope, + } + + if result := <-Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil { + return redirectUri + "?error=server_error&state=" + state, nil + } + + if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil { + return redirectUri + "?error=server_error&state=" + state, nil + } + + return redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State), nil +} + +func GetOAuthAccessToken(clientId, grantType, redirectUri, code, secret, refreshToken string) (*model.AccessResponse, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + var oauthApp *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound) + } else { + oauthApp = result.Data.(*model.OAuthApp) + } + + if oauthApp.ClientSecret != secret { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusForbidden) + } + + var user *model.User + var accessData *model.AccessData + var accessRsp *model.AccessResponse + if grantType == model.ACCESS_TOKEN_GRANT_TYPE { + + var authData *model.AuthData + if result := <-Srv.Store.OAuth().GetAuthData(code); result.Err != nil { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusInternalServerError) + } else { + authData = result.Data.(*model.AuthData) + } + + if authData.IsExpired() { + <-Srv.Store.OAuth().RemoveAuthData(authData.Code) + 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) + } + + if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusBadRequest) + } + + if result := <-Srv.Store.User().Get(authData.UserId); result.Err != nil { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound) + } else { + user = result.Data.(*model.User) + } + + if result := <-Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId); result.Err != nil { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "", http.StatusInternalServerError) + } else if result.Data != nil { + accessData := result.Data.(*model.AccessData) + if accessData.IsExpired() { + if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil { + return nil, err + } else { + 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, + ExpiresIn: int32((accessData.ExpiresAt - model.GetMillis()) / 1000), + } + } + } else { + // create a new session and return new access token + var session *model.Session + if result, err := newSession(oauthApp.Name, user); err != nil { + return nil, err + } else { + session = result + } + + accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt, Scope: authData.Scope} + + if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + 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(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24), + } + } + + <-Srv.Store.OAuth().RemoveAuthData(authData.Code) + } else { + // when grantType is refresh_token + if result := <-Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "", http.StatusNotFound) + } else { + accessData = result.Data.(*model.AccessData) + } + + if result := <-Srv.Store.User().Get(accessData.UserId); result.Err != nil { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound) + } else { + user = result.Data.(*model.User) + } + + if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil { + return nil, err + } else { + accessRsp = access + } + } + + return accessRsp, nil +} + +func 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.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays) + session.AddProp(model.SESSION_PROP_PLATFORM, appName) + session.AddProp(model.SESSION_PROP_OS, "OAuth2") + session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2") + + if result := <-Srv.Store.Session().Save(session); result.Err != nil { + return nil, model.NewAppError("newSession", "api.oauth.get_access_token.internal_session.app_error", nil, "", http.StatusInternalServerError) + } else { + session = result.Data.(*model.Session) + AddSessionToCache(session) + } + + return session, nil +} + +func newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) { + var session *model.Session + <-Srv.Store.Session().Remove(accessData.Token) //remove the previous session + + if result, err := newSession(appName, user); err != nil { + return nil, err + } else { + session = result + } + + accessData.Token = session.Token + accessData.ExpiresAt = session.ExpiresAt + if result := <-Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + return nil, model.NewAppError("newSessionUpdateToken", "web.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError) + } + accessRsp := &model.AccessResponse{ + AccessToken: session.Token, + TokenType: model.ACCESS_TOKEN_TYPE, + ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24), + } + + return accessRsp, nil +} + +func GetOAuthLoginEndpoint(service, teamId, redirectTo, loginHint string) (string, *model.AppError) { + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN + if len(teamId) != 0 { + stateProps["team_id"] = teamId + } + + if len(redirectTo) != 0 { + stateProps["redirect_to"] = redirectTo + } + + if authUrl, err := GetAuthorizationCode(service, stateProps, loginHint); err != nil { + return "", err + } else { + return authUrl, nil + } +} + +func GetOAuthSignupEndpoint(service, teamId string) (string, *model.AppError) { + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SIGNUP + if len(teamId) != 0 { + stateProps["team_id"] = teamId + } + + if authUrl, err := GetAuthorizationCode(service, stateProps, ""); err != nil { + return "", err + } else { + return authUrl, nil + } +} + +func GetAuthorizedAppsForUser(userId string, page, perPage int) ([]*model.OAuthApp, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return nil, model.NewAppError("GetAuthorizedAppsForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.OAuth().GetAuthorizedApps(userId, page*perPage, perPage); result.Err != nil { + return nil, result.Err + } else { + apps := result.Data.([]*model.OAuthApp) + for k, a := range apps { + a.Sanitize() + apps[k] = a + } + + return apps, nil + } +} + +func DeauthorizeOAuthAppForUser(userId, appId string) *model.AppError { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return model.NewAppError("DeauthorizeOAuthAppForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + // revoke app sessions + if result := <-Srv.Store.OAuth().GetAccessDataByUserForApp(userId, appId); result.Err != nil { + return result.Err + } else { + accessData := result.Data.([]*model.AccessData) + + for _, a := range accessData { + if err := RevokeAccessToken(a.Token); err != nil { + return err + } + + if rad := <-Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil { + return rad.Err + } + } + } + + // Deauthorize the app + if err := (<-Srv.Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, appId)).Err; err != nil { + return err + } + + return nil +} + +func RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + return nil, model.NewAppError("RegenerateOAuthAppSecret", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented) + } + + app.ClientSecret = model.NewId() + if update := <-Srv.Store.OAuth().UpdateApp(app); update.Err != nil { + return nil, update.Err + } + + return app, nil +} + func RevokeAccessToken(token string) *model.AppError { session, _ := GetSession(token) schan := Srv.Store.Session().Remove(token) @@ -42,10 +405,122 @@ func RevokeAccessToken(token string) *model.AppError { return nil } +func CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string) (*model.User, *model.AppError) { + defer func() { + ioutil.ReadAll(body) + body.Close() + }() + + action := props["action"] + + switch action { + case model.OAUTH_ACTION_SIGNUP: + return CreateOAuthUser(service, body, teamId) + case model.OAUTH_ACTION_LOGIN: + return LoginByOAuth(service, body, teamId) + case model.OAUTH_ACTION_EMAIL_TO_SSO: + return CompleteSwitchWithOAuth(service, body, props["email"]) + case model.OAUTH_ACTION_SSO_TO_EMAIL: + return LoginByOAuth(service, body, teamId) + default: + return LoginByOAuth(service, body, teamId) + } +} + +func LoginByOAuth(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) { + buf := bytes.Buffer{} + buf.ReadFrom(userData) + + authData := "" + 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) + } else { + authData = provider.GetAuthDataFromJson(bytes.NewReader(buf.Bytes())) + } + + 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 := GetUserByAuth(&authData, service) + if err != nil { + if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { + return CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), teamId) + } + return nil, err + } + + if err = UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service); err != nil { + return nil, err + } + + if len(teamId) > 0 { + err = AddUserToTeamByTeamId(teamId, user) + } + + if err != nil { + return nil, err + } + + return user, nil +} + +func CompleteSwitchWithOAuth(service string, userData io.ReadCloser, email string) (*model.User, *model.AppError) { + authData := "" + ssoEmail := "" + 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) + } else { + ssoUser := provider.GetUserFromJson(userData) + ssoEmail = ssoUser.Email + + 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) + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + return nil, result.Err + } else { + user = result.Data.(*model.User) + } + + if err := RevokeAllSessions(user.Id); err != nil { + return nil, err + } + + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil { + return nil, result.Err + } + + go func() { + if err := SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, utils.GetSiteURL()); err != nil { + l4g.Error(err.Error()) + } + }() + + return user, nil +} + func GetAuthorizationCode(service string, props map[string]string, loginHint string) (string, *model.AppError) { sso := utils.Cfg.GetSSOService(service) if sso != nil && !sso.Enable { - return "", model.NewLocAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service) + return "", model.NewAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented) } clientId := sso.Id diff --git a/app/team.go b/app/team.go index 327ab7f3e..d4e6d6308 100644 --- a/app/team.go +++ b/app/team.go @@ -6,6 +6,7 @@ package app import ( "fmt" "net/http" + "net/url" "strconv" "strings" @@ -747,3 +748,33 @@ func GetTeamStats(teamId string) (*model.TeamStats, *model.AppError) { return stats, nil } + +func GetTeamIdFromQuery(query url.Values) (string, *model.AppError) { + hash := query.Get("h") + inviteId := query.Get("id") + + if len(hash) > 0 { + data := query.Get("d") + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest) + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "", http.StatusBadRequest) + } + + return props["id"], nil + } else if len(inviteId) > 0 { + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + // soft fail, so we still create user but don't auto-join team + l4g.Error("%v", result.Err) + } else { + return result.Data.(*model.Team).Id, nil + } + } + + return "", nil +} diff --git a/model/access.go b/model/access.go index bab29dd84..9e16ed58b 100644 --- a/model/access.go +++ b/model/access.go @@ -21,6 +21,7 @@ type AccessData struct { RefreshToken string `json:"refresh_token"` RedirectUri string `json:"redirect_uri"` ExpiresAt int64 `json:"expires_at"` + Scope string `json:"scope"` } type AccessResponse struct { diff --git a/store/sql_oauth_store.go b/store/sql_oauth_store.go index bc97ee33a..6311b56dd 100644 --- a/store/sql_oauth_store.go +++ b/store/sql_oauth_store.go @@ -4,6 +4,7 @@ package store import ( + "net/http" "strings" "github.com/go-gorp/gorp" @@ -42,6 +43,7 @@ func NewSqlOAuthStore(sqlStore *SqlStore) OAuthStore { tableAccess.ColMap("Token").SetMaxSize(26) tableAccess.ColMap("RefreshToken").SetMaxSize(26) tableAccess.ColMap("RedirectUri").SetMaxSize(256) + tableAccess.ColMap("Scope").SetMaxSize(128) tableAccess.SetUniqueTogether("ClientId", "UserId") } @@ -138,9 +140,9 @@ func (as SqlOAuthStore) GetApp(id string) StoreChannel { result := StoreResult{} if obj, err := as.GetReplica().Get(model.OAuthApp{}, id); err != nil { - result.Err = model.NewLocAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.finding.app_error", nil, "app_id="+id+", "+err.Error()) + result.Err = model.NewAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.finding.app_error", nil, "app_id="+id+", "+err.Error(), http.StatusInternalServerError) } else if obj == nil { - result.Err = model.NewLocAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.find.app_error", nil, "app_id="+id) + result.Err = model.NewAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.find.app_error", nil, "app_id="+id, http.StatusNotFound) } else { result.Data = obj.(*model.OAuthApp) } @@ -153,7 +155,7 @@ func (as SqlOAuthStore) GetApp(id string) StoreChannel { return storeChannel } -func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel { +func (as SqlOAuthStore) GetAppByUser(userId string, offset, limit int) StoreChannel { storeChannel := make(StoreChannel, 1) @@ -162,8 +164,8 @@ func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel { var apps []*model.OAuthApp - if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = :UserId", map[string]interface{}{"UserId": userId}); err != nil { - result.Err = model.NewLocAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_app_by_user.find.app_error", nil, "user_id="+userId+", "+err.Error()) + if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = :UserId LIMIT :Limit OFFSET :Offset", map[string]interface{}{"UserId": userId, "Offset": offset, "Limit": limit}); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_app_by_user.find.app_error", nil, "user_id="+userId+", "+err.Error(), http.StatusInternalServerError) } result.Data = apps @@ -175,7 +177,7 @@ func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel { return storeChannel } -func (as SqlOAuthStore) GetApps() StoreChannel { +func (as SqlOAuthStore) GetApps(offset, limit int) StoreChannel { storeChannel := make(StoreChannel, 1) @@ -184,8 +186,8 @@ func (as SqlOAuthStore) GetApps() StoreChannel { var apps []*model.OAuthApp - if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps"); err != nil { - result.Err = model.NewLocAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error()) + if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps LIMIT :Limit OFFSET :Offset", map[string]interface{}{"Offset": offset, "Limit": limit}); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } result.Data = apps @@ -197,7 +199,7 @@ func (as SqlOAuthStore) GetApps() StoreChannel { return storeChannel } -func (as SqlOAuthStore) GetAuthorizedApps(userId string) StoreChannel { +func (as SqlOAuthStore) GetAuthorizedApps(userId string, offset, limit int) StoreChannel { storeChannel := make(StoreChannel, 1) go func() { @@ -207,8 +209,8 @@ func (as SqlOAuthStore) GetAuthorizedApps(userId string) StoreChannel { if _, err := as.GetReplica().Select(&apps, `SELECT o.* FROM OAuthApps AS o INNER JOIN - Preferences AS p ON p.Name=o.Id AND p.UserId=:UserId`, map[string]interface{}{"UserId": userId}); err != nil { - result.Err = model.NewLocAppError("SqlOAuthStore.GetAuthorizedApps", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error()) + Preferences AS p ON p.Name=o.Id AND p.UserId=:UserId LIMIT :Limit OFFSET :Offset`, map[string]interface{}{"UserId": userId, "Offset": offset, "Limit": limit}); err != nil { + result.Err = model.NewAppError("SqlOAuthStore.GetAuthorizedApps", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } result.Data = apps diff --git a/store/sql_oauth_store_test.go b/store/sql_oauth_store_test.go index d0c04c53f..8c707562f 100644 --- a/store/sql_oauth_store_test.go +++ b/store/sql_oauth_store_test.go @@ -56,7 +56,7 @@ func TestOAuthStoreGetApp(t *testing.T) { } // Lets try and get the app from a user that hasn't created any apps - if result := (<-store.OAuth().GetAppByUser("fake0123456789abcderfgret1")); result.Err == nil { + if result := (<-store.OAuth().GetAppByUser("fake0123456789abcderfgret1", 0, 1000)); result.Err == nil { if len(result.Data.([]*model.OAuthApp)) > 0 { t.Fatal("Should have failed. Fake user hasn't created any apps") } @@ -64,11 +64,11 @@ func TestOAuthStoreGetApp(t *testing.T) { t.Fatal(result.Err) } - if err := (<-store.OAuth().GetAppByUser(a1.CreatorId)).Err; err != nil { + if err := (<-store.OAuth().GetAppByUser(a1.CreatorId, 0, 1000)).Err; err != nil { t.Fatal(err) } - if err := (<-store.OAuth().GetApps()).Err; err != nil { + if err := (<-store.OAuth().GetApps(0, 1000)).Err; err != nil { t.Fatal(err) } } @@ -324,7 +324,7 @@ func TestOAuthGetAuthorizedApps(t *testing.T) { Must(store.OAuth().SaveApp(&a1)) // Lets try and get an Authorized app for a user who hasn't authorized it - if result := <-store.OAuth().GetAuthorizedApps("fake0123456789abcderfgret1"); result.Err == nil { + if result := <-store.OAuth().GetAuthorizedApps("fake0123456789abcderfgret1", 0, 1000); result.Err == nil { if len(result.Data.([]*model.OAuthApp)) > 0 { t.Fatal("Should have failed. Fake user hasn't authorized the app") } @@ -340,7 +340,7 @@ func TestOAuthGetAuthorizedApps(t *testing.T) { p.Value = "true" Must(store.Preference().Save(&model.Preferences{p})) - if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId); result.Err != nil { + if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000); result.Err != nil { t.Fatal(result.Err) } else { apps := result.Data.([]*model.OAuthApp) @@ -368,7 +368,7 @@ func TestOAuthGetAccessDataByUserForApp(t *testing.T) { p.Value = "true" Must(store.Preference().Save(&model.Preferences{p})) - if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId); result.Err != nil { + if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000); result.Err != nil { t.Fatal(result.Err) } else { apps := result.Data.([]*model.OAuthApp) diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go index dbdf12605..b25e7e73d 100644 --- a/store/sql_upgrade.go +++ b/store/sql_upgrade.go @@ -257,6 +257,7 @@ func UpgradeDatabaseToVersion38(sqlStore *SqlStore) { func UpgradeDatabaseToVersion39(sqlStore *SqlStore) { // TODO: Uncomment following condition when version 3.9.0 is released //if shouldPerformUpgrade(sqlStore, VERSION_3_8_0, VERSION_3_9_0) { + sqlStore.CreateColumnIfNotExists("OAuthAccessData", "Scope", "varchar(128)", "varchar(128)", model.DEFAULT_SCOPE) // saveSchemaVersion(sqlStore, VERSION_3_9_0) //} diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 52e45ed7d..5ea04155d 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -389,8 +389,7 @@ func (us SqlUserStore) Get(id string) StoreChannel { if obj, err := us.GetReplica().Get(model.User{}, id); err != nil { result.Err = model.NewLocAppError("SqlUserStore.Get", "store.sql_user.get.app_error", nil, "user_id="+id+", "+err.Error()) } else if obj == nil { - result.Err = model.NewLocAppError("SqlUserStore.Get", MISSING_ACCOUNT_ERROR, nil, "user_id="+id) - result.Err.StatusCode = http.StatusNotFound + result.Err = model.NewAppError("SqlUserStore.Get", MISSING_ACCOUNT_ERROR, nil, "user_id="+id, http.StatusNotFound) } else { result.Data = obj.(*model.User) } diff --git a/store/store.go b/store/store.go index b78d4a458..18f7374dc 100644 --- a/store/store.go +++ b/store/store.go @@ -246,9 +246,9 @@ type OAuthStore interface { SaveApp(app *model.OAuthApp) StoreChannel UpdateApp(app *model.OAuthApp) StoreChannel GetApp(id string) StoreChannel - GetAppByUser(userId string) StoreChannel - GetApps() StoreChannel - GetAuthorizedApps(userId string) StoreChannel + GetAppByUser(userId string, offset, limit int) StoreChannel + GetApps(offset, limit int) StoreChannel + GetAuthorizedApps(userId string, offset, limit int) StoreChannel DeleteApp(id string) StoreChannel SaveAuthData(authData *model.AuthData) StoreChannel GetAuthData(code string) StoreChannel diff --git a/webapp/tests/client/client_oauth.test.jsx b/webapp/tests/client/client_oauth.test.jsx index acf35f18f..44ce95a19 100644 --- a/webapp/tests/client/client_oauth.test.jsx +++ b/webapp/tests/client/client_oauth.test.jsx @@ -12,7 +12,7 @@ describe('Client.OAuth', function() { app.name = 'test'; app.homepage = 'homepage'; app.description = 'desc'; - app.callback_urls = ''; + app.callback_urls = ['']; TestHelper.basicClient().registerOAuthApp( app, @@ -33,7 +33,7 @@ describe('Client.OAuth', function() { TestHelper.basicClient().allowOAuth2( 'GET', - '123456', + '12345678901234567890123456', 'http://nowhere.com', 'state', 'scope', -- cgit v1.2.3-1-g7c22