diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/context.go | 11 | ||||
-rw-r--r-- | api/oauth.go | 489 | ||||
-rw-r--r-- | api/oauth_test.go | 274 | ||||
-rw-r--r-- | api/user.go | 15 |
4 files changed, 588 insertions, 201 deletions
diff --git a/api/context.go b/api/context.go index 6976feb8f..9a2f9b9ea 100644 --- a/api/context.go +++ b/api/context.go @@ -68,6 +68,10 @@ func UserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Han return &handler{h, true, false, false, false, false, false} } +func AppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{h, false, false, false, false, false, true} +} + func ApiAdminSystemRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { return &handler{h, true, true, true, false, false, false} } @@ -102,7 +106,6 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.RequestId = model.NewId() c.IpAddress = GetIpAddress(r) c.TeamId = mux.Vars(r)["team_id"] - h.isApi = IsApiCall(r) token := "" isTokenFromQueryString := false @@ -147,10 +150,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v", model.CurrentVersion, utils.CfgLastModified)) - // Instruct the browser not to display us in an iframe for anti-clickjacking + // Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking if !h.isApi { - w.Header().Set("X-Frame-Options", "DENY") - w.Header().Set("Content-Security-Policy", "frame-ancestors 'none'") + w.Header().Set("X-Frame-Options", "SAMEORIGIN") + w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'") } else { // All api response bodies will be JSON formatted by default w.Header().Set("Content-Type", "application/json") diff --git a/api/oauth.go b/api/oauth.go index d41f526b4..e2ea001a0 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -18,6 +18,7 @@ import ( "github.com/gorilla/mux" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) @@ -25,14 +26,17 @@ func InitOAuth() { l4g.Debug(utils.T("api.oauth.init.debug")) BaseRoutes.OAuth.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") + BaseRoutes.OAuth.Handle("/list", ApiUserRequired(getOAuthApps)).Methods("GET") + BaseRoutes.OAuth.Handle("/app/{client_id}", ApiUserRequired(getOAuthAppInfo)).Methods("GET") BaseRoutes.OAuth.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/delete", ApiUserRequired(deleteOAuthApp)).Methods("POST") BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") - BaseRoutes.Root.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/authorize", AppHandlerTrustRequester(authorizeOAuth)).Methods("GET") BaseRoutes.Root.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") // Handle all the old routes, to be later removed @@ -48,6 +52,14 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { return } + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !c.IsSystemAdmin() { + c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + app := model.OAuthAppFromJson(r.Body) if app == nil { @@ -65,7 +77,6 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { return } else { app = result.Data.(*model.OAuthApp) - app.ClientSecret = secret c.LogAudit("client_id=" + app.Id) @@ -75,6 +86,62 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { } +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 + } + + isSystemAdmin := c.IsSystemAdmin() + + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !isSystemAdmin { + c.Err = model.NewLocAppError("getOAuthAppsByUser", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + + var ochan store.StoreChannel + if isSystemAdmin { + ochan = Srv.Store.OAuth().GetApps() + } else { + ochan = Srv.Store.OAuth().GetAppByUser(c.Session.UserId) + } + + if result := <-ochan; result.Err != nil { + c.Err = result.Err + return + } else { + apps := result.Data.([]*model.OAuthApp) + 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 app *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.database.app_error", nil, "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + app.Sanitize() + w.Write([]byte(app.ToJson())) +} + 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, "") @@ -108,6 +175,10 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { scope := r.URL.Query().Get("scope") state := r.URL.Query().Get("state") + if len(scope) == 0 { + scope = model.DEFAULT_SCOPE + } + var app *model.OAuthApp if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.database.app_error", nil, "") @@ -131,6 +202,20 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { 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, + } + + if result := <-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 + } + if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil { responseData["redirect"] = redirectUri + "?error=server_error&state=" + state w.Write([]byte(model.MapToJson(responseData))) @@ -140,7 +225,7 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("success") responseData["redirect"] = redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State) - + w.Header().Set("Content-Type", "application/json") w.Write([]byte(model.MapToJson(responseData))) } @@ -149,24 +234,16 @@ func RevokeAccessToken(token string) *model.AppError { schan := Srv.Store.Session().Remove(token) sessionCache.Remove(token) - var accessData *model.AccessData if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil { return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "") - } else { - accessData = result.Data.(*model.AccessData) } tchan := Srv.Store.OAuth().RemoveAccessData(token) - cchan := Srv.Store.OAuth().RemoveAuthData(accessData.AuthCode) if result := <-tchan; result.Err != nil { return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "") } - if result := <-cchan; result.Err != nil { - return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_code.app_error", nil, "") - } - if result := <-schan; result.Err != nil { return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "") } @@ -215,6 +292,10 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = JoinUserToTeamById(teamId, user) } if c.Err == nil { + if val, ok := props["redirect_to"]; ok { + http.Redirect(w, r, c.GetSiteURL()+val, http.StatusTemporaryRedirect) + return + } http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } break @@ -242,7 +323,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") + c.Err = model.NewLocAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -253,8 +334,12 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { scope := r.URL.Query().Get("scope") state := r.URL.Query().Get("state") + if len(scope) == 0 { + scope = model.DEFAULT_SCOPE + } + if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") + c.Err = model.NewLocAppError("authorizeOAuth", "api.oauth.authorize_oauth.missing.app_error", nil, "") return } @@ -266,31 +351,67 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { app = result.Data.(*model.OAuthApp) } - var team *model.Team - if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil { - c.Err = result.Err + // here we should check if the user is logged in + if len(c.Session.UserId) == 0 { + http.Redirect(w, r, c.GetSiteURL()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound) return - } else { - team = result.Data.(*model.Team) } - page := utils.NewHTMLTemplate("authorize", c.Locale) - page.Props["Title"] = c.T("web.authorize_oauth.title") - page.Props["TeamName"] = team.Name - page.Props["AppName"] = app.Name - page.Props["ResponseType"] = responseType - page.Props["ClientId"] = clientId - page.Props["RedirectUri"] = redirect - page.Props["Scope"] = scope - page.Props["State"] = state - if err := page.RenderToWriter(w); err != nil { - c.SetUnknownError(page.TemplateName, err.Error()) + isAuthorized := false + if result := <-Srv.Store.Preference().Get(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, clientId); result.Err == nil { + // when we support scopes we should check if the scopes match + isAuthorized = true } + + // Automatically allow if the app is trusted + if app.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.GetSiteURL() + "/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 + } + } + + if result, err := doAllow(); 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 + } + } + + w.Header().Set("Content-Type", "text/html") + + w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public") + http.ServeFile(w, r, utils.FindDir("webapp/dist")+"root.html") } func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -299,126 +420,167 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { r.ParseForm() + code := r.FormValue("code") + refreshToken := r.FormValue("refresh_token") + grantType := r.FormValue("grant_type") - if grantType != model.ACCESS_TOKEN_GRANT_TYPE { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") + switch grantType { + case model.ACCESS_TOKEN_GRANT_TYPE: + if len(code) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "") + return + } + case model.REFRESH_TOKEN_GRANT_TYPE: + if len(refreshToken) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "") + return + } + default: + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "") return } clientId := r.FormValue("client_id") if len(clientId) != 26 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "") return } secret := r.FormValue("client_secret") if len(secret) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") - return - } - - code := r.FormValue("code") - if len(code) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") - return - } - - redirectUri := r.FormValue("redirect_uri") - - achan := Srv.Store.OAuth().GetApp(clientId) - tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code) - - authData := GetAuthData(code) - - if authData == nil { - c.LogAudit("fail - invalid auth code") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - uchan := Srv.Store.User().Get(authData.UserId) - - if authData.IsExpired() { - c.LogAudit("fail - auth code expired") - c.Err = model.NewLocAppError("getAccessToken", "web.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", "web.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", "web.get_access_token.expired_code.app_error", nil, "") + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.bad_client_secret.app_error", nil, "") return } var app *model.OAuthApp + achan := Srv.Store.OAuth().GetApp(clientId) if result := <-achan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "") return } else { app = result.Data.(*model.OAuthApp) } - if !model.ComparePassword(app.ClientSecret, secret) { + if app.ClientSecret != secret { c.LogAudit("fail - invalid client credentials") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "") return } - callback := redirectUri - if len(callback) == 0 { - callback = app.CallbackUrls[0] - } + 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 result := <-tchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") - return - } else if result.Data != nil { - c.LogAudit("fail - auth code has been used previously") - accessData := result.Data.(*model.AccessData) + if authData.IsExpired() { + <-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 + } - // Revoke access token, related auth code, and session from DB as well as from cache - if err := RevokeAccessToken(accessData.Token); err != nil { - l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) + 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 } - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.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 + } - var user *model.User - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") - return - } else { - user = result.Data.(*model.User) - } + uchan := 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) + } - session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true} + tchan := 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(app.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(app.Name, user); err != nil { + c.Err = err + return + } else { + session = result + } - if result := <-Srv.Store.Session().Save(session); result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") - return + accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt} + + if result := <-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), + } + } + + <-Srv.Store.OAuth().RemoveAuthData(authData.Code) } else { - session = result.Data.(*model.Session) - AddSessionToCache(session) - } + // when grantType is refresh_token + if result := <-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) + } - accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} + uchan := 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) + } - if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { - l4g.Error(result.Err) - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") - return + if access, err := newSessionUpdateToken(app.Name, accessData, user); err != nil { + c.Err = err + return + } else { + accessRsp = access + } } - accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} - w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Pragma", "no-cache") @@ -432,6 +594,7 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] loginHint := r.URL.Query().Get("login_hint") + redirectTo := r.URL.Query().Get("redirect_to") teamId, err := getTeamIdFromQuery(r.URL.Query()) if err != nil { @@ -445,6 +608,10 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { stateProps["team_id"] = teamId } + if len(redirectTo) != 0 { + stateProps["redirect_to"] = redirectTo + } + if authUrl, err := GetAuthorizationCode(c, service, stateProps, loginHint); err != nil { c.Err = err return @@ -462,12 +629,12 @@ func getTeamIdFromQuery(query url.Values) (string, *model.AppError) { props := model.MapFromJson(strings.NewReader(data)) if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - return "", model.NewLocAppError("getTeamIdFromQuery", "web.singup_with_oauth.invalid_link.app_error", nil, "") + 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", "web.singup_with_oauth.expired_link.app_error", nil, "") + return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "") } return props["id"], nil @@ -488,7 +655,7 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { service := params["service"] if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.disabled.app_error", nil, "") + c.Err = model.NewLocAppError("signupWithOAuth", "api.oauth.singup_with_oauth.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -666,3 +833,93 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), strings.Title(service)+" SSO") } + +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 + } + + isSystemAdmin := c.IsSystemAdmin() + + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !isSystemAdmin { + c.Err = model.NewLocAppError("deleteOAuthApp", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + + c.LogAudit("attempt") + + props := model.MapFromJson(r.Body) + + id := props["id"] + if len(id) == 0 { + c.SetInvalidParam("deleteOAuthApp", "id") + return + } + + if result := <-Srv.Store.OAuth().GetApp(id); result.Err != nil { + c.Err = result.Err + return + } else { + if c.Session.UserId != result.Data.(*model.OAuthApp).CreatorId && !isSystemAdmin { + 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 := (<-Srv.Store.OAuth().DeleteApp(id)).Err; err != nil { + c.Err = err + return + } + + c.LogAudit("success") + ReturnStatusOK(w) +} + +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.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_session.app_error", nil, "") + } 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.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), + } + + return accessRsp, nil +} diff --git a/api/oauth_test.go b/api/oauth_test.go index aa3c025a7..b719e17cc 100644 --- a/api/oauth_test.go +++ b/api/oauth_test.go @@ -11,131 +11,245 @@ import ( ) func TestRegisterApp(t *testing.T) { - th := Setup().InitBasic() - Client := th.BasicClient + th := Setup().InitBasic().InitSystemAdmin() + Client := th.SystemAdminClient app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - if _, err := Client.RegisterApp(app); err == nil { t.Fatal("should have failed - oauth providing turned off") } - } else { + } - Client.Logout() + utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true - if _, err := Client.RegisterApp(app); err == nil { - t.Fatal("not logged in - should have failed") - } + Client.Logout() - th.LoginBasic() + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("not logged in - should have failed") + } - if result, err := Client.RegisterApp(app); err != nil { - t.Fatal(err) - } else { - rapp := result.Data.(*model.OAuthApp) - if len(rapp.Id) != 26 { - t.Fatal("clientid didn't return properly") - } - if len(rapp.ClientSecret) != 26 { - t.Fatal("client secret didn't return properly") - } - } + th.LoginSystemAdmin() - app = &model.OAuthApp{Name: "", Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} - if _, err := Client.RegisterApp(app); err == nil { - t.Fatal("missing name - should have failed") + if result, err := Client.RegisterApp(app); err != nil { + t.Fatal(err) + } else { + rapp := result.Data.(*model.OAuthApp) + if len(rapp.Id) != 26 { + t.Fatal("clientid didn't return properly") } - - app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} - if _, err := Client.RegisterApp(app); err == nil { - t.Fatal("missing homepage - should have failed") + if len(rapp.ClientSecret) != 26 { + t.Fatal("client secret didn't return properly") } + } - app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{}} - if _, err := Client.RegisterApp(app); err == nil { - t.Fatal("missing callback url - should have failed") - } + app = &model.OAuthApp{Name: "", Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing name - should have failed") + } + + app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing homepage - should have failed") + } + + app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing callback url - should have failed") } } func TestAllowOAuth(t *testing.T) { - th := Setup().InitBasic() + th := Setup().InitBasic().InitSystemAdmin() Client := th.BasicClient + AdminClient := th.SystemAdminClient + utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + app = AdminClient.Must(AdminClient.RegisterApp(app)).Data.(*model.OAuthApp) state := "123" - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "12345678901234567890123456", app.CallbackUrls[0], "all", state); err == nil { - t.Fatal("should have failed - oauth service providing turned off") - } + utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = false + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - oauth providing turned off") + } + + utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true + + if result, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err != nil { + t.Fatal(err) } else { - app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp) + redirect := result.Data.(map[string]string)["redirect"] + if len(redirect) == 0 { + t.Fatal("redirect url should be set") + } - if result, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err != nil { - t.Fatal(err) + ru, _ := url.Parse(redirect) + if ru == nil { + t.Fatal("redirect url unparseable") } else { - redirect := result.Data.(map[string]string)["redirect"] - if len(redirect) == 0 { - t.Fatal("redirect url should be set") + if len(ru.Query().Get("code")) == 0 { + t.Fatal("authorization code not returned") } - - ru, _ := url.Parse(redirect) - if ru == nil { - t.Fatal("redirect url unparseable") - } else { - if len(ru.Query().Get("code")) == 0 { - t.Fatal("authorization code not returned") - } - if ru.Query().Get("state") != state { - t.Fatal("returned state doesn't match") - } + if ru.Query().Get("state") != state { + t.Fatal("returned state doesn't match") } } + } - if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "all", state); err == nil { - t.Fatal("should have failed - no redirect_url given") - } + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "all", state); err == nil { + t.Fatal("should have failed - no redirect_url given") + } - if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "", state); err == nil { - t.Fatal("should have failed - no redirect_url given") + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "", state); err == nil { + t.Fatal("should have failed - no redirect_url given") + } + + if result, err := Client.AllowOAuth("junk", app.Id, app.CallbackUrls[0], "all", state); err != nil { + t.Fatal(err) + } else { + redirect := result.Data.(map[string]string)["redirect"] + if len(redirect) == 0 { + t.Fatal("redirect url should be set") } - if result, err := Client.AllowOAuth("junk", app.Id, app.CallbackUrls[0], "all", state); err != nil { - t.Fatal(err) + ru, _ := url.Parse(redirect) + if ru == nil { + t.Fatal("redirect url unparseable") } else { - redirect := result.Data.(map[string]string)["redirect"] - if len(redirect) == 0 { - t.Fatal("redirect url should be set") + if ru.Query().Get("error") != "unsupported_response_type" { + t.Fatal("wrong error returned") } - - ru, _ := url.Parse(redirect) - if ru == nil { - t.Fatal("redirect url unparseable") - } else { - if ru.Query().Get("error") != "unsupported_response_type" { - t.Fatal("wrong error returned") - } - if ru.Query().Get("state") != state { - t.Fatal("returned state doesn't match") - } + if ru.Query().Get("state") != state { + t.Fatal("returned state doesn't match") } } + } - if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "", app.CallbackUrls[0], "all", state); err == nil { - t.Fatal("should have failed - empty client id") + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - empty client id") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "junk", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - bad client id") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://somewhereelse.com", "all", state); err == nil { + t.Fatal("should have failed - redirect uri host does not match app host") + } +} + +func TestGetOAuthAppsByUser(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + Client := th.BasicClient + AdminClient := th.SystemAdminClient + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + if _, err := Client.GetOAuthAppsByUser(); err == nil { + t.Fatal("should have failed - oauth providing turned off") } - if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "junk", app.CallbackUrls[0], "all", state); err == nil { - t.Fatal("should have failed - bad client id") + } + + utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true + + if _, err := Client.GetOAuthAppsByUser(); err == nil { + t.Fatal("Should have failed. only admin is permitted") + } + + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + + if result, err := Client.GetOAuthAppsByUser(); err != nil { + t.Fatal(err) + } else { + apps := result.Data.([]*model.OAuthApp) + + if len(apps) != 0 { + t.Fatal("incorrect number of apps should have been 0") } + } - if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://somewhereelse.com", "all", state); err == nil { - t.Fatal("should have failed - redirect uri host does not match app host") + app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp) + + if result, err := Client.GetOAuthAppsByUser(); err != nil { + t.Fatal(err) + } else { + apps := result.Data.([]*model.OAuthApp) + + if len(apps) != 1 { + t.Fatal("incorrect number of apps should have been 1") + } + } + + app = &model.OAuthApp{Name: "TestApp4" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + app = AdminClient.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp) + + if result, err := AdminClient.GetOAuthAppsByUser(); err != nil { + t.Fatal(err) + } else { + apps := result.Data.([]*model.OAuthApp) + + if len(apps) != 4 { + t.Fatal("incorrect number of apps should have been 4") + } + } +} + +func TestGetOAuthAppInfo(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + Client := th.BasicClient + AdminClient := th.SystemAdminClient + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + if _, err := Client.GetOAuthAppInfo("fakeId"); err == nil { + t.Fatal("should have failed - oauth providing turned off") } + + } + + utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true + + app := &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + app = AdminClient.Must(AdminClient.RegisterApp(app)).Data.(*model.OAuthApp) + + if _, err := Client.GetOAuthAppInfo(app.Id); err != nil { + t.Fatal(err) + } +} + +func TestOAuthDeleteApp(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + Client := th.BasicClient + AdminClient := th.SystemAdminClient + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + if _, err := Client.DeleteOAuthApp("fakeId"); err == nil { + t.Fatal("should have failed - oauth providing turned off") + } + + } + + utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + + app := &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp) + + if _, err := Client.DeleteOAuthApp(app.Id); err != nil { + t.Fatal(err) + } + + app = &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp) + + if _, err := AdminClient.DeleteOAuthApp(app.Id); err != nil { + t.Fatal(err) } } diff --git a/api/user.go b/api/user.go index 506c0ba44..5f3060b1e 100644 --- a/api/user.go +++ b/api/user.go @@ -2484,15 +2484,23 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) { return } action := r.URL.Query().Get("action") + redirectTo := r.URL.Query().Get("redirect_to") + relayProps := map[string]string{} relayState := "" if len(action) != 0 { - relayProps := map[string]string{} relayProps["team_id"] = teamId relayProps["action"] = action if action == model.OAUTH_ACTION_EMAIL_TO_SSO { relayProps["email"] = r.URL.Query().Get("email") } + } + + if len(redirectTo) != 0 { + relayProps["redirect_to"] = redirectTo + } + + if len(relayProps) > 0 { relayState = b64.StdEncoding.EncodeToString([]byte(model.MapToJson(relayProps))) } @@ -2555,6 +2563,11 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { break } doLogin(c, w, r, user, "") + + if val, ok := relayProps["redirect_to"]; ok { + http.Redirect(w, r, c.GetSiteURL()+val, http.StatusFound) + return + } http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusFound) } } |