diff --git a/api4/oauth.go b/api4/oauth.go index a8008f3f23..2c4bd7123d 100644 --- a/api4/oauth.go +++ b/api4/oauth.go @@ -5,15 +5,8 @@ package api4 import ( "net/http" - "net/url" - "path/filepath" - "strings" - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/mattermost/mattermost-server/utils/fileutils" ) func (api *API) InitOAuth() { @@ -26,23 +19,6 @@ func (api *API) InitOAuth() { api.BaseRoutes.OAuthApp.Handle("/regen_secret", api.ApiSessionRequired(regenerateOAuthAppSecret)).Methods("POST") api.BaseRoutes.User.Handle("/oauth/apps/authorized", api.ApiSessionRequired(getAuthorizedOAuthApps)).Methods("GET") - - // API version independent OAuth 2.0 as a service provider endpoints - api.BaseRoutes.Root.Handle("/oauth/authorize", api.ApiHandlerTrustRequester(authorizeOAuthPage)).Methods("GET") - api.BaseRoutes.Root.Handle("/oauth/authorize", api.ApiSessionRequired(authorizeOAuthApp)).Methods("POST") - api.BaseRoutes.Root.Handle("/oauth/deauthorize", api.ApiSessionRequired(deauthorizeOAuthApp)).Methods("POST") - api.BaseRoutes.Root.Handle("/oauth/access_token", api.ApiHandlerTrustRequester(getAccessToken)).Methods("POST") - - // API version independent OAuth as a client endpoints - api.BaseRoutes.Root.Handle("/oauth/{service:[A-Za-z0-9]+}/complete", api.ApiHandler(completeOAuth)).Methods("GET") - api.BaseRoutes.Root.Handle("/oauth/{service:[A-Za-z0-9]+}/login", api.ApiHandler(loginWithOAuth)).Methods("GET") - api.BaseRoutes.Root.Handle("/oauth/{service:[A-Za-z0-9]+}/mobile_login", api.ApiHandler(mobileLoginWithOAuth)).Methods("GET") - api.BaseRoutes.Root.Handle("/oauth/{service:[A-Za-z0-9]+}/signup", api.ApiHandler(signupWithOAuth)).Methods("GET") - - // Old endpoints for backwards compatibility, needed to not break SSO for any old setups - api.BaseRoutes.Root.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", api.ApiHandler(completeOAuth)).Methods("GET") - api.BaseRoutes.Root.Handle("/signup/{service:[A-Za-z0-9]+}/complete", api.ApiHandler(completeOAuth)).Methods("GET") - api.BaseRoutes.Root.Handle("/login/{service:[A-Za-z0-9]+}/complete", api.ApiHandler(completeOAuth)).Methods("GET") } func createOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -273,349 +249,3 @@ func getAuthorizedOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) w.Write([]byte(model.OAuthAppListToJson(apps))) } - -func authorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { - authRequest := model.AuthorizeRequestFromJson(r.Body) - if authRequest == nil { - c.SetInvalidParam("authorize_request") - } - - if err := authRequest.IsValid(); err != nil { - c.Err = err - return - } - - if c.App.Session.IsOAuth { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - c.Err.DetailedError += ", attempted access by oauth app" - return - } - - c.LogAudit("attempt") - - redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.App.Session.UserId, authRequest) - - if err != nil { - c.Err = err - return - } - - c.LogAudit("") - - w.Write([]byte(model.MapToJson(map[string]string{"redirect": redirectUrl}))) -} - -func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { - requestData := model.MapFromJson(r.Body) - clientId := requestData["client_id"] - - if len(clientId) != 26 { - c.SetInvalidParam("client_id") - return - } - - err := c.App.DeauthorizeOAuthAppForUser(c.App.Session.UserId, clientId) - if err != nil { - c.Err = err - return - } - - c.LogAudit("success") - ReturnStatusOK(w) -} - -func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { - if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider { - err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented) - utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) - return - } - - authRequest := &model.AuthorizeRequest{ - ResponseType: r.URL.Query().Get("response_type"), - ClientId: r.URL.Query().Get("client_id"), - RedirectUri: r.URL.Query().Get("redirect_uri"), - Scope: r.URL.Query().Get("scope"), - State: r.URL.Query().Get("state"), - } - - loginHint := r.URL.Query().Get("login_hint") - - if err := authRequest.IsValid(); err != nil { - utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) - return - } - - oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId) - if err != nil { - utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) - return - } - - // here we should check if the user is logged in - if len(c.App.Session.UserId) == 0 { - if loginHint == model.USER_AUTH_SERVICE_SAML { - http.Redirect(w, r, c.GetSiteURLHeader()+"/login/sso/saml?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound) - } else { - http.Redirect(w, r, c.GetSiteURLHeader()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound) - } - return - } - - if !oauthApp.IsValidRedirectURL(authRequest.RedirectUri) { - err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest) - utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) - return - } - - isAuthorized := false - - if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.App.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, authRequest.ClientId); err == nil { - // when we support scopes we should check if the scopes match - isAuthorized = true - } - - // Automatically allow if the app is trusted - if oauthApp.IsTrusted || isAuthorized { - redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.App.Session.UserId, authRequest) - - if err != nil { - utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) - return - } - - http.Redirect(w, r, redirectUrl, http.StatusFound) - return - } - - w.Header().Set("X-Frame-Options", "SAMEORIGIN") - w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'") - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public") - - staticDir, _ := fileutils.FindDir(model.CLIENT_DIR) - http.ServeFile(w, r, filepath.Join(staticDir, "root.html")) -} - -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") - switch grantType { - case model.ACCESS_TOKEN_GRANT_TYPE: - if len(code) == 0 { - c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "", http.StatusBadRequest) - return - } - case model.REFRESH_TOKEN_GRANT_TYPE: - if len(refreshToken) == 0 { - c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "", http.StatusBadRequest) - return - } - default: - c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "", http.StatusBadRequest) - return - } - - clientId := r.FormValue("client_id") - if len(clientId) != 26 { - c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "", http.StatusBadRequest) - return - } - - secret := r.FormValue("client_secret") - if len(secret) == 0 { - c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_secret.app_error", nil, "", http.StatusBadRequest) - return - } - - redirectUri := r.FormValue("redirect_uri") - - c.LogAudit("attempt") - - accessRsp, err := c.App.GetOAuthAccessTokenForCodeFlow(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.LogAudit("success") - - w.Write([]byte(accessRsp.ToJson())) -} - -func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireService() - if c.Err != nil { - return - } - - service := c.Params.Service - - oauthError := r.URL.Query().Get("error") - if oauthError == "access_denied" { - utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{ - "type": []string{"oauth_access_denied"}, - "service": []string{strings.Title(service)}, - }, c.App.AsymmetricSigningKey()) - return - } - - code := r.URL.Query().Get("code") - if len(code) == 0 { - utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{ - "type": []string{"oauth_missing_code"}, - "service": []string{strings.Title(service)}, - }, c.App.AsymmetricSigningKey()) - return - } - - state := r.URL.Query().Get("state") - - uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete" - - body, teamId, props, err := c.App.AuthorizeOAuthUser(w, r, service, code, state, uri) - - action := "" - if props != nil { - action = props["action"] - } - - if err != nil { - err.Translate(c.App.T) - mlog.Error(err.Error()) - if action == model.OAUTH_ACTION_MOBILE { - w.Write([]byte(err.ToJson())) - } else { - utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) - } - return - } - - user, err := c.App.CompleteOAuth(service, body, teamId, props) - if err != nil { - err.Translate(c.App.T) - mlog.Error(err.Error()) - if action == model.OAUTH_ACTION_MOBILE { - w.Write([]byte(err.ToJson())) - } else { - utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) - } - return - } - - 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 { - session, err := c.App.DoLogin(w, r, user, "") - if err != nil { - err.Translate(c.App.T) - c.Err = err - if action == model.OAUTH_ACTION_MOBILE { - w.Write([]byte(err.ToJson())) - } - return - } - - c.App.AttachSessionCookies(w, r, session) - - c.App.Session = *session - - if _, ok := props["redirect_to"]; ok { - redirectUrl = props["redirect_to"] - } else { - redirectUrl = c.GetSiteURLHeader() - } - } - - if action == model.OAUTH_ACTION_MOBILE { - ReturnStatusOK(w) - return - } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - http.Redirect(w, r, redirectUrl, http.StatusTemporaryRedirect) -} - -func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireService() - if c.Err != nil { - return - } - - loginHint := r.URL.Query().Get("login_hint") - redirectTo := r.URL.Query().Get("redirect_to") - - teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query()) - if err != nil { - c.Err = err - return - } - - authUrl, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAUTH_ACTION_LOGIN, redirectTo, loginHint) - if err != nil { - c.Err = err - return - } - - http.Redirect(w, r, authUrl, http.StatusFound) -} - -func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireService() - if c.Err != nil { - return - } - - teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query()) - if err != nil { - c.Err = err - return - } - - authUrl, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAUTH_ACTION_MOBILE, "", "") - if err != nil { - c.Err = err - return - } - - http.Redirect(w, r, authUrl, http.StatusFound) -} - -func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireService() - if c.Err != nil { - return - } - - if !*c.App.Config().TeamSettings.EnableUserCreation { - utils.RenderWebError(c.App.Config(), w, r, http.StatusBadRequest, url.Values{ - "message": []string{utils.T("api.oauth.singup_with_oauth.disabled.app_error")}, - }, c.App.AsymmetricSigningKey()) - return - } - - teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query()) - if err != nil { - c.Err = err - return - } - - authUrl, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId) - if err != nil { - c.Err = err - return - } - - http.Redirect(w, r, authUrl, http.StatusFound) -} diff --git a/api4/oauth_test.go b/api4/oauth_test.go index a0246d5475..184e146b43 100644 --- a/api4/oauth_test.go +++ b/api4/oauth_test.go @@ -4,22 +4,12 @@ package api4 import ( - "encoding/base64" - "io" "io/ioutil" "net/http" - "net/http/httptest" - "net/url" "strconv" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/mattermost/mattermost-server/web" ) func TestCreateOAuthApp(t *testing.T) { @@ -642,578 +632,9 @@ func TestGetAuthorizedOAuthAppsForUser(t *testing.T) { CheckNoError(t, resp) } -func TestAuthorizeOAuthApp(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() - Client := th.Client - AdminClient := th.SystemAdminClient - - enableOAuth := *th.App.Config().ServiceSettings.EnableOAuthServiceProvider - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) - }() - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) - - oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} - - rapp, resp := AdminClient.CreateOAuthApp(oapp) - CheckNoError(t, resp) - - authRequest := &model.AuthorizeRequest{ - ResponseType: model.AUTHCODE_RESPONSE_TYPE, - ClientId: rapp.Id, - RedirectUri: rapp.CallbackUrls[0], - Scope: "", - State: "123", - } - - // Test auth code flow - ruri, resp := Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - - if len(ruri) == 0 { - t.Fatal("redirect url should be set") - } - - ru, _ := url.Parse(ruri) - 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") != authRequest.State { - t.Fatal("returned state doesn't match") - } - } - - // Test implicit flow - authRequest.ResponseType = model.IMPLICIT_RESPONSE_TYPE - ruri, resp = Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - require.False(t, len(ruri) == 0, "redirect url should be set") - - ru, _ = url.Parse(ruri) - require.NotNil(t, ru, "redirect url unparseable") - values, err := url.ParseQuery(ru.Fragment) - require.Nil(t, err) - assert.False(t, len(values.Get("access_token")) == 0, "access_token not returned") - assert.Equal(t, authRequest.State, values.Get("state"), "returned state doesn't match") - - oldToken := Client.AuthToken - Client.AuthToken = values.Get("access_token") - _, resp = Client.AuthorizeOAuthApp(authRequest) - CheckForbiddenStatus(t, resp) - - Client.AuthToken = oldToken - - authRequest.RedirectUri = "" - _, resp = Client.AuthorizeOAuthApp(authRequest) - CheckBadRequestStatus(t, resp) - - authRequest.RedirectUri = "http://somewhereelse.com" - _, resp = Client.AuthorizeOAuthApp(authRequest) - CheckBadRequestStatus(t, resp) - - authRequest.RedirectUri = rapp.CallbackUrls[0] - authRequest.ResponseType = "" - _, resp = Client.AuthorizeOAuthApp(authRequest) - CheckBadRequestStatus(t, resp) - - authRequest.ResponseType = model.AUTHCODE_RESPONSE_TYPE - authRequest.ClientId = "" - _, resp = Client.AuthorizeOAuthApp(authRequest) - CheckBadRequestStatus(t, resp) - - authRequest.ClientId = model.NewId() - _, resp = Client.AuthorizeOAuthApp(authRequest) - CheckNotFoundStatus(t, resp) -} - -func TestDeauthorizeOAuthApp(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() - Client := th.Client - AdminClient := th.SystemAdminClient - - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) - }() - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) - - oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} - - rapp, resp := AdminClient.CreateOAuthApp(oapp) - CheckNoError(t, resp) - - authRequest := &model.AuthorizeRequest{ - ResponseType: model.AUTHCODE_RESPONSE_TYPE, - ClientId: rapp.Id, - RedirectUri: rapp.CallbackUrls[0], - Scope: "", - State: "123", - } - - _, resp = Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - - pass, resp := Client.DeauthorizeOAuthApp(rapp.Id) - CheckNoError(t, resp) - - if !pass { - t.Fatal("should have passed") - } - - _, resp = Client.DeauthorizeOAuthApp("junk") - CheckBadRequestStatus(t, resp) - - _, resp = Client.DeauthorizeOAuthApp(model.NewId()) - CheckNoError(t, resp) - - Client.Logout() - _, resp = Client.DeauthorizeOAuthApp(rapp.Id) - CheckUnauthorizedStatus(t, resp) -} - -func TestOAuthAccessToken(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - - th := Setup().InitBasic() - defer th.TearDown() - - Client := th.Client - - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) - }() - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) - - defaultRolePermissions := th.SaveDefaultRolePermissions() - defer func() { - th.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.TEAM_USER_ROLE_ID) - th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) - - oauthApp := &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} - oauthApp = Client.Must(Client.CreateOAuthApp(oauthApp)).(*model.OAuthApp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false }) - data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}} - - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Log(resp.StatusCode) - t.Fatal("should have failed - oauth providing turned off") - } - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) - - authRequest := &model.AuthorizeRequest{ - ResponseType: model.AUTHCODE_RESPONSE_TYPE, - ClientId: oauthApp.Id, - RedirectUri: oauthApp.CallbackUrls[0], - Scope: "all", - State: "123", - } - - redirect, resp := Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - rurl, _ := url.Parse(redirect) - - Client.Logout() - - data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{oauthApp.Id}, "client_secret": []string{oauthApp.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}} - - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - bad grant type") - } - - data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) - data.Set("client_id", "") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - missing client id") - } - data.Set("client_id", "junk") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - bad client id") - } - - data.Set("client_id", oauthApp.Id) - data.Set("client_secret", "") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - missing client secret") - } - - data.Set("client_secret", "junk") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - bad client secret") - } - - data.Set("client_secret", oauthApp.ClientSecret) - data.Set("code", "") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - missing code") - } - - data.Set("code", "junk") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - bad code") - } - - data.Set("code", rurl.Query().Get("code")) - data.Set("redirect_uri", "junk") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - non-matching redirect uri") - } - - // reset data for successful request - data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) - data.Set("client_id", oauthApp.Id) - data.Set("client_secret", oauthApp.ClientSecret) - data.Set("code", rurl.Query().Get("code")) - data.Set("redirect_uri", oauthApp.CallbackUrls[0]) - - token := "" - refreshToken := "" - if rsp, resp := Client.GetOAuthAccessToken(data); resp.Error != nil { - t.Fatal(resp.Error) - } else { - if len(rsp.AccessToken) == 0 { - t.Fatal("access token not returned") - } else if len(rsp.RefreshToken) == 0 { - t.Fatal("refresh token not returned") - } else { - token = rsp.AccessToken - refreshToken = rsp.RefreshToken - } - if rsp.TokenType != model.ACCESS_TOKEN_TYPE { - t.Fatal("access token type incorrect") - } - } - - if _, err := Client.DoApiGet("/users?page=0&per_page=100&access_token="+token, ""); err != nil { - t.Fatal(err) - } - - if _, resp := Client.GetUsers(0, 100, ""); resp.Error == nil { - t.Fatal("should have failed - no access token provided") - } - - if _, resp := Client.GetUsers(0, 100, ""); resp.Error == nil { - t.Fatal("should have failed - bad access token provided") - } - - Client.SetOAuthToken(token) - if users, resp := Client.GetUsers(0, 100, ""); resp.Error != nil { - t.Fatal(resp.Error) - } else { - if len(users) == 0 { - t.Fatal("users empty - did not get results correctly") - } - } - - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("should have failed - tried to reuse auth code") - } - - data.Set("grant_type", model.REFRESH_TOKEN_GRANT_TYPE) - data.Set("client_id", oauthApp.Id) - data.Set("client_secret", oauthApp.ClientSecret) - data.Set("refresh_token", "") - data.Set("redirect_uri", oauthApp.CallbackUrls[0]) - data.Del("code") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("Should have failed - refresh token empty") - } - - data.Set("refresh_token", refreshToken) - if rsp, resp := Client.GetOAuthAccessToken(data); resp.Error != nil { - t.Fatal(resp.Error) - } else { - if len(rsp.AccessToken) == 0 { - t.Fatal("access token not returned") - } else if len(rsp.RefreshToken) == 0 { - t.Fatal("refresh token not returned") - } else if rsp.RefreshToken == refreshToken { - t.Fatal("refresh token did not update") - } - - if rsp.TokenType != model.ACCESS_TOKEN_TYPE { - t.Fatal("access token type incorrect") - } - Client.SetOAuthToken(rsp.AccessToken) - _, resp = Client.GetMe("") - if resp.Error != nil { - t.Fatal(resp.Error) - } - - data.Set("refresh_token", rsp.RefreshToken) - } - - if rsp, resp := Client.GetOAuthAccessToken(data); resp.Error != nil { - t.Fatal(resp.Error) - } else { - if len(rsp.AccessToken) == 0 { - t.Fatal("access token not returned") - } else if len(rsp.RefreshToken) == 0 { - t.Fatal("refresh token not returned") - } else if rsp.RefreshToken == refreshToken { - t.Fatal("refresh token did not update") - } - - if rsp.TokenType != model.ACCESS_TOKEN_TYPE { - t.Fatal("access token type incorrect") - } - Client.SetOAuthToken(rsp.AccessToken) - _, resp = Client.GetMe("") - if resp.Error != nil { - t.Fatal(resp.Error) - } - } - - authData := &model.AuthData{ClientId: oauthApp.Id, RedirectUri: oauthApp.CallbackUrls[0], UserId: th.BasicUser.Id, Code: model.NewId(), ExpiresIn: -1} - _, err := th.App.Srv.Store.OAuth().SaveAuthData(authData) - require.Nil(t, err) - - data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) - data.Set("client_id", oauthApp.Id) - data.Set("client_secret", oauthApp.ClientSecret) - data.Set("redirect_uri", oauthApp.CallbackUrls[0]) - data.Set("code", authData.Code) - data.Del("refresh_token") - if _, resp := Client.GetOAuthAccessToken(data); resp.Error == nil { - t.Fatal("Should have failed - code is expired") - } - - Client.ClearOAuthToken() -} - -func TestOAuthComplete(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - - th := Setup().InitBasic() - defer th.TearDown() - - Client := th.Client - - gitLabSettingsEnable := th.App.Config().GitLabSettings.Enable - gitLabSettingsAuthEndpoint := th.App.Config().GitLabSettings.AuthEndpoint - gitLabSettingsId := th.App.Config().GitLabSettings.Id - gitLabSettingsSecret := th.App.Config().GitLabSettings.Secret - gitLabSettingsTokenEndpoint := th.App.Config().GitLabSettings.TokenEndpoint - gitLabSettingsUserApiEndpoint := th.App.Config().GitLabSettings.UserApiEndpoint - enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Enable = gitLabSettingsEnable }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.AuthEndpoint = gitLabSettingsAuthEndpoint }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Id = gitLabSettingsId }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Secret = gitLabSettingsSecret }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.TokenEndpoint = gitLabSettingsTokenEndpoint }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.UserApiEndpoint = gitLabSettingsUserApiEndpoint }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider }) - }() - - r, err := HttpGet(Client.Url+"/login/gitlab/complete?code=123", Client.HttpClient, "", true) - assert.NotNil(t, err) - closeBody(r) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = true }) - r, err = HttpGet(Client.Url+"/login/gitlab/complete?code=123&state=!#$#F@#Yˆ&~ñ", Client.HttpClient, "", true) - assert.NotNil(t, err) - closeBody(r) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.AuthEndpoint = Client.Url + "/oauth/authorize" }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Id = model.NewId() }) - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_LOGIN - stateProps["team_id"] = th.BasicTeam.Id - stateProps["redirect_to"] = *th.App.Config().GitLabSettings.AuthEndpoint - - state := base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) - r, err = HttpGet(Client.Url+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), Client.HttpClient, "", true) - assert.NotNil(t, err) - closeBody(r) - - stateProps["hash"] = utils.HashSha256(*th.App.Config().GitLabSettings.Id) - state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) - r, err = HttpGet(Client.Url+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), Client.HttpClient, "", true) - assert.NotNil(t, err) - closeBody(r) - - // We are going to use mattermost as the provider emulating gitlab - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) - - defaultRolePermissions := th.SaveDefaultRolePermissions() - defer func() { - th.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.TEAM_USER_ROLE_ID) - th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) - - oauthApp := &model.OAuthApp{ - Name: "TestApp5" + model.NewId(), - Homepage: "https://nowhere.com", - Description: "test", - CallbackUrls: []string{ - Client.Url + "/signup/" + model.SERVICE_GITLAB + "/complete", - Client.Url + "/login/" + model.SERVICE_GITLAB + "/complete", - }, - IsTrusted: true, - } - oauthApp = Client.Must(Client.CreateOAuthApp(oauthApp)).(*model.OAuthApp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Id = oauthApp.Id }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Secret = oauthApp.ClientSecret }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.AuthEndpoint = Client.Url + "/oauth/authorize" }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.TokenEndpoint = Client.Url + "/oauth/access_token" }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.UserApiEndpoint = Client.ApiUrl + "/users/me" }) - - provider := &MattermostTestProvider{} - - authRequest := &model.AuthorizeRequest{ - ResponseType: model.AUTHCODE_RESPONSE_TYPE, - ClientId: oauthApp.Id, - RedirectUri: oauthApp.CallbackUrls[0], - Scope: "all", - State: "123", - } - - redirect, resp := Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - rurl, _ := url.Parse(redirect) - - code := rurl.Query().Get("code") - stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO - delete(stateProps, "team_id") - stateProps["redirect_to"] = *th.App.Config().GitLabSettings.AuthEndpoint - stateProps["hash"] = utils.HashSha256(*th.App.Config().GitLabSettings.Id) - stateProps["redirect_to"] = "/oauth/authorize" - state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) - if r, err := HttpGet(Client.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), Client.HttpClient, "", false); err == nil { - closeBody(r) - } - - einterfaces.RegisterOauthProvider(model.SERVICE_GITLAB, provider) - - redirect, resp = Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - rurl, _ = url.Parse(redirect) - - code = rurl.Query().Get("code") - if r, err := HttpGet(Client.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), Client.HttpClient, "", false); err == nil { - closeBody(r) - } - - if _, err := th.App.Srv.Store.User().UpdateAuthData( - th.BasicUser.Id, model.SERVICE_GITLAB, &th.BasicUser.Email, th.BasicUser.Email, true); err != nil { - t.Fatal(err) - } - - redirect, resp = Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - rurl, _ = url.Parse(redirect) - - code = rurl.Query().Get("code") - stateProps["action"] = model.OAUTH_ACTION_LOGIN - state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) - if r, err := HttpGet(Client.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), Client.HttpClient, "", false); err == nil { - closeBody(r) - } - - redirect, resp = Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - rurl, _ = url.Parse(redirect) - - code = rurl.Query().Get("code") - delete(stateProps, "action") - state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) - if r, err := HttpGet(Client.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), Client.HttpClient, "", false); err == nil { - closeBody(r) - } - - redirect, resp = Client.AuthorizeOAuthApp(authRequest) - CheckNoError(t, resp) - rurl, _ = url.Parse(redirect) - - code = rurl.Query().Get("code") - stateProps["action"] = model.OAUTH_ACTION_SIGNUP - state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) - if r, err := HttpGet(Client.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), Client.HttpClient, "", false); err == nil { - closeBody(r) - } -} - -func TestOAuthComplete_AccessDenied(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() - - c := &Context{ - App: th.App, - Params: &web.Params{ - Service: "TestService", - }, - } - responseWriter := httptest.NewRecorder() - request, _ := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/signup/TestService/complete?error=access_denied", nil) - - completeOAuth(c, responseWriter, request) - - response := responseWriter.Result() - - assert.Equal(t, http.StatusTemporaryRedirect, response.StatusCode) - - location, _ := url.Parse(response.Header.Get("Location")) - assert.Equal(t, "oauth_access_denied", location.Query().Get("type")) - assert.Equal(t, "TestService", location.Query().Get("service")) -} - -func HttpGet(url string, httpClient *http.Client, authToken string, followRedirect bool) (*http.Response, *model.AppError) { - rq, _ := http.NewRequest("GET", url, nil) - rq.Close = true - - if len(authToken) > 0 { - rq.Header.Set(model.HEADER_AUTH, authToken) - } - - if !followRedirect { - httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - } - - if rp, err := httpClient.Do(rq); err != nil { - return nil, model.NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) - } else if rp.StatusCode == 304 { - return rp, nil - } else if rp.StatusCode == 307 { - return rp, nil - } else if rp.StatusCode >= 300 { - defer closeBody(rp) - return rp, model.AppErrorFromJson(rp.Body) - } else { - return rp, nil - } -} - func closeBody(r *http.Response) { if r != nil && r.Body != nil { ioutil.ReadAll(r.Body) r.Body.Close() } } - -type MattermostTestProvider struct { -} - -func (m *MattermostTestProvider) GetUserFromJson(data io.Reader) *model.User { - user := model.UserFromJson(data) - user.AuthData = &user.Email - return user -} diff --git a/web/handlers.go b/web/handlers.go index d9b8edbf3a..a67c585ec1 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -8,6 +8,8 @@ import ( "net/http" "time" + "github.com/NYTimes/gziphandler" + "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" @@ -240,3 +242,73 @@ func (h *Handler) checkCSRFToken(c *Context, r *http.Request, token string, toke return csrfCheckNeeded, csrfCheckPassed } + +// ApiHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be +// granted. +func (w *Web) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + handler := &Handler{ + GetGlobalAppOptions: w.GetGlobalAppOptions, + HandleFunc: h, + RequireSession: false, + TrustRequester: false, + RequireMfa: false, + IsStatic: false, + } + if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler +} + +// ApiHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are +// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the +// websocket. +func (w *Web) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + handler := &Handler{ + GetGlobalAppOptions: w.GetGlobalAppOptions, + HandleFunc: h, + RequireSession: false, + TrustRequester: true, + RequireMfa: false, + IsStatic: false, + } + if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler +} + +// ApiSessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to +// be granted. +func (w *Web) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + handler := &Handler{ + GetGlobalAppOptions: w.GetGlobalAppOptions, + HandleFunc: h, + RequireSession: true, + TrustRequester: false, + RequireMfa: true, + IsStatic: false, + } + if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler +} + +// apiHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are +// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the +// websocket. +func (w *Web) apiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + handler := &Handler{ + GetGlobalAppOptions: w.GetGlobalAppOptions, + HandleFunc: h, + RequireSession: false, + TrustRequester: true, + RequireMfa: false, + IsStatic: false, + } + if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler +} diff --git a/web/oauth.go b/web/oauth.go new file mode 100644 index 0000000000..9bcc61d0ce --- /dev/null +++ b/web/oauth.go @@ -0,0 +1,387 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package web + +import ( + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/mattermost/mattermost-server/utils/fileutils" +) + +func (w *Web) InitOAuth() { + // API version independent OAuth 2.0 as a service provider endpoints + w.MainRouter.Handle("/oauth/authorize", w.ApiHandlerTrustRequester(authorizeOAuthPage)).Methods("GET") + w.MainRouter.Handle("/oauth/authorize", w.ApiSessionRequired(authorizeOAuthApp)).Methods("POST") + w.MainRouter.Handle("/oauth/deauthorize", w.ApiSessionRequired(deauthorizeOAuthApp)).Methods("POST") + w.MainRouter.Handle("/oauth/access_token", w.ApiHandlerTrustRequester(getAccessToken)).Methods("POST") + + // API version independent OAuth as a client endpoints + w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET") + w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/login", w.ApiHandler(loginWithOAuth)).Methods("GET") + w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/mobile_login", w.ApiHandler(mobileLoginWithOAuth)).Methods("GET") + w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/signup", w.ApiHandler(signupWithOAuth)).Methods("GET") + + // Old endpoints for backwards compatibility, needed to not break SSO for any old setups + w.MainRouter.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET") + w.MainRouter.Handle("/signup/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET") + w.MainRouter.Handle("/login/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET") + w.MainRouter.Handle("/api/v4/oauth_test", w.ApiSessionRequired(testHandler)).Methods("GET") +} + +func testHandler(c *Context, w http.ResponseWriter, r *http.Request) { + ReturnStatusOK(w) +} + +func authorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { + authRequest := model.AuthorizeRequestFromJson(r.Body) + if authRequest == nil { + c.SetInvalidParam("authorize_request") + } + + if err := authRequest.IsValid(); err != nil { + c.Err = err + return + } + + if c.App.Session.IsOAuth { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + c.Err.DetailedError += ", attempted access by oauth app" + return + } + + c.LogAudit("attempt") + + redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.App.Session.UserId, authRequest) + + if err != nil { + c.Err = err + return + } + + c.LogAudit("") + + w.Write([]byte(model.MapToJson(map[string]string{"redirect": redirectUrl}))) +} + +func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { + requestData := model.MapFromJson(r.Body) + clientId := requestData["client_id"] + + if len(clientId) != 26 { + c.SetInvalidParam("client_id") + return + } + + err := c.App.DeauthorizeOAuthAppForUser(c.App.Session.UserId, clientId) + if err != nil { + c.Err = err + return + } + + c.LogAudit("success") + ReturnStatusOK(w) +} + +func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { + if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider { + err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) + return + } + + authRequest := &model.AuthorizeRequest{ + ResponseType: r.URL.Query().Get("response_type"), + ClientId: r.URL.Query().Get("client_id"), + RedirectUri: r.URL.Query().Get("redirect_uri"), + Scope: r.URL.Query().Get("scope"), + State: r.URL.Query().Get("state"), + } + + loginHint := r.URL.Query().Get("login_hint") + + if err := authRequest.IsValid(); err != nil { + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) + return + } + + oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId) + if err != nil { + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) + return + } + + // here we should check if the user is logged in + if len(c.App.Session.UserId) == 0 { + if loginHint == model.USER_AUTH_SERVICE_SAML { + http.Redirect(w, r, c.GetSiteURLHeader()+"/login/sso/saml?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound) + } else { + http.Redirect(w, r, c.GetSiteURLHeader()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound) + } + return + } + + if !oauthApp.IsValidRedirectURL(authRequest.RedirectUri) { + err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest) + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) + return + } + + isAuthorized := false + + if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.App.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, authRequest.ClientId); err == nil { + // when we support scopes we should check if the scopes match + isAuthorized = true + } + + // Automatically allow if the app is trusted + if oauthApp.IsTrusted || isAuthorized { + redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.App.Session.UserId, authRequest) + + if err != nil { + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) + return + } + + http.Redirect(w, r, redirectUrl, http.StatusFound) + return + } + + w.Header().Set("X-Frame-Options", "SAMEORIGIN") + w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public") + + staticDir, _ := fileutils.FindDir(model.CLIENT_DIR) + http.ServeFile(w, r, filepath.Join(staticDir, "root.html")) +} + +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") + switch grantType { + case model.ACCESS_TOKEN_GRANT_TYPE: + if len(code) == 0 { + c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "", http.StatusBadRequest) + return + } + case model.REFRESH_TOKEN_GRANT_TYPE: + if len(refreshToken) == 0 { + c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "", http.StatusBadRequest) + return + } + default: + c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "", http.StatusBadRequest) + return + } + + clientId := r.FormValue("client_id") + if len(clientId) != 26 { + c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "", http.StatusBadRequest) + return + } + + secret := r.FormValue("client_secret") + if len(secret) == 0 { + c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_secret.app_error", nil, "", http.StatusBadRequest) + return + } + + redirectUri := r.FormValue("redirect_uri") + + c.LogAudit("attempt") + + accessRsp, err := c.App.GetOAuthAccessTokenForCodeFlow(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.LogAudit("success") + + w.Write([]byte(accessRsp.ToJson())) +} + +func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireService() + if c.Err != nil { + return + } + + service := c.Params.Service + + oauthError := r.URL.Query().Get("error") + if oauthError == "access_denied" { + utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{ + "type": []string{"oauth_access_denied"}, + "service": []string{strings.Title(service)}, + }, c.App.AsymmetricSigningKey()) + return + } + + code := r.URL.Query().Get("code") + if len(code) == 0 { + utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{ + "type": []string{"oauth_missing_code"}, + "service": []string{strings.Title(service)}, + }, c.App.AsymmetricSigningKey()) + return + } + + state := r.URL.Query().Get("state") + + uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete" + + body, teamId, props, err := c.App.AuthorizeOAuthUser(w, r, service, code, state, uri) + + action := "" + if props != nil { + action = props["action"] + } + + if err != nil { + err.Translate(c.App.T) + mlog.Error(err.Error()) + if action == model.OAUTH_ACTION_MOBILE { + w.Write([]byte(err.ToJson())) + } else { + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) + } + return + } + + user, err := c.App.CompleteOAuth(service, body, teamId, props) + if err != nil { + err.Translate(c.App.T) + mlog.Error(err.Error()) + if action == model.OAUTH_ACTION_MOBILE { + w.Write([]byte(err.ToJson())) + } else { + utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey()) + } + return + } + + 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 { + session, err := c.App.DoLogin(w, r, user, "") + if err != nil { + err.Translate(c.App.T) + c.Err = err + if action == model.OAUTH_ACTION_MOBILE { + w.Write([]byte(err.ToJson())) + } + return + } + + c.App.AttachSessionCookies(w, r, session) + + c.App.Session = *session + + if _, ok := props["redirect_to"]; ok { + redirectUrl = props["redirect_to"] + } else { + redirectUrl = c.GetSiteURLHeader() + } + } + + if action == model.OAUTH_ACTION_MOBILE { + ReturnStatusOK(w) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + http.Redirect(w, r, redirectUrl, http.StatusTemporaryRedirect) +} + +func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireService() + if c.Err != nil { + return + } + + loginHint := r.URL.Query().Get("login_hint") + redirectTo := r.URL.Query().Get("redirect_to") + + teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query()) + if err != nil { + c.Err = err + return + } + + authUrl, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAUTH_ACTION_LOGIN, redirectTo, loginHint) + if err != nil { + c.Err = err + return + } + + http.Redirect(w, r, authUrl, http.StatusFound) +} + +func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireService() + if c.Err != nil { + return + } + + teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query()) + if err != nil { + c.Err = err + return + } + + authUrl, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAUTH_ACTION_MOBILE, "", "") + if err != nil { + c.Err = err + return + } + + http.Redirect(w, r, authUrl, http.StatusFound) +} + +func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireService() + if c.Err != nil { + return + } + + if !*c.App.Config().TeamSettings.EnableUserCreation { + utils.RenderWebError(c.App.Config(), w, r, http.StatusBadRequest, url.Values{ + "message": []string{utils.T("api.oauth.singup_with_oauth.disabled.app_error")}, + }, c.App.AsymmetricSigningKey()) + return + } + + teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query()) + if err != nil { + c.Err = err + return + } + + authUrl, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId) + if err != nil { + c.Err = err + return + } + + http.Redirect(w, r, authUrl, http.StatusFound) +} diff --git a/web/oauth_test.go b/web/oauth_test.go new file mode 100644 index 0000000000..9974bdafe3 --- /dev/null +++ b/web/oauth_test.go @@ -0,0 +1,795 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package web + +import ( + "encoding/base64" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/mattermost/mattermost-server/einterfaces" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOAuthComplete_AccessDenied(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + c := &Context{ + App: th.App, + Params: &Params{ + Service: "TestService", + }, + } + responseWriter := httptest.NewRecorder() + request, _ := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/signup/TestService/complete?error=access_denied", nil) + + completeOAuth(c, responseWriter, request) + + response := responseWriter.Result() + + assert.Equal(t, http.StatusTemporaryRedirect, response.StatusCode) + + location, _ := url.Parse(response.Header.Get("Location")) + assert.Equal(t, "oauth_access_denied", location.Query().Get("type")) + assert.Equal(t, "TestService", location.Query().Get("service")) +} + +func TestAuthorizeOAuthApp(t *testing.T) { + th := Setup().InitBasic() + th.Login(ApiClient, th.SystemAdminUser) + defer th.TearDown() + + enableOAuth := *th.App.Config().ServiceSettings.EnableOAuthServiceProvider + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) + }() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) + + oapp := &model.OAuthApp{ + Name: GenerateTestAppName(), + Homepage: "https://nowhere.com", + Description: "test", + CallbackUrls: []string{"https://nowhere.com"}, + CreatorId: th.SystemAdminUser.Id, + } + + rapp, appErr := th.App.CreateOAuthApp(oapp) + CheckNoAppError(t, appErr) + + authRequest := &model.AuthorizeRequest{ + ResponseType: model.AUTHCODE_RESPONSE_TYPE, + ClientId: rapp.Id, + RedirectUri: rapp.CallbackUrls[0], + Scope: "", + State: "123", + } + + // Test auth code flow + ruri, resp := ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + + if len(ruri) == 0 { + t.Fatal("redirect url should be set") + } + + ru, _ := url.Parse(ruri) + 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") != authRequest.State { + t.Fatal("returned state doesn't match") + } + } + + // Test implicit flow + authRequest.ResponseType = model.IMPLICIT_RESPONSE_TYPE + ruri, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + require.False(t, len(ruri) == 0, "redirect url should be set") + + ru, _ = url.Parse(ruri) + require.NotNil(t, ru, "redirect url unparseable") + values, err := url.ParseQuery(ru.Fragment) + require.Nil(t, err) + assert.False(t, len(values.Get("access_token")) == 0, "access_token not returned") + assert.Equal(t, authRequest.State, values.Get("state"), "returned state doesn't match") + + oldToken := ApiClient.AuthToken + ApiClient.AuthToken = values.Get("access_token") + _, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckForbiddenStatus(t, resp) + + ApiClient.AuthToken = oldToken + + authRequest.RedirectUri = "" + _, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckBadRequestStatus(t, resp) + + authRequest.RedirectUri = "http://somewhereelse.com" + _, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckBadRequestStatus(t, resp) + + authRequest.RedirectUri = rapp.CallbackUrls[0] + authRequest.ResponseType = "" + _, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckBadRequestStatus(t, resp) + + authRequest.ResponseType = model.AUTHCODE_RESPONSE_TYPE + authRequest.ClientId = "" + _, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckBadRequestStatus(t, resp) + + authRequest.ClientId = model.NewId() + _, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckNotFoundStatus(t, resp) +} + +func TestDeauthorizeOAuthApp(t *testing.T) { + th := Setup().InitBasic() + th.Login(ApiClient, th.SystemAdminUser) + defer th.TearDown() + + enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) + }() + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) + + oapp := &model.OAuthApp{ + Name: GenerateTestAppName(), + Homepage: "https://nowhere.com", + Description: "test", + CallbackUrls: []string{"https://nowhere.com"}, + CreatorId: th.SystemAdminUser.Id, + } + + rapp, appErr := th.App.CreateOAuthApp(oapp) + CheckNoAppError(t, appErr) + + authRequest := &model.AuthorizeRequest{ + ResponseType: model.AUTHCODE_RESPONSE_TYPE, + ClientId: rapp.Id, + RedirectUri: rapp.CallbackUrls[0], + Scope: "", + State: "123", + } + + _, resp := ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + + pass, resp := ApiClient.DeauthorizeOAuthApp(rapp.Id) + CheckNoError(t, resp) + + if !pass { + t.Fatal("should have passed") + } + + _, resp = ApiClient.DeauthorizeOAuthApp("junk") + CheckBadRequestStatus(t, resp) + + _, resp = ApiClient.DeauthorizeOAuthApp(model.NewId()) + CheckNoError(t, resp) + + th.Logout(ApiClient) + _, resp = ApiClient.DeauthorizeOAuthApp(rapp.Id) + CheckUnauthorizedStatus(t, resp) +} + +func TestOAuthAccessToken(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + + th := Setup().InitBasic() + th.Login(ApiClient, th.SystemAdminUser) + defer th.TearDown() + + enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) + }() + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) + + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer func() { + th.RestoreDefaultRolePermissions(defaultRolePermissions) + }() + th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + + oauthApp := &model.OAuthApp{ + Name: "TestApp5" + model.NewId(), + Homepage: "https://nowhere.com", + Description: "test", + CallbackUrls: []string{"https://nowhere.com"}, + CreatorId: th.SystemAdminUser.Id, + } + oauthApp, appErr := th.App.CreateOAuthApp(oauthApp) + CheckNoAppError(t, appErr) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false }) + data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}} + + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Log(resp.StatusCode) + t.Fatal("should have failed - oauth providing turned off") + } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) + + authRequest := &model.AuthorizeRequest{ + ResponseType: model.AUTHCODE_RESPONSE_TYPE, + ClientId: oauthApp.Id, + RedirectUri: oauthApp.CallbackUrls[0], + Scope: "all", + State: "123", + } + + redirect, resp := ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + rurl, _ := url.Parse(redirect) + + ApiClient.Logout() + + data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{oauthApp.Id}, "client_secret": []string{oauthApp.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}} + + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - bad grant type") + } + + data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + data.Set("client_id", "") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - missing client id") + } + data.Set("client_id", "junk") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - bad client id") + } + + data.Set("client_id", oauthApp.Id) + data.Set("client_secret", "") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - missing client secret") + } + + data.Set("client_secret", "junk") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - bad client secret") + } + + data.Set("client_secret", oauthApp.ClientSecret) + data.Set("code", "") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - missing code") + } + + data.Set("code", "junk") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - bad code") + } + + data.Set("code", rurl.Query().Get("code")) + data.Set("redirect_uri", "junk") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - non-matching redirect uri") + } + + // reset data for successful request + data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + data.Set("client_id", oauthApp.Id) + data.Set("client_secret", oauthApp.ClientSecret) + data.Set("code", rurl.Query().Get("code")) + data.Set("redirect_uri", oauthApp.CallbackUrls[0]) + + token := "" + refreshToken := "" + if rsp, resp := ApiClient.GetOAuthAccessToken(data); resp.Error != nil { + t.Fatal(resp.Error) + } else { + if len(rsp.AccessToken) == 0 { + t.Fatal("access token not returned") + } else if len(rsp.RefreshToken) == 0 { + t.Fatal("refresh token not returned") + } else { + token = rsp.AccessToken + refreshToken = rsp.RefreshToken + } + if rsp.TokenType != model.ACCESS_TOKEN_TYPE { + t.Fatal("access token type incorrect") + } + } + + if _, err := ApiClient.DoApiGet("/oauth_test", ""); err != nil { + t.Fatal(err) + } + + ApiClient.SetOAuthToken("") + if _, err := ApiClient.DoApiGet("/oauth_test", ""); err == nil { + t.Fatal("should have failed - no access token provided") + } + + ApiClient.SetOAuthToken("badtoken") + if _, err := ApiClient.DoApiGet("/oauth_test", ""); err == nil { + t.Fatal("should have failed - bad token provided") + } + + ApiClient.SetOAuthToken(token) + if _, err := ApiClient.DoApiGet("/oauth_test", ""); err != nil { + t.Fatal(err) + } + + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("should have failed - tried to reuse auth code") + } + + data.Set("grant_type", model.REFRESH_TOKEN_GRANT_TYPE) + data.Set("client_id", oauthApp.Id) + data.Set("client_secret", oauthApp.ClientSecret) + data.Set("refresh_token", "") + data.Set("redirect_uri", oauthApp.CallbackUrls[0]) + data.Del("code") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("Should have failed - refresh token empty") + } + + data.Set("refresh_token", refreshToken) + if rsp, resp := ApiClient.GetOAuthAccessToken(data); resp.Error != nil { + t.Fatal(resp.Error) + } else { + if len(rsp.AccessToken) == 0 { + t.Fatal("access token not returned") + } else if len(rsp.RefreshToken) == 0 { + t.Fatal("refresh token not returned") + } else if rsp.RefreshToken == refreshToken { + t.Fatal("refresh token did not update") + } + + if rsp.TokenType != model.ACCESS_TOKEN_TYPE { + t.Fatal("access token type incorrect") + } + ApiClient.SetOAuthToken(rsp.AccessToken) + if _, err := ApiClient.DoApiGet("/oauth_test", ""); err != nil { + t.Fatal(err) + } + + data.Set("refresh_token", rsp.RefreshToken) + } + + if rsp, resp := ApiClient.GetOAuthAccessToken(data); resp.Error != nil { + t.Fatal(resp.Error) + } else { + if len(rsp.AccessToken) == 0 { + t.Fatal("access token not returned") + } else if len(rsp.RefreshToken) == 0 { + t.Fatal("refresh token not returned") + } else if rsp.RefreshToken == refreshToken { + t.Fatal("refresh token did not update") + } + + if rsp.TokenType != model.ACCESS_TOKEN_TYPE { + t.Fatal("access token type incorrect") + } + ApiClient.SetOAuthToken(rsp.AccessToken) + if _, err := ApiClient.DoApiGet("/oauth_test", ""); err != nil { + t.Fatal(err) + } + + } + + authData := &model.AuthData{ClientId: oauthApp.Id, RedirectUri: oauthApp.CallbackUrls[0], UserId: th.BasicUser.Id, Code: model.NewId(), ExpiresIn: -1} + _, err := th.App.Srv.Store.OAuth().SaveAuthData(authData) + require.Nil(t, err) + + data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + data.Set("client_id", oauthApp.Id) + data.Set("client_secret", oauthApp.ClientSecret) + data.Set("redirect_uri", oauthApp.CallbackUrls[0]) + data.Set("code", authData.Code) + data.Del("refresh_token") + if _, resp := ApiClient.GetOAuthAccessToken(data); resp.Error == nil { + t.Fatal("Should have failed - code is expired") + } + + ApiClient.ClearOAuthToken() +} + +func TestOAuthComplete(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + + th := Setup().InitBasic() + th.Login(ApiClient, th.SystemAdminUser) + defer th.TearDown() + + gitLabSettingsEnable := th.App.Config().GitLabSettings.Enable + gitLabSettingsAuthEndpoint := th.App.Config().GitLabSettings.AuthEndpoint + gitLabSettingsId := th.App.Config().GitLabSettings.Id + gitLabSettingsSecret := th.App.Config().GitLabSettings.Secret + gitLabSettingsTokenEndpoint := th.App.Config().GitLabSettings.TokenEndpoint + gitLabSettingsUserApiEndpoint := th.App.Config().GitLabSettings.UserApiEndpoint + enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Enable = gitLabSettingsEnable }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.AuthEndpoint = gitLabSettingsAuthEndpoint }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Id = gitLabSettingsId }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Secret = gitLabSettingsSecret }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.TokenEndpoint = gitLabSettingsTokenEndpoint }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.UserApiEndpoint = gitLabSettingsUserApiEndpoint }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider }) + }() + + r, err := HttpGet(ApiClient.Url+"/login/gitlab/complete?code=123", ApiClient.HttpClient, "", true) + assert.NotNil(t, err) + closeBody(r) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = true }) + r, err = HttpGet(ApiClient.Url+"/login/gitlab/complete?code=123&state=!#$#F@#Yˆ&~ñ", ApiClient.HttpClient, "", true) + assert.NotNil(t, err) + closeBody(r) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.AuthEndpoint = ApiClient.Url + "/oauth/authorize" }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Id = model.NewId() }) + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN + stateProps["team_id"] = th.BasicTeam.Id + stateProps["redirect_to"] = *th.App.Config().GitLabSettings.AuthEndpoint + + state := base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + r, err = HttpGet(ApiClient.Url+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), ApiClient.HttpClient, "", true) + assert.NotNil(t, err) + closeBody(r) + + stateProps["hash"] = utils.HashSha256(*th.App.Config().GitLabSettings.Id) + state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + r, err = HttpGet(ApiClient.Url+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), ApiClient.HttpClient, "", true) + assert.NotNil(t, err) + closeBody(r) + + // We are going to use mattermost as the provider emulating gitlab + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) + + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer func() { + th.RestoreDefaultRolePermissions(defaultRolePermissions) + }() + th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + + oauthApp := &model.OAuthApp{ + Name: "TestApp5" + model.NewId(), + Homepage: "https://nowhere.com", + Description: "test", + CallbackUrls: []string{ + ApiClient.Url + "/signup/" + model.SERVICE_GITLAB + "/complete", + ApiClient.Url + "/login/" + model.SERVICE_GITLAB + "/complete", + }, + CreatorId: th.SystemAdminUser.Id, + IsTrusted: true, + } + oauthApp, appErr := th.App.CreateOAuthApp(oauthApp) + CheckNoAppError(t, appErr) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Id = oauthApp.Id }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Secret = oauthApp.ClientSecret }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.AuthEndpoint = ApiClient.Url + "/oauth/authorize" }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.TokenEndpoint = ApiClient.Url + "/oauth/access_token" }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.UserApiEndpoint = ApiClient.ApiUrl + "/users/me" }) + + provider := &MattermostTestProvider{} + + authRequest := &model.AuthorizeRequest{ + ResponseType: model.AUTHCODE_RESPONSE_TYPE, + ClientId: oauthApp.Id, + RedirectUri: oauthApp.CallbackUrls[0], + Scope: "all", + State: "123", + } + + redirect, resp := ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + rurl, _ := url.Parse(redirect) + + code := rurl.Query().Get("code") + stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO + delete(stateProps, "team_id") + stateProps["redirect_to"] = *th.App.Config().GitLabSettings.AuthEndpoint + stateProps["hash"] = utils.HashSha256(*th.App.Config().GitLabSettings.Id) + stateProps["redirect_to"] = "/oauth/authorize" + state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + if r, err := HttpGet(ApiClient.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), ApiClient.HttpClient, "", false); err == nil { + closeBody(r) + } + + einterfaces.RegisterOauthProvider(model.SERVICE_GITLAB, provider) + + redirect, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + rurl, _ = url.Parse(redirect) + + code = rurl.Query().Get("code") + if r, err := HttpGet(ApiClient.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), ApiClient.HttpClient, "", false); err == nil { + closeBody(r) + } + + if _, err := th.App.Srv.Store.User().UpdateAuthData( + th.BasicUser.Id, model.SERVICE_GITLAB, &th.BasicUser.Email, th.BasicUser.Email, true); err != nil { + t.Fatal(err) + } + + redirect, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + rurl, _ = url.Parse(redirect) + + code = rurl.Query().Get("code") + stateProps["action"] = model.OAUTH_ACTION_LOGIN + state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + if r, err := HttpGet(ApiClient.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), ApiClient.HttpClient, "", false); err == nil { + closeBody(r) + } + + redirect, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + rurl, _ = url.Parse(redirect) + + code = rurl.Query().Get("code") + delete(stateProps, "action") + state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + if r, err := HttpGet(ApiClient.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), ApiClient.HttpClient, "", false); err == nil { + closeBody(r) + } + + redirect, resp = ApiClient.AuthorizeOAuthApp(authRequest) + CheckNoError(t, resp) + rurl, _ = url.Parse(redirect) + + code = rurl.Query().Get("code") + stateProps["action"] = model.OAUTH_ACTION_SIGNUP + state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + if r, err := HttpGet(ApiClient.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), ApiClient.HttpClient, "", false); err == nil { + closeBody(r) + } +} + +func HttpGet(url string, httpClient *http.Client, authToken string, followRedirect bool) (*http.Response, *model.AppError) { + rq, _ := http.NewRequest("GET", url, nil) + rq.Close = true + + if len(authToken) > 0 { + rq.Header.Set(model.HEADER_AUTH, authToken) + } + + if !followRedirect { + httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + } + + if rp, err := httpClient.Do(rq); err != nil { + return nil, model.NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) + } else if rp.StatusCode == 304 { + return rp, nil + } else if rp.StatusCode == 307 { + return rp, nil + } else if rp.StatusCode >= 300 { + defer closeBody(rp) + return rp, model.AppErrorFromJson(rp.Body) + } else { + return rp, nil + } +} + +func closeBody(r *http.Response) { + if r != nil && r.Body != nil { + ioutil.ReadAll(r.Body) + r.Body.Close() + } +} + +type MattermostTestProvider struct { +} + +func (m *MattermostTestProvider) GetUserFromJson(data io.Reader) *model.User { + user := model.UserFromJson(data) + user.AuthData = &user.Email + return user +} + +func GenerateTestAppName() string { + return "fakeoauthapp" + model.NewRandomString(10) +} + +func CheckNoAppError(t *testing.T, err *model.AppError) { + t.Helper() + + if err != nil { + t.Fatalf("Expected no error, got %q", err.Error()) + } +} + +func CheckNoError(t *testing.T, resp *model.Response) { + t.Helper() + + if resp.Error != nil { + t.Fatalf("Expected no error, got %q", resp.Error.Error()) + } +} + +func checkHTTPStatus(t *testing.T, resp *model.Response, expectedStatus int, expectError bool) { + t.Helper() + + switch { + case resp == nil: + t.Fatalf("Unexpected nil response, expected http:%v, expectError:%v)", expectedStatus, expectError) + + case expectError && resp.Error == nil: + t.Fatalf("Expected a non-nil error and http status:%v, got nil, %v", expectedStatus, resp.StatusCode) + + case !expectError && resp.Error != nil: + t.Fatalf("Expected no error and http status:%v, got %q, http:%v", expectedStatus, resp.Error, resp.StatusCode) + + case resp.StatusCode != expectedStatus: + t.Fatalf("Expected http status:%v, got %v (err: %q)", expectedStatus, resp.StatusCode, resp.Error) + } +} + +func CheckForbiddenStatus(t *testing.T, resp *model.Response) { + t.Helper() + checkHTTPStatus(t, resp, http.StatusForbidden, true) +} + +func CheckUnauthorizedStatus(t *testing.T, resp *model.Response) { + t.Helper() + checkHTTPStatus(t, resp, http.StatusUnauthorized, true) +} + +func CheckNotFoundStatus(t *testing.T, resp *model.Response) { + t.Helper() + checkHTTPStatus(t, resp, http.StatusNotFound, true) +} + +func CheckBadRequestStatus(t *testing.T, resp *model.Response) { + t.Helper() + checkHTTPStatus(t, resp, http.StatusBadRequest, true) +} + +func (th *TestHelper) Login(client *model.Client4, user *model.User) { + session := &model.Session{ + UserId: user.Id, + Roles: user.GetRawRoles(), + IsOAuth: false, + } + session, _ = th.App.CreateSession(session) + client.AuthToken = session.Token + client.AuthType = model.HEADER_BEARER +} + +func (th *TestHelper) Logout(client *model.Client4) { + client.AuthToken = "" +} + +func (th *TestHelper) SaveDefaultRolePermissions() map[string][]string { + utils.DisableDebugLogForTest() + + results := make(map[string][]string) + + for _, roleName := range []string{ + "system_user", + "system_admin", + "team_user", + "team_admin", + "channel_user", + "channel_admin", + } { + role, err1 := th.App.GetRoleByName(roleName) + if err1 != nil { + utils.EnableDebugLogForTest() + panic(err1) + } + + results[roleName] = role.Permissions + } + + utils.EnableDebugLogForTest() + return results +} + +func (th *TestHelper) RestoreDefaultRolePermissions(data map[string][]string) { + utils.DisableDebugLogForTest() + + for roleName, permissions := range data { + role, err1 := th.App.GetRoleByName(roleName) + if err1 != nil { + utils.EnableDebugLogForTest() + panic(err1) + } + + if strings.Join(role.Permissions, " ") == strings.Join(permissions, " ") { + continue + } + + role.Permissions = permissions + + _, err2 := th.App.UpdateRole(role) + if err2 != nil { + utils.EnableDebugLogForTest() + panic(err2) + } + } + + utils.EnableDebugLogForTest() +} + +// func (th *TestHelper) RemovePermissionFromRole(permission string, roleName string) { +// utils.DisableDebugLogForTest() + +// role, err1 := th.App.GetRoleByName(roleName) +// if err1 != nil { +// utils.EnableDebugLogForTest() +// panic(err1) +// } + +// var newPermissions []string +// for _, p := range role.Permissions { +// if p != permission { +// newPermissions = append(newPermissions, p) +// } +// } + +// if strings.Join(role.Permissions, " ") == strings.Join(newPermissions, " ") { +// utils.EnableDebugLogForTest() +// return +// } + +// role.Permissions = newPermissions + +// _, err2 := th.App.UpdateRole(role) +// if err2 != nil { +// utils.EnableDebugLogForTest() +// panic(err2) +// } + +// utils.EnableDebugLogForTest() +// } + +func (th *TestHelper) AddPermissionToRole(permission string, roleName string) { + utils.DisableDebugLogForTest() + + role, err1 := th.App.GetRoleByName(roleName) + if err1 != nil { + utils.EnableDebugLogForTest() + panic(err1) + } + + for _, existingPermission := range role.Permissions { + if existingPermission == permission { + utils.EnableDebugLogForTest() + return + } + } + + role.Permissions = append(role.Permissions, permission) + + _, err2 := th.App.UpdateRole(role) + if err2 != nil { + utils.EnableDebugLogForTest() + panic(err2) + } + + utils.EnableDebugLogForTest() +} diff --git a/web/web.go b/web/web.go index 1e1499c0d6..fdbc74db0e 100644 --- a/web/web.go +++ b/web/web.go @@ -33,6 +33,7 @@ func New(config configservice.ConfigService, globalOptions app.AppOptionCreator, MainRouter: root, } + web.InitOAuth() web.InitWebhooks() web.InitSaml() web.InitStatic()