// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/einterfaces" "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" ) func TestGetOAuthAccessTokenForImplicitFlow(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) t.Run("BasicFlow_Success", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "fakeoauthapp" + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}, } oapp, err := th.App.CreateOAuthApp(oapp) require.Nil(t, err) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: oapp.Id, RedirectURI: oapp.CallbackUrls[0], Scope: "", State: "123", } session, err := th.App.GetOAuthAccessTokenForImplicitFlow(th.Context, th.BasicUser.Id, authRequest) assert.Nil(t, err) assert.NotNil(t, session) }) t.Run("OAuthDisabled_ShouldFail", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "fakeoauthapp" + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}, } oapp, err := th.App.CreateOAuthApp(oapp) require.Nil(t, err) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false }) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: oapp.Id, RedirectURI: oapp.CallbackUrls[0], Scope: "", State: "123", } session, err := th.App.GetOAuthAccessTokenForImplicitFlow(th.Context, th.BasicUser.Id, authRequest) assert.NotNil(t, err) assert.Nil(t, session) }) t.Run("BadClientId_ShouldFail", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: "invalid_client_id", RedirectURI: "https://nowhere.com", Scope: "", State: "123", } session, err := th.App.GetOAuthAccessTokenForImplicitFlow(th.Context, th.BasicUser.Id, authRequest) assert.NotNil(t, err) assert.Nil(t, session) }) t.Run("BadUserId_ShouldFail", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "fakeoauthapp" + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}, } oapp, err := th.App.CreateOAuthApp(oapp) require.Nil(t, err) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: oapp.Id, RedirectURI: oapp.CallbackUrls[0], Scope: "", State: "123", } session, err := th.App.GetOAuthAccessTokenForImplicitFlow(th.Context, "invalid_user_id", authRequest) assert.NotNil(t, err) assert.Nil(t, session) }) t.Run("PublicClient_Success", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) dcrRequest := &model.ClientRegistrationRequest{ ClientName: new("Public Client Test"), RedirectURIs: []string{"https://example.com/callback"}, TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone), ClientURI: new("https://example.com"), } publicApp, appErr := th.App.RegisterOAuthClient(th.Context, dcrRequest, th.BasicUser2.Id) require.Nil(t, appErr) require.Empty(t, publicApp.ClientSecret) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: publicApp.Id, RedirectURI: publicApp.CallbackUrls[0], Scope: "user", State: "test_state", } redirectURL, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.Nil(t, appErr) require.Contains(t, redirectURL, "#access_token=") require.Contains(t, redirectURL, "token_type=bearer") require.Contains(t, redirectURL, "state=test_state") // Parse the access token from the fragment uri, err := url.Parse(redirectURL) require.NoError(t, err) fragment := uri.Fragment fragmentValues, err := url.ParseQuery(fragment) require.NoError(t, err) accessToken := fragmentValues.Get("access_token") require.NotEmpty(t, accessToken) // Verify session exists session, appErr := th.App.GetSession(accessToken) require.Nil(t, appErr) require.NotNil(t, session) require.Equal(t, th.BasicUser.Id, session.UserId) require.True(t, session.IsOAuth) // Verify access data exists for public client accessData, err := th.App.Srv().Store().OAuth().GetAccessData(accessToken) require.NoError(t, err) require.NotNil(t, accessData) require.Equal(t, publicApp.Id, accessData.ClientId) require.Equal(t, th.BasicUser.Id, accessData.UserId) require.Empty(t, accessData.RefreshToken) }) t.Run("ConfidentialClient_Success", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) confidentialApp := &model.OAuthApp{ Name: "Confidential Client Test", CreatorId: th.BasicUser2.Id, Homepage: "https://example.com", Description: "test confidential client", CallbackUrls: []string{"https://example.com/callback"}, ClientSecret: model.NewId(), } confidentialApp, appErr := th.App.CreateOAuthApp(confidentialApp) require.Nil(t, appErr) require.NotEmpty(t, confidentialApp.ClientSecret) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: confidentialApp.Id, RedirectURI: confidentialApp.CallbackUrls[0], Scope: "user", State: "test_state", } redirectURL, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.Nil(t, appErr) require.Contains(t, redirectURL, "#access_token=") require.Contains(t, redirectURL, "token_type=bearer") require.Contains(t, redirectURL, "state=test_state") // Parse the access token from the fragment uri, err := url.Parse(redirectURL) require.NoError(t, err) fragment := uri.Fragment fragmentValues, err := url.ParseQuery(fragment) require.NoError(t, err) accessToken := fragmentValues.Get("access_token") require.NotEmpty(t, accessToken) // Verify session exists session, appErr := th.App.GetSession(accessToken) require.Nil(t, appErr) require.NotNil(t, session) require.Equal(t, th.BasicUser.Id, session.UserId) require.True(t, session.IsOAuth) // Verify access data exists for confidential client accessData, err := th.App.Srv().Store().OAuth().GetAccessData(accessToken) require.NoError(t, err) require.NotNil(t, accessData) require.Equal(t, confidentialApp.Id, accessData.ClientId) require.Equal(t, th.BasicUser.Id, accessData.UserId) require.Empty(t, accessData.RefreshToken) }) } func TestOAuthRevokeAccessToken(t *testing.T) { mainHelper.Parallel(t) th := Setup(t) session := &model.Session{} session.CreateAt = model.GetMillis() session.UserId = model.NewId() session.Token = model.NewId() session.Roles = model.SystemUserRoleId th.App.SetSessionExpireInHours(session, 24) session, err := th.App.CreateSession(th.Context, session) require.Nil(t, err) err = th.App.RevokeAccessToken(th.Context, session.Token) require.NotNil(t, err, "Should have failed does not have an access token") require.Equal(t, http.StatusBadRequest, err.StatusCode) } func TestOAuthDeleteApp(t *testing.T) { mainHelper.Parallel(t) th := Setup(t) *th.App.Config().ServiceSettings.EnableOAuthServiceProvider = true a1 := &model.OAuthApp{} a1.CreatorId = model.NewId() a1.Name = "TestApp" + model.NewId() a1.CallbackUrls = []string{"https://nowhere.com"} a1.Homepage = "https://nowhere.com" a1, appErr := th.App.CreateOAuthApp(a1) require.Nil(t, appErr) session := &model.Session{} session.CreateAt = model.GetMillis() session.UserId = model.NewId() session.Token = model.NewId() session.Roles = model.SystemUserRoleId session.IsOAuth = true th.App.ch.srv.platform.SetSessionExpireInHours(session, 24) session, appErr = th.App.CreateSession(th.Context, session) require.Nil(t, appErr) accessData := &model.AccessData{} accessData.Token = session.Token accessData.UserId = session.UserId accessData.RedirectUri = "http://example.com" accessData.ClientId = a1.Id accessData.ExpiresAt = session.ExpiresAt _, err := th.App.Srv().Store().OAuth().SaveAccessData(accessData) require.NoError(t, err) appErr = th.App.DeleteOAuthApp(th.Context, a1.Id) require.Nil(t, appErr) _, appErr = th.App.GetSession(session.Token) require.NotNil(t, appErr, "should not get session from cache or db") } func TestAuthorizeOAuthUser(t *testing.T) { mainHelper.Parallel(t) setup := func(t *testing.T, enable, tokenEndpoint, userEndpoint bool, serverURL string) *TestHelper { mainHelper.Parallel(t) th := Setup(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = enable if tokenEndpoint { *cfg.GitLabSettings.TokenEndpoint = serverURL + "/token" } else { *cfg.GitLabSettings.TokenEndpoint = "" } if userEndpoint { *cfg.GitLabSettings.UserAPIEndpoint = serverURL + "/user" } else { *cfg.GitLabSettings.UserAPIEndpoint = "" } // The test server binds to a loopback (reserved) address, which the // filtered OAuth client refuses unless it is explicitly allowlisted. if serverURL != "" { if u, parseErr := url.Parse(serverURL); parseErr == nil { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = u.Hostname() } } }) return th } makeState := func(token *model.Token) string { return base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(map[string]string{ "token": token.Token, }))) } makeToken := func(th *TestHelper, cookie string) *model.Token { token, appErr := th.App.CreateOAuthStateToken(generateOAuthStateTokenExtra("", "", cookie)) require.Nil(t, appErr) return token } makeRequest := func(cookie string) *http.Request { request, err := http.NewRequest(http.MethodGet, "https://mattermost.example.com", nil) require.NoError(t, err) if cookie != "" { request.AddCookie(&http.Cookie{ Name: CookieOAuth, Value: cookie, }) } return request } t.Run("not enabled", func(t *testing.T) { th := setup(t, false, true, true, "") _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, nil, nil, model.ServiceGitlab, "", "", "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.unsupported.app_error", err.Id) }) t.Run("with an improperly encoded state", func(t *testing.T) { th := setup(t, true, true, true, "") state := "!" _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, nil, nil, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) t.Run("without a stored token", func(t *testing.T) { th := setup(t, true, true, true, "") state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(map[string]string{ "token": model.NewId(), }))) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, nil, nil, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.oauth.invalid_state_token.app_error", err.Id) assert.Error(t, err.Unwrap()) }) t.Run("with a stored token of the wrong type", func(t *testing.T) { th := setup(t, true, true, true, "") token := model.NewToken("invalid", "") require.NoError(t, th.App.Srv().Store().Token().Save(token)) state := makeState(token) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, nil, nil, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.oauth.invalid_state_token.app_error", err.Id) assert.Equal(t, "", err.DetailedError) }) t.Run("with email missing when changing login types", func(t *testing.T) { th := setup(t, true, true, true, "") email := "" action := model.OAuthActionEmailToSSO cookie := model.NewId() token, err := th.App.CreateOAuthStateToken(generateOAuthStateTokenExtra(email, action, cookie)) require.Nil(t, err) state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(map[string]string{ "action": action, "email": email, "token": token.Token, }))) _, _, _, err = th.App.AuthorizeOAuthUser(th.Context, nil, nil, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) t.Run("without an OAuth cookie", func(t *testing.T) { th := setup(t, true, true, true, "") cookie := model.NewId() request := makeRequest("") state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, nil, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) t.Run("with an invalid token", func(t *testing.T) { th := setup(t, true, true, true, "") cookie := model.NewId() token, err := th.App.CreateOAuthStateToken(model.NewId()) require.Nil(t, err) request := makeRequest(cookie) state := makeState(token) _, _, _, err = th.App.AuthorizeOAuthUser(th.Context, nil, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) t.Run("with an incorrect token endpoint", func(t *testing.T) { th := setup(t, true, false, true, "") cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.token_failed.app_error", err.Id) }) t.Run("refuses a reserved-IP token endpoint that is not allowlisted", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := json.NewEncoder(w).Encode(&model.AccessResponse{ AccessToken: model.NewId(), TokenType: model.AccessTokenType, }) require.NoError(t, err) })) defer server.Close() th := setup(t, true, true, true, server.URL) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "" }) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.token_failed.app_error", err.Id) assert.ErrorContains(t, err.Unwrap(), "reserved range") }) t.Run("with an error token response", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_response.app_error", err.Id) assert.Contains(t, err.DetailedError, "status_code=418") }) t.Run("with an invalid token response", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("invalid")) require.NoError(t, err) })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_response.app_error", err.Id) assert.Contains(t, err.DetailedError, "response_body=invalid") }) t.Run("with an invalid token type", func(t *testing.T) { accessToken := model.NewId() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := json.NewEncoder(w).Encode(&model.AccessResponse{ AccessToken: accessToken, TokenType: "", }) require.NoError(t, err) })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_token.app_error", err.Id) assert.NotContains(t, err.DetailedError, accessToken) assert.NotContains(t, err.Error(), accessToken) assert.Contains(t, err.DetailedError, `"access_token":"[REDACTED]"`) }) t.Run("with a JSON error token response containing a token", func(t *testing.T) { accessToken := model.NewId() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) _, err := w.Write([]byte(`{"access_token": "` + accessToken + `", "error": "teapot"}`)) require.NoError(t, err) })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_response.app_error", err.Id) assert.NotContains(t, err.DetailedError, accessToken) assert.NotContains(t, err.Error(), accessToken) assert.Contains(t, err.DetailedError, "status_code=418") assert.Contains(t, err.DetailedError, "teapot") }) t.Run("with a form encoded token response", func(t *testing.T) { accessToken := model.NewId() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-www-form-urlencoded") _, err := w.Write([]byte("access_token=" + accessToken + "&token_type=bearer&scope=read")) require.NoError(t, err) })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_response.app_error", err.Id) assert.NotContains(t, err.DetailedError, accessToken) assert.NotContains(t, err.Error(), accessToken) assert.Contains(t, err.DetailedError, "access_token=[REDACTED]") assert.Contains(t, err.DetailedError, "scope=read") }) t.Run("with an empty token response", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := json.NewEncoder(w).Encode(&model.AccessResponse{ AccessToken: "", TokenType: model.AccessTokenType, }) require.NoError(t, err) })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.missing.app_error", err.Id) }) t.Run("with an incorrect user endpoint", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := json.NewEncoder(w).Encode(&model.AccessResponse{ AccessToken: model.NewId(), TokenType: model.AccessTokenType, }) require.NoError(t, err) })) defer server.Close() th := setup(t, true, true, false, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.service.app_error", err.Id) }) t.Run("with an error user response", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/token": t.Log("hit token") err := json.NewEncoder(w).Encode(&model.AccessResponse{ AccessToken: model.NewId(), TokenType: model.AccessTokenType, }) require.NoError(t, err) case "/user": t.Log("hit user") w.WriteHeader(http.StatusTeapot) } })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.response.app_error", err.Id) }) t.Run("with an error user response due to GitLab TOS", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/token": t.Log("hit token") err := json.NewEncoder(w).Encode(&model.AccessResponse{ AccessToken: model.NewId(), TokenType: model.AccessTokenType, }) require.NoError(t, err) case "/user": t.Log("hit user") w.WriteHeader(http.StatusForbidden) _, err := w.Write([]byte("Terms of Service")) require.NoError(t, err) } })) defer server.Close() th := setup(t, true, true, true, server.URL) cookie := model.NewId() request := makeRequest(cookie) state := makeState(makeToken(th, cookie)) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, &httptest.ResponseRecorder{}, request, model.ServiceGitlab, "", state, "") require.NotNil(t, err) assert.Equal(t, "oauth.gitlab.tos.error", err.Id) }) t.Run("with error in GetSSOSettings", func(t *testing.T) { th := setup(t, true, true, true, "") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.OpenIdSettings.Enable = true }) providerMock := &mocks.OAuthProvider{} providerMock.On("GetSSOSettings", mock.AnythingOfType("*request.Context"), mock.Anything, model.ServiceOpenid).Return(nil, errors.New("error")) einterfaces.RegisterOAuthProvider(model.ServiceOpenid, providerMock) _, _, _, err := th.App.AuthorizeOAuthUser(th.Context, nil, nil, model.ServiceOpenid, "", "", "") require.NotNil(t, err) assert.Equal(t, "api.user.get_authorization_code.endpoint.app_error", err.Id) }) t.Run("enabled and properly configured", func(t *testing.T) { testCases := []struct { Description string SiteURL string ExpectedSetCookieHeaderRegexp string }{ {"no subpath", "http://localhost:8065", "^MMOAUTH=; Path=/"}, {"subpath", "http://localhost:8065/subpath", "^MMOAUTH=; Path=/subpath"}, } for _, tc := range testCases { t.Run(tc.Description, func(t *testing.T) { userData := "Hello, World!" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/token": err := json.NewEncoder(w).Encode(&model.AccessResponse{ AccessToken: model.NewId(), TokenType: model.AccessTokenType, }) require.NoError(t, err) case "/user": w.WriteHeader(http.StatusOK) _, err := w.Write([]byte(userData)) require.NoError(t, err) } })) defer server.Close() th := setup(t, true, true, true, server.URL) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = tc.SiteURL }) cookie := model.NewId() request := makeRequest(cookie) stateProps := map[string]string{ "team_id": model.NewId(), "token": makeToken(th, cookie).Token, } state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps))) recorder := httptest.ResponseRecorder{} body, receivedStateProps, _, err := th.App.AuthorizeOAuthUser(th.Context, &recorder, request, model.ServiceGitlab, "", state, "") require.Nil(t, err) require.NotNil(t, body) bodyBytes, bodyErr := io.ReadAll(body) require.NoError(t, bodyErr) assert.Equal(t, userData, string(bodyBytes)) // team_id is no longer returned as it was removed for security reasons assert.Equal(t, stateProps, receivedStateProps) assert.Nil(t, err) cookies := recorder.Header().Get("Set-Cookie") assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies) }) } }) } func TestGetAuthorizationCode(t *testing.T) { mainHelper.Parallel(t) t.Run("not enabled", func(t *testing.T) { th := Setup(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = false }) _, err := th.App.GetAuthorizationCode(th.Context, nil, nil, model.ServiceGitlab, map[string]string{}, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.unsupported.app_error", err.Id) }) t.Run("enabled and properly configured", func(t *testing.T) { th := Setup(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = true }) testCases := []struct { Description string SiteURL string ExpectedSetCookieHeaderRegexp string }{ {"no subpath", "http://localhost:8065", "^MMOAUTH=[a-z0-9]+; Path=/"}, {"subpath", "http://localhost:8065/subpath", "^MMOAUTH=[a-z0-9]+; Path=/subpath"}, } for _, tc := range testCases { t.Run(tc.Description, func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = tc.SiteURL }) request, _ := http.NewRequest(http.MethodGet, "https://mattermost.example.com", nil) stateProps := map[string]string{ "email": "email@example.com", "action": "action", } recorder := httptest.ResponseRecorder{} url, err := th.App.GetAuthorizationCode(th.Context, &recorder, request, model.ServiceGitlab, stateProps, "") require.Nil(t, err) assert.NotEmpty(t, url) cookies := recorder.Header().Get("Set-Cookie") assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies) }) } }) } func TestDeauthorizeOAuthApp(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "fakeoauthapp" + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}, } oapp, appErr := th.App.CreateOAuthApp(oapp) require.Nil(t, appErr) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: oapp.Id, RedirectURI: oapp.CallbackUrls[0], Scope: "", State: "123", } redirectUrl, appErr := th.App.GetOAuthCodeRedirect(th.BasicUser.Id, authRequest) assert.Nil(t, appErr) dErr := th.App.DeauthorizeOAuthAppForUser(th.Context, th.BasicUser.Id, oapp.Id) assert.Nil(t, dErr) uri, uErr := url.Parse(redirectUrl) require.NoError(t, uErr) queryParams := uri.Query() code := queryParams.Get("code") data, err := th.App.Srv().Store().OAuth().GetAuthData(code) require.Equal(t, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code)), err) assert.Nil(t, data) } func TestDeactivatedUserOAuthApp(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "fakeoauthapp" + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}, } oapp, appErr := th.App.CreateOAuthApp(oapp) require.Nil(t, appErr) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: oapp.Id, RedirectURI: oapp.CallbackUrls[0], Scope: "", State: "123", } redirectUrl, appErr := th.App.GetOAuthCodeRedirect(th.BasicUser.Id, authRequest) assert.Nil(t, appErr) uri, err := url.Parse(redirectUrl) require.NoError(t, err) queryParams := uri.Query() code := queryParams.Get("code") _, appErr = th.App.UpdateActive(th.Context, th.BasicUser, false) require.Nil(t, appErr) resp, appErr := th.App.GetOAuthAccessTokenForCodeFlow(th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", "") assert.Nil(t, resp) require.NotNil(t, appErr, "Should not get access token") require.Equal(t, http.StatusBadRequest, appErr.StatusCode) assert.Equal(t, "api.oauth.get_access_token.expired_code.app_error", appErr.Id) } func TestRegisterOAuthClient(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) t.Run("Valid DCR request with client_uri", func(t *testing.T) { request := &model.ClientRegistrationRequest{ RedirectURIs: []string{"https://example.com/callback/" + model.NewId()}, ClientName: new("Test Client"), ClientURI: new("https://example.com"), } app, appErr := th.App.RegisterOAuthClient(th.Context, request, th.BasicUser.Id) require.Nil(t, appErr) require.NotNil(t, app) assert.Equal(t, request.RedirectURIs, []string(app.CallbackUrls)) assert.True(t, app.IsDynamicallyRegistered) assert.Equal(t, th.BasicUser.Id, app.CreatorId) assert.NotEmpty(t, app.Id) assert.NotEmpty(t, app.ClientSecret) assert.Equal(t, "https://example.com", app.Homepage) // client_uri is mapped to homepage }) t.Run("Valid DCR request without client_uri", func(t *testing.T) { request := &model.ClientRegistrationRequest{ RedirectURIs: []string{"https://example.com/callback/" + model.NewId()}, ClientName: new("Test Client"), } app, appErr := th.App.RegisterOAuthClient(th.Context, request, th.BasicUser.Id) require.Nil(t, appErr) require.NotNil(t, app) assert.Equal(t, request.RedirectURIs, []string(app.CallbackUrls)) assert.True(t, app.IsDynamicallyRegistered) assert.Equal(t, th.BasicUser.Id, app.CreatorId) assert.NotEmpty(t, app.Id) assert.NotEmpty(t, app.ClientSecret) assert.Equal(t, "", app.Homepage) // Homepage is empty when client_uri is not provided }) t.Run("Invalid client_uri", func(t *testing.T) { request := &model.ClientRegistrationRequest{ RedirectURIs: []string{"https://example.com/callback/" + model.NewId()}, ClientName: new("Test Client"), ClientURI: new("invalid-url"), } _, appErr := th.App.RegisterOAuthClient(th.Context, request, th.BasicUser.Id) require.NotNil(t, appErr) assert.Equal(t, "model.oauth.is_valid.homepage.app_error", appErr.Id) }) t.Run("PublicClient_Success", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableDynamicClientRegistration = new(true) }) dcrRequest := &model.ClientRegistrationRequest{ RedirectURIs: []string{"https://example.com/callback"}, ClientName: new("Test Public Client"), TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone), } registeredApp, appErr := th.App.RegisterOAuthClient(th.Context, dcrRequest, "") require.Nil(t, appErr) require.NotNil(t, registeredApp) require.Empty(t, registeredApp.ClientSecret) require.True(t, registeredApp.IsPublicClient()) require.Equal(t, model.ClientAuthMethodNone, registeredApp.GetTokenEndpointAuthMethod()) require.True(t, registeredApp.IsDynamicallyRegistered) }) t.Run("Custom scheme redirect URI persists end-to-end", func(t *testing.T) { request := &model.ClientRegistrationRequest{ RedirectURIs: []string{"cursor://anysphere.cursor-mcp/oauth/callback"}, ClientName: new("Desktop Client"), } app, appErr := th.App.RegisterOAuthClient(th.Context, request, th.BasicUser.Id) require.Nil(t, appErr) require.NotNil(t, app) assert.Equal(t, request.RedirectURIs, []string(app.CallbackUrls)) assert.True(t, app.IsDynamicallyRegistered) assert.NotEmpty(t, app.Id) }) } func TestGetAuthorizationServerMetadata_DCRConfig(t *testing.T) { th := Setup(t) // Enable OAuth service provider and set SiteURL th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = new(true) cfg.ServiceSettings.SiteURL = new("https://example.com") }) t.Run("DCR disabled", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableDynamicClientRegistration = new(false) }) metadata, err := th.App.GetAuthorizationServerMetadata(th.Context) require.Nil(t, err) require.NotNil(t, metadata) // Should not include registration endpoint when DCR is disabled assert.Empty(t, metadata.RegistrationEndpoint) // Should include basic OAuth endpoints assert.Equal(t, "https://example.com", metadata.Issuer) assert.Equal(t, "https://example.com/oauth/authorize", metadata.AuthorizationEndpoint) assert.Equal(t, "https://example.com/oauth/access_token", metadata.TokenEndpoint) }) t.Run("DCR enabled", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableDynamicClientRegistration = new(true) }) metadata, err := th.App.GetAuthorizationServerMetadata(th.Context) require.Nil(t, err) require.NotNil(t, metadata) // Should include registration endpoint when DCR is enabled assert.Equal(t, "https://example.com/api/v4/oauth/apps/register", metadata.RegistrationEndpoint) // Should include basic OAuth endpoints assert.Equal(t, "https://example.com", metadata.Issuer) assert.Equal(t, "https://example.com/oauth/authorize", metadata.AuthorizationEndpoint) assert.Equal(t, "https://example.com/oauth/access_token", metadata.TokenEndpoint) }) } func TestGetOAuthAccessTokenForCodeFlow(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Helper function to create a confidential OAuth app createConfidentialOAuthApp := func(name string) *model.OAuthApp { oapp := &model.OAuthApp{ Name: name + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://example.com/callback"}, } oapp, err := th.App.CreateOAuthApp(oapp) require.Nil(t, err) return oapp } // Helper function to get authorization code getAuthorizationCode := func(app *model.OAuthApp, resource string) string { authRequest := &model.AuthorizeRequest{ ResponseType: model.AuthCodeResponseType, ClientId: app.Id, RedirectURI: app.CallbackUrls[0], Scope: "user", State: "test_state", Resource: resource, } redirectURI, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.Nil(t, appErr) uri, urlErr := url.Parse(redirectURI) require.NoError(t, urlErr) code := uri.Query().Get("code") require.NotEmpty(t, code) return code } t.Run("PublicClient_WithPKCE_Success", func(t *testing.T) { dcrRequest := &model.ClientRegistrationRequest{ ClientName: new("Public Client Test"), RedirectURIs: []string{"https://example.com/callback"}, TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone), ClientURI: new("https://example.com"), } publicApp, appErr := th.App.RegisterOAuthClient(th.Context, dcrRequest, th.BasicUser2.Id) require.Nil(t, appErr) require.Empty(t, publicApp.ClientSecret) codeVerifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" codeChallenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" codeChallengeMethod := model.PKCECodeChallengeMethodS256 authRequest := &model.AuthorizeRequest{ ResponseType: model.ResponseTypeCode, ClientId: publicApp.Id, RedirectURI: publicApp.CallbackUrls[0], Scope: "user", State: "test_state", CodeChallenge: codeChallenge, CodeChallengeMethod: codeChallengeMethod, } redirectURL, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.Nil(t, appErr) uri, err := url.Parse(redirectURL) require.NoError(t, err) code := uri.Query().Get("code") require.NotEmpty(t, code) accessResponse, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, publicApp.Id, model.AccessTokenGrantType, authRequest.RedirectURI, code, "", "", codeVerifier, "", ) require.Nil(t, appErr) require.NotNil(t, accessResponse) require.NotEmpty(t, accessResponse.AccessToken) require.Equal(t, model.AccessTokenType, accessResponse.TokenType) require.Empty(t, accessResponse.RefreshToken) }) t.Run("PublicClient_WithoutPKCE_ShouldFail", func(t *testing.T) { dcrRequest := &model.ClientRegistrationRequest{ ClientName: new("Public Client Test"), RedirectURIs: []string{"https://example.com/callback"}, TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone), ClientURI: new("https://example.com"), } publicApp, appErr := th.App.RegisterOAuthClient(th.Context, dcrRequest, th.BasicUser2.Id) require.Nil(t, appErr) authRequest := &model.AuthorizeRequest{ ResponseType: model.ResponseTypeCode, ClientId: publicApp.Id, RedirectURI: publicApp.CallbackUrls[0], Scope: "user", State: "test_state", } _, appErr = th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "pkce_required") }) t.Run("ConfidentialClient_WithPKCE_Success", func(t *testing.T) { confidentialApp := &model.OAuthApp{ Name: "Confidential Client Test", CreatorId: th.BasicUser2.Id, Homepage: "https://example.com", Description: "test confidential client", CallbackUrls: []string{"https://example.com/callback"}, ClientSecret: model.NewId(), } confidentialApp, appErr := th.App.CreateOAuthApp(confidentialApp) require.Nil(t, appErr) require.NotEmpty(t, confidentialApp.ClientSecret) codeVerifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" codeChallenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" codeChallengeMethod := model.PKCECodeChallengeMethodS256 authRequest := &model.AuthorizeRequest{ ResponseType: model.ResponseTypeCode, ClientId: confidentialApp.Id, RedirectURI: confidentialApp.CallbackUrls[0], Scope: "user", State: "test_state", CodeChallenge: codeChallenge, CodeChallengeMethod: codeChallengeMethod, } redirectURL, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.Nil(t, appErr) uri, err := url.Parse(redirectURL) require.NoError(t, err) code := uri.Query().Get("code") require.NotEmpty(t, code) accessResponse, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, confidentialApp.Id, model.AccessTokenGrantType, authRequest.RedirectURI, code, confidentialApp.ClientSecret, "", codeVerifier, "", ) require.Nil(t, appErr) require.NotNil(t, accessResponse) require.NotEmpty(t, accessResponse.AccessToken) require.Equal(t, model.AccessTokenType, accessResponse.TokenType) require.NotEmpty(t, accessResponse.RefreshToken) }) t.Run("ConfidentialClient_WithoutPKCE_Success", func(t *testing.T) { confidentialApp := &model.OAuthApp{ Name: "Confidential Client Test", CreatorId: th.BasicUser2.Id, Homepage: "https://example.com", Description: "test confidential client", CallbackUrls: []string{"https://example.com/callback"}, ClientSecret: model.NewId(), } confidentialApp, appErr := th.App.CreateOAuthApp(confidentialApp) require.Nil(t, appErr) authRequest := &model.AuthorizeRequest{ ResponseType: model.ResponseTypeCode, ClientId: confidentialApp.Id, RedirectURI: confidentialApp.CallbackUrls[0], Scope: "user", State: "test_state", } redirectURL, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.Nil(t, appErr) uri, err := url.Parse(redirectURL) require.NoError(t, err) code := uri.Query().Get("code") require.NotEmpty(t, code) accessResponse, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, confidentialApp.Id, model.AccessTokenGrantType, authRequest.RedirectURI, code, confidentialApp.ClientSecret, "", "", "", ) require.Nil(t, appErr) require.NotNil(t, accessResponse) require.NotEmpty(t, accessResponse.AccessToken) require.Equal(t, model.AccessTokenType, accessResponse.TokenType) require.NotEmpty(t, accessResponse.RefreshToken) }) t.Run("ConfidentialClient_PKCEEnforcement", func(t *testing.T) { confidentialApp := &model.OAuthApp{ Name: "Confidential Client Test", CreatorId: th.BasicUser2.Id, Homepage: "https://example.com", Description: "test confidential client", CallbackUrls: []string{"https://example.com/callback"}, ClientSecret: model.NewId(), } confidentialApp, appErr := th.App.CreateOAuthApp(confidentialApp) require.Nil(t, appErr) codeChallenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" codeChallengeMethod := model.PKCECodeChallengeMethodS256 authRequest := &model.AuthorizeRequest{ ResponseType: model.ResponseTypeCode, ClientId: confidentialApp.Id, RedirectURI: confidentialApp.CallbackUrls[0], Scope: "user", State: "test_state", CodeChallenge: codeChallenge, CodeChallengeMethod: codeChallengeMethod, } redirectURL, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest) require.Nil(t, appErr) uri, err := url.Parse(redirectURL) require.NoError(t, err) code := uri.Query().Get("code") require.NotEmpty(t, code) _, appErr = th.App.GetOAuthAccessTokenForCodeFlow( th.Context, confidentialApp.Id, model.AccessTokenGrantType, authRequest.RedirectURI, code, confidentialApp.ClientSecret, "", "", "", ) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "pkce") }) t.Run("PublicClient_NoRefreshToken", func(t *testing.T) { dcrRequest := &model.ClientRegistrationRequest{ ClientName: new("Public Client Test"), RedirectURIs: []string{"https://example.com/callback"}, TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone), ClientURI: new("https://example.com"), } publicApp, appErr := th.App.RegisterOAuthClient(th.Context, dcrRequest, th.BasicUser2.Id) require.Nil(t, appErr) _, appErr = th.App.GetOAuthAccessTokenForCodeFlow( th.Context, publicApp.Id, model.RefreshTokenGrantType, "https://example.com/callback", "", "", "some_fake_refresh_token", "", "", ) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "public_client_refresh_token.app_error") }) t.Run("WithResourceParameter_Success", func(t *testing.T) { oapp := createConfidentialOAuthApp("TestResourceApp") resourceParam := "https://api.example.com/resource" code := getAuthorizationCode(oapp, resourceParam) accessResponse, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", resourceParam, ) require.Nil(t, appErr) require.NotNil(t, accessResponse) require.NotEmpty(t, accessResponse.AccessToken) require.Equal(t, model.AccessTokenType, accessResponse.TokenType) require.Equal(t, resourceParam, accessResponse.Audience) }) t.Run("ResourceParameterValidation", func(t *testing.T) { oapp := createConfidentialOAuthApp("TestResourceValidationApp") t.Run("Invalid resource parameter should fail", func(t *testing.T) { code := getAuthorizationCode(oapp, "") _, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", "invalid-resource-uri", ) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "resource") }) t.Run("Resource with fragment should fail", func(t *testing.T) { code := getAuthorizationCode(oapp, "") _, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", "https://api.example.com/resource#fragment", ) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "resource") }) }) t.Run("RefreshTokenWithResource", func(t *testing.T) { oapp := createConfidentialOAuthApp("TestRefreshResourceApp") resourceParam := "https://api.example.com/resource" t.Run("Refresh token with matching resource should succeed", func(t *testing.T) { code := getAuthorizationCode(oapp, resourceParam) // Get initial access token initialResponse, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", resourceParam, ) require.Nil(t, appErr) require.NotEmpty(t, initialResponse.RefreshToken) refreshResponse, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.RefreshTokenGrantType, oapp.CallbackUrls[0], "", oapp.ClientSecret, initialResponse.RefreshToken, "", resourceParam, ) require.Nil(t, appErr) require.NotNil(t, refreshResponse) require.Equal(t, resourceParam, refreshResponse.Audience) }) t.Run("Refresh token with mismatched resource should fail", func(t *testing.T) { code := getAuthorizationCode(oapp, resourceParam) // Get initial access token with original resource initialResponse, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", resourceParam, ) require.Nil(t, appErr) require.NotEmpty(t, initialResponse.RefreshToken) // Try to refresh with different resource - should fail _, appErr = th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.RefreshTokenGrantType, oapp.CallbackUrls[0], "", oapp.ClientSecret, initialResponse.RefreshToken, "", "https://different.api.com/resource", ) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "resource_mismatch") }) }) t.Run("DifferentClient_CannotRedeemCode", func(t *testing.T) { appA := createConfidentialOAuthApp("TestClientA") appB := createConfidentialOAuthApp("TestClientB") code := getAuthorizationCode(appA, "") _, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, appB.Id, model.AccessTokenGrantType, appA.CallbackUrls[0], code, appB.ClientSecret, "", "", "", ) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "client_id_mismatch") require.Equal(t, http.StatusBadRequest, appErr.StatusCode) }) t.Run("DifferentClient_CannotUseRefreshToken", func(t *testing.T) { appA := createConfidentialOAuthApp("TestClientA") appB := createConfidentialOAuthApp("TestClientB") code := getAuthorizationCode(appA, "") // Get a valid refresh token for appA tokenResp, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, appA.Id, model.AccessTokenGrantType, appA.CallbackUrls[0], code, appA.ClientSecret, "", "", "", ) require.Nil(t, appErr) require.NotEmpty(t, tokenResp.RefreshToken) // Try to use appA's refresh token with appB's credentials _, appErr = th.App.GetOAuthAccessTokenForCodeFlow( th.Context, appB.Id, model.RefreshTokenGrantType, appB.CallbackUrls[0], "", appB.ClientSecret, tokenResp.RefreshToken, "", "", ) require.NotNil(t, appErr) require.Contains(t, appErr.Id, "client_id_mismatch") require.Equal(t, http.StatusBadRequest, appErr.StatusCode) }) t.Run("RefreshToken_InvalidatesOldTokenInCache", func(t *testing.T) { oapp := createConfidentialOAuthApp("TestCacheInvalidation") code := getAuthorizationCode(oapp, "") // Get initial access token (newSession adds it to the session cache). initialResp, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", "", ) require.Nil(t, appErr) require.NotEmpty(t, initialResp.AccessToken) require.NotEmpty(t, initialResp.RefreshToken) oldToken := initialResp.AccessToken // Confirm the old session is reachable (from cache or DB). _, appErr = th.App.GetSession(oldToken) require.Nil(t, appErr, "old token should be valid before refresh") // Rotate the token. newResp, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.RefreshTokenGrantType, oapp.CallbackUrls[0], "", oapp.ClientSecret, initialResp.RefreshToken, "", "", ) require.Nil(t, appErr) require.NotEmpty(t, newResp.AccessToken) require.NotEqual(t, oldToken, newResp.AccessToken) // The old token must be rejected immediately, the session cache must // have been cleared so the removed DB row is not masked. _, appErr = th.App.GetSession(oldToken) require.NotNil(t, appErr, "old token must be invalid after refresh") // The new token must still be accepted. _, appErr = th.App.GetSession(newResp.AccessToken) require.Nil(t, appErr, "new token must remain valid after refresh") }) } func TestOAuthRefreshTokenGrantRejectsDeactivatedUser(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "RefreshGrantDeactivated_" + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://example.com/callback"}, ClientSecret: model.NewId(), } oapp, appErr := th.App.CreateOAuthApp(oapp) require.Nil(t, appErr) user := th.CreateUser(t) authRequest := &model.AuthorizeRequest{ ResponseType: model.AuthCodeResponseType, ClientId: oapp.Id, RedirectURI: oapp.CallbackUrls[0], Scope: "user", State: "test_state", } redirectURL, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, user.Id, authRequest) require.Nil(t, appErr) uri, parseErr := url.Parse(redirectURL) require.NoError(t, parseErr) code := uri.Query().Get("code") require.NotEmpty(t, code) tokenResp, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "", "", "", ) require.Nil(t, appErr) require.NotEmpty(t, tokenResp.AccessToken) require.NotEmpty(t, tokenResp.RefreshToken) require.NoError(t, th.App.Srv().Store().Session().Remove(tokenResp.AccessToken)) _, appErr = th.App.UpdateActive(th.Context, user, false) require.Nil(t, appErr) refreshResp, appErr := th.App.GetOAuthAccessTokenForCodeFlow( th.Context, oapp.Id, model.RefreshTokenGrantType, oapp.CallbackUrls[0], "", oapp.ClientSecret, tokenResp.RefreshToken, "", "", ) require.NotNil(t, appErr, "refresh token grant must fail for an inactive user") require.Nil(t, refreshResp) } func TestOAuthImplicitGrantRejectsDeactivatedUser(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "ImplicitGrantDeactivated_" + model.NewRandomString(10), CreatorId: th.BasicUser2.Id, Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://example.com/callback"}, ClientSecret: model.NewId(), } oapp, appErr := th.App.CreateOAuthApp(oapp) require.Nil(t, appErr) user := th.CreateUser(t) _, appErr = th.App.UpdateActive(th.Context, user, false) require.Nil(t, appErr) authRequest := &model.AuthorizeRequest{ ResponseType: model.ImplicitResponseType, ClientId: oapp.Id, RedirectURI: oapp.CallbackUrls[0], Scope: "user", State: "test_state", } session, appErr := th.App.GetOAuthAccessTokenForImplicitFlow(th.Context, user.Id, authRequest) require.NotNil(t, appErr, "implicit grant must fail for an inactive user") require.Nil(t, session) accessData, sErr := th.App.Srv().Store().OAuth().GetAccessDataByUserForApp(user.Id, oapp.Id) require.NoError(t, sErr) require.Empty(t, accessData, "no access data may be persisted for an inactive user") } func TestSwitchOAuthToEmail(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) setupOAuthUser := func(t *testing.T) *model.User { t.Helper() authData := model.NewId() _, err := th.App.Srv().Store().User().UpdateAuthData(th.BasicUser.Id, model.UserAuthServiceGitlab, &authData, th.BasicUser.Email, true) require.NoError(t, err) th.App.InvalidateCacheForUser(th.BasicUser.Id) user, appErr := th.App.GetUser(th.Context, th.BasicUser.Id) require.Nil(t, appErr) require.Equal(t, model.UserAuthServiceGitlab, user.AuthService) return user } t.Run("rejects integration session", func(t *testing.T) { user := setupOAuthUser(t) rctx := th.Context.WithSession(&model.Session{UserId: user.Id, Id: model.NewId(), IsOAuth: true}) _, appErr := th.App.SwitchOAuthToEmail(rctx, user.Email, model.NewTestPassword(), user.Id) require.NotNil(t, appErr) require.Equal(t, "api.user.oauth_to_email.integration_session.app_error", appErr.Id) require.Equal(t, http.StatusForbidden, appErr.StatusCode) user, appErr = th.App.GetUser(th.Context, user.Id) require.Nil(t, appErr) require.Equal(t, model.UserAuthServiceGitlab, user.AuthService) }) t.Run("allows regular session", func(t *testing.T) { user := setupOAuthUser(t) rctx := th.Context.WithSession(&model.Session{UserId: user.Id, Id: model.NewId()}) link, appErr := th.App.SwitchOAuthToEmail(rctx, user.Email, model.NewTestPassword(), user.Id) require.Nil(t, appErr) require.Equal(t, "/login?extra=signin_change", link) user, appErr = th.App.GetUser(th.Context, user.Id) require.Nil(t, appErr) require.Empty(t, user.AuthService) }) } func TestParseOAuthStateTokenExtra(t *testing.T) { t.Run("valid token with normal values", func(t *testing.T) { email, action, cookie, err := parseOAuthStateTokenExtra("user@example.com:email_to_sso:randomcookie123") require.NoError(t, err) assert.Equal(t, "user@example.com", email) assert.Equal(t, "email_to_sso", action) assert.Equal(t, "randomcookie123", cookie) }) t.Run("valid token with empty email and action", func(t *testing.T) { email, action, cookie, err := parseOAuthStateTokenExtra("::randomcookie123") require.NoError(t, err) assert.Equal(t, "", email) assert.Equal(t, "", action) assert.Equal(t, "randomcookie123", cookie) }) t.Run("token with too many colons", func(t *testing.T) { _, _, _, err := parseOAuthStateTokenExtra("user@example.com:action:value:extra") require.Error(t, err) assert.Contains(t, err.Error(), "expected exactly 3 parts") assert.Contains(t, err.Error(), "got 4") }) t.Run("token with too few colons", func(t *testing.T) { _, _, _, err := parseOAuthStateTokenExtra("user@example.com:email_to_sso") require.Error(t, err) assert.Contains(t, err.Error(), "expected exactly 3 parts") assert.Contains(t, err.Error(), "got 2") }) t.Run("token with no colons", func(t *testing.T) { _, _, _, err := parseOAuthStateTokenExtra("invalidtoken") require.Error(t, err) assert.Contains(t, err.Error(), "expected exactly 3 parts") assert.Contains(t, err.Error(), "got 1") }) t.Run("empty token string", func(t *testing.T) { _, _, _, err := parseOAuthStateTokenExtra("") require.Error(t, err) assert.Contains(t, err.Error(), "expected exactly 3 parts") }) } func TestAuthorizeOAuthUser_InvalidToken(t *testing.T) { mainHelper.Parallel(t) th := Setup(t) mockProvider := &mocks.OAuthProvider{} einterfaces.RegisterOAuthProvider(model.ServiceOpenid, mockProvider) service := model.ServiceOpenid th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true cfg.OpenIdSettings.Enable = new(true) cfg.OpenIdSettings.Id = new("test-client-id") cfg.OpenIdSettings.Secret = new("test-secret") cfg.OpenIdSettings.Scope = new(OpenIDScope) }) mockProvider.On("GetSSOSettings", mock.Anything, mock.Anything, service).Return(&model.SSOSettings{ Enable: new(true), Id: new("test-client-id"), Secret: new("test-secret"), }, nil) t.Run("rejects token with extra delimiters in email field", func(t *testing.T) { cookieValue := model.NewId() invalidEmail := "user@example.com:action" action := "email_to_sso" tokenExtra := generateOAuthStateTokenExtra(invalidEmail, action, cookieValue) token, err := th.App.CreateOAuthStateToken(tokenExtra) require.Nil(t, err) stateProps := map[string]string{ "token": token.Token, "email": "user@example.com", "action": action, } state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps))) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ Name: CookieOAuth, Value: "action:" + cookieValue, }) _, _, _, appErr := th.App.AuthorizeOAuthUser(th.Context, w, r, service, "auth-code", state, "http://localhost/callback") require.NotNil(t, appErr) assert.Equal(t, http.StatusBadRequest, appErr.StatusCode) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", appErr.Id) }) t.Run("rejects token with mismatched email", func(t *testing.T) { cookieValue := model.NewId() action := "email_to_sso" tokenExtra := generateOAuthStateTokenExtra("token@example.com", action, cookieValue) token, err := th.App.CreateOAuthStateToken(tokenExtra) require.Nil(t, err) stateProps := map[string]string{ "token": token.Token, "email": "state@example.com", "action": action, } state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps))) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ Name: CookieOAuth, Value: cookieValue, }) _, _, _, appErr := th.App.AuthorizeOAuthUser(th.Context, w, r, service, "auth-code", state, "http://localhost/callback") require.NotNil(t, appErr) assert.Equal(t, http.StatusBadRequest, appErr.StatusCode) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", appErr.Id) }) t.Run("rejects token with mismatched action", func(t *testing.T) { cookieValue := model.NewId() email := "user@example.com" tokenExtra := generateOAuthStateTokenExtra(email, "email_to_sso", cookieValue) token, err := th.App.CreateOAuthStateToken(tokenExtra) require.Nil(t, err) stateProps := map[string]string{ "token": token.Token, "email": email, "action": "sso_to_email", } state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps))) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ Name: CookieOAuth, Value: cookieValue, }) _, _, _, appErr := th.App.AuthorizeOAuthUser(th.Context, w, r, service, "auth-code", state, "http://localhost/callback") require.NotNil(t, appErr) assert.Equal(t, http.StatusBadRequest, appErr.StatusCode) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", appErr.Id) }) t.Run("rejects token with mismatched cookie", func(t *testing.T) { email := "user@example.com" action := "email_to_sso" tokenExtra := generateOAuthStateTokenExtra(email, action, "token-cookie-value") token, err := th.App.CreateOAuthStateToken(tokenExtra) require.Nil(t, err) stateProps := map[string]string{ "token": token.Token, "email": email, "action": action, } state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps))) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ Name: CookieOAuth, Value: "different-cookie-value", }) _, _, _, appErr := th.App.AuthorizeOAuthUser(th.Context, w, r, service, "auth-code", state, "http://localhost/callback") require.NotNil(t, appErr) assert.Equal(t, http.StatusBadRequest, appErr.StatusCode) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", appErr.Id) }) } // TestLoginByIntune_InterfaceNotAvailable tests that LoginByIntune returns proper error when enterprise not compiled func TestLoginByIntune_InterfaceNotAvailable(t *testing.T) { th := Setup(t).InitBasic(t) // Intune interface should be nil in non-enterprise setup require.Nil(t, th.App.Intune()) // Attempt login user, appErr := th.App.LoginByIntune(th.Context, "fake-token") // Should return error require.Nil(t, user) require.NotNil(t, appErr) assert.Equal(t, "api.user.login_by_intune.not_available.app_error", appErr.Id) assert.Equal(t, http.StatusNotImplemented, appErr.StatusCode) } // TestLoginByIntune_NotConfigured tests that LoginByIntune returns proper error when Intune not configured func TestLoginByIntune_NotConfigured(t *testing.T) { th := SetupEnterprise(t).InitBasic(t) // Create mock Intune interface mockIntune := &mocks.IntuneInterface{} mockIntune.On("IsConfigured").Return(false) // Replace Intune interface with mock originalIntune := th.App.ch.Intune th.App.ch.Intune = mockIntune defer func() { th.App.ch.Intune = originalIntune }() // Attempt login user, appErr := th.App.LoginByIntune(th.Context, "fake-token") // Should return error require.Nil(t, user) require.NotNil(t, appErr) assert.Equal(t, "api.user.login_by_intune.not_configured.app_error", appErr.Id) assert.Equal(t, http.StatusBadRequest, appErr.StatusCode) mockIntune.AssertExpectations(t) } // TestLoginByIntune_Success_Office365 tests successful login with Office365 auth service func TestLoginByIntune_Success_Office365(t *testing.T) { th := SetupEnterprise(t).InitBasic(t) // Create test user with Office365 auth testUser, appErr := th.App.CreateUser(th.Context, &model.User{ Email: "office365user@example.com", Username: "office365user", AuthService: model.ServiceOffice365, AuthData: new("test-oid-123"), EmailVerified: true, }) require.Nil(t, appErr) // Create mock Intune interface mockIntune := &mocks.IntuneInterface{} mockIntune.On("IsConfigured").Return(true) mockIntune.On("Login", mock.Anything, "valid-token").Return(testUser, nil) // Replace Intune interface with mock originalIntune := th.App.ch.Intune th.App.ch.Intune = mockIntune defer func() { th.App.ch.Intune = originalIntune }() // Attempt login user, appErr := th.App.LoginByIntune(th.Context, "valid-token") // Should succeed require.Nil(t, appErr) require.NotNil(t, user) assert.Equal(t, testUser.Id, user.Id) assert.Equal(t, model.ServiceOffice365, user.AuthService) mockIntune.AssertExpectations(t) } // TestLoginByIntune_Success_SAML tests successful login with SAML auth service func TestLoginByIntune_Success_SAML(t *testing.T) { th := SetupEnterprise(t).InitBasic(t) // Create test user with SAML auth testUser, appErr := th.App.CreateUser(th.Context, &model.User{ Email: "samluser@example.com", Username: "samluser", AuthService: model.UserAuthServiceSaml, AuthData: new("test@example.com"), EmailVerified: true, }) require.Nil(t, appErr) // Create mock Intune interface mockIntune := &mocks.IntuneInterface{} mockIntune.On("IsConfigured").Return(true) mockIntune.On("Login", mock.Anything, "valid-token").Return(testUser, nil) // Replace Intune interface with mock originalIntune := th.App.ch.Intune th.App.ch.Intune = mockIntune defer func() { th.App.ch.Intune = originalIntune }() // Attempt login user, appErr := th.App.LoginByIntune(th.Context, "valid-token") // Should succeed require.Nil(t, appErr) require.NotNil(t, user) assert.Equal(t, testUser.Id, user.Id) assert.Equal(t, model.UserAuthServiceSaml, user.AuthService) mockIntune.AssertExpectations(t) } // TestLoginByIntune_BotAccountBlocked tests that bot accounts cannot login via Intune func TestLoginByIntune_BotAccountBlocked(t *testing.T) { th := SetupEnterprise(t).InitBasic(t) // Create bot account bot := th.CreateBot(t) botUser, appErr := th.App.GetUser(th.Context, bot.UserId) require.Nil(t, appErr) // Create mock Intune interface that returns bot user mockIntune := &mocks.IntuneInterface{} mockIntune.On("IsConfigured").Return(true) mockIntune.On("Login", mock.Anything, "bot-token").Return(botUser, nil) // Replace Intune interface with mock originalIntune := th.App.ch.Intune th.App.ch.Intune = mockIntune defer func() { th.App.ch.Intune = originalIntune }() // Attempt login user, appErr := th.App.LoginByIntune(th.Context, "bot-token") // Should be blocked require.Nil(t, user) require.NotNil(t, appErr) assert.Equal(t, "api.user.login_by_intune.bot_login_forbidden.app_error", appErr.Id) assert.Equal(t, http.StatusForbidden, appErr.StatusCode) mockIntune.AssertExpectations(t) } // TestLoginByIntune_AccountLocked tests that deleted/locked accounts cannot login func TestLoginByIntune_AccountLocked(t *testing.T) { th := SetupEnterprise(t).InitBasic(t) // Create user and then soft delete it deletedUser, appErr := th.App.CreateUser(th.Context, &model.User{ Email: "deleteduser@example.com", Username: "deleteduser", AuthService: model.ServiceOffice365, AuthData: new("deleted-oid-123"), EmailVerified: true, }) require.Nil(t, appErr) // Soft delete the user (deactivate) _, appErr = th.App.UpdateActive(th.Context, deletedUser, false) require.Nil(t, appErr) // Reload user to get updated DeleteAt deletedUser, appErr = th.App.GetUser(th.Context, deletedUser.Id) require.Nil(t, appErr) // Create mock Intune interface that returns deleted user mockIntune := &mocks.IntuneInterface{} mockIntune.On("IsConfigured").Return(true) mockIntune.On("Login", mock.Anything, "deleted-token").Return(deletedUser, nil) // Replace Intune interface with mock originalIntune := th.App.ch.Intune th.App.ch.Intune = mockIntune defer func() { th.App.ch.Intune = originalIntune }() // Attempt login user, appErr := th.App.LoginByIntune(th.Context, "deleted-token") // Should be blocked require.Nil(t, user) require.NotNil(t, appErr) assert.Equal(t, "api.user.login_by_intune.account_locked.app_error", appErr.Id) assert.Equal(t, http.StatusConflict, appErr.StatusCode) mockIntune.AssertExpectations(t) } // TestLoginByIntune_TokenValidationFailure tests that invalid tokens are rejected func TestLoginByIntune_TokenValidationFailure(t *testing.T) { th := SetupEnterprise(t).InitBasic(t) // Create mock Intune interface that returns validation error mockIntune := &mocks.IntuneInterface{} mockIntune.On("IsConfigured").Return(true) mockIntune.On("Login", mock.Anything, "invalid-token").Return(nil, model.NewAppError( "IntuneInterface.Login", "ent.intune.validate_token.invalid_token.app_error", nil, "token validation failed", http.StatusBadRequest, )) // Replace Intune interface with mock originalIntune := th.App.ch.Intune th.App.ch.Intune = mockIntune defer func() { th.App.ch.Intune = originalIntune }() // Attempt login user, appErr := th.App.LoginByIntune(th.Context, "invalid-token") // Should return validation error require.Nil(t, user) require.NotNil(t, appErr) assert.Equal(t, "ent.intune.validate_token.invalid_token.app_error", appErr.Id) assert.Equal(t, http.StatusBadRequest, appErr.StatusCode) mockIntune.AssertExpectations(t) } func TestRedactOAuthTokenResponse(t *testing.T) { testCases := []struct { Description string Body string Expected string }{ { "empty body", "", "", }, { "body without any token", `{"error":"invalid_grant","error_description":"code expired"}`, `{"error":"invalid_grant","error_description":"code expired"}`, }, { "json access token", `{"access_token":"abcd1234","token_type":"bearer"}`, `{"access_token":"[REDACTED]","token_type":"bearer"}`, }, { "json access token with whitespace", `{"access_token" : "abcd1234"}`, `{"access_token" : "[REDACTED]"}`, }, { "json access token with mixed case key", `{"Access_Token":"abcd1234"}`, `{"Access_Token":"[REDACTED]"}`, }, { "json refresh and id tokens", `{"access_token":"a","refresh_token":"b","id_token":"c","expires_in":3600}`, `{"access_token":"[REDACTED]","refresh_token":"[REDACTED]","id_token":"[REDACTED]","expires_in":3600}`, }, { "form encoded tokens", "access_token=abcd1234&scope=read&refresh_token=efgh5678", "access_token=[REDACTED]&scope=read&refresh_token=[REDACTED]", }, { "truncated json body", `{"access_token":"abcd1234`, `{"access_token":"[REDACTED]`, }, { "json access token containing an escaped quote", `{"access_token":"abc\"def","token_type":"bearer"}`, `{"access_token":"[REDACTED]","token_type":"bearer"}`, }, { "json access token ending with an escaped backslash", `{"access_token":"abc\\","token_type":"bearer"}`, `{"access_token":"[REDACTED]","token_type":"bearer"}`, }, { "json access token containing escaped quotes and backslashes", `{"refresh_token":"a\\b\"c\\\"d","expires_in":3600}`, `{"refresh_token":"[REDACTED]","expires_in":3600}`, }, { "truncated json body ending with a lone backslash", `{"access_token":"abcd1234\`, `{"access_token":"[REDACTED]\`, }, { "json member name with escaped underscore", `{"access\u005ftoken":"abcd1234","token_type":"bearer"}`, `{"access\u005ftoken":"[REDACTED]","token_type":"bearer"}`, }, { "json member name with escaped underscore and mixed case", `{"Refresh\u005FTOKEN":"abcd1234"}`, `{"Refresh\u005FTOKEN":"[REDACTED]"}`, }, { "form field name with percent encoded underscore", "access%5Ftoken=abcd1234&scope=read", "access%5Ftoken=[REDACTED]&scope=read", }, { "form field name with lowercase percent encoded underscore", "scope=read&id%5ftoken=abcd1234", "scope=read&id%5ftoken=[REDACTED]", }, { "form encoded body inside a json error field", `{"error":"access_token=abcd1234"}`, `{"error":"access_token=[REDACTED]"}`, }, { "form encoded token in json error field preceded by field with = in its value", `{"error_uri":"https://provider.com/help?code=42","error_description":"access_token=SECRET"}`, `{"error_uri":"https://provider.com/help?code=42","error_description":"access_token=[REDACTED]"}`, }, { "json member name that merely ends with a token name", `{"my_access_token_hint":"abcd1234"}`, `{"my_access_token_hint":"abcd1234"}`, }, { "form field name that merely contains a token name", "xaccess_token=abcd1234", "xaccess_token=abcd1234", }, } for _, tc := range testCases { t.Run(tc.Description, func(t *testing.T) { assert.Equal(t, tc.Expected, redactOAuthTokenResponse(tc.Body)) }) } t.Run("no part of a token value survives redaction", func(t *testing.T) { for _, body := range []string{ `{"access_token":"abc\"def","token_type":"bearer"}`, `{"access_token":"abc\\","token_type":"bearer"}`, `{"refresh_token":"a\\b\"c\\\"def","expires_in":3600}`, `{"id_token":"abc\"def"}`, "access_token=abc%22def&scope=read", `{"access\u005ftoken":"abcdef"}`, "access%5Ftoken=abcdef&scope=read", } { actual := redactOAuthTokenResponse(body) assert.NotContains(t, actual, "abc") assert.NotContains(t, actual, "def") } }) }