mattermost-community-enterp.../channels/app/oauth_test.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

1617 lines
51 KiB
Go

// 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()
defer th.TearDown()
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: model.NewPointer("Public Client Test"),
RedirectURIs: []string{"https://example.com/callback"},
TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone),
ClientURI: model.NewPointer("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)
defer th.TearDown()
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)
defer th.TearDown()
*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 = ""
}
})
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, "")
defer th.TearDown()
_, _, _, 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, "")
defer th.TearDown()
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, "")
defer th.TearDown()
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, "")
defer th.TearDown()
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, "")
defer th.TearDown()
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, "")
defer th.TearDown()
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, "")
defer th.TearDown()
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, "")
defer th.TearDown()
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("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)
defer th.TearDown()
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)
defer th.TearDown()
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) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: model.NewId(),
TokenType: "",
})
require.NoError(t, err)
}))
defer server.Close()
th := setup(t, true, true, true, server.URL)
defer th.TearDown()
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)
})
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)
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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, "")
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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()
defer th.TearDown()
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()
defer th.TearDown()
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()
defer th.TearDown()
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: model.NewPointer("Test Client"),
ClientURI: model.NewPointer("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: model.NewPointer("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: model.NewPointer("Test Client"),
ClientURI: model.NewPointer("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 = model.NewPointer(true)
})
dcrRequest := &model.ClientRegistrationRequest{
RedirectURIs: []string{"https://example.com/callback"},
ClientName: model.NewPointer("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)
})
}
func TestGetAuthorizationServerMetadata_DCRConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Enable OAuth service provider and set SiteURL
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.EnableOAuthServiceProvider = model.NewPointer(true)
cfg.ServiceSettings.SiteURL = model.NewPointer("https://example.com")
})
t.Run("DCR disabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.EnableDynamicClientRegistration = model.NewPointer(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 = model.NewPointer(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()
defer th.TearDown()
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: model.NewPointer("Public Client Test"),
RedirectURIs: []string{"https://example.com/callback"},
TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone),
ClientURI: model.NewPointer("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: model.NewPointer("Public Client Test"),
RedirectURIs: []string{"https://example.com/callback"},
TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone),
ClientURI: model.NewPointer("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: model.NewPointer("Public Client Test"),
RedirectURIs: []string{"https://example.com/callback"},
TokenEndpointAuthMethod: model.NewPointer(model.ClientAuthMethodNone),
ClientURI: model.NewPointer("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")
})
})
}
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)
defer th.TearDown()
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 = model.NewPointer(true)
cfg.OpenIdSettings.Id = model.NewPointer("test-client-id")
cfg.OpenIdSettings.Secret = model.NewPointer("test-secret")
cfg.OpenIdSettings.Scope = model.NewPointer(OpenIDScope)
})
mockProvider.On("GetSSOSettings", mock.Anything, mock.Anything, service).Return(&model.SSOSettings{
Enable: model.NewPointer(true),
Id: model.NewPointer("test-client-id"),
Secret: model.NewPointer("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)
})
}