mattermost/server/channels/web/oauth_test.go
Jesse Hallam e3fbf8711f
MM-68149: Upgrade to Go 1.26.2 (#36418)
* MM-68149: upgrade to Go 1.26.2

Update go directive in go.mod and .go-version.

* MM-68149: replace pointer helpers with Go 1.26 new()

Go 1.26 extends the built-in new() to accept an initial value expression,
making typed-pointer helpers like model.NewPointer(x), bToP(x), and boolPtr(x)
redundant. Replace every call site with new(x) and remove the now-unused
helper functions and their //go:fix inline directives.

* MM-68149: apply go fix for reflect API and format-string changes

- reflect.Ptr → reflect.Pointer (renamed in Go 1.18, deprecated alias removed in 1.26)
- reflect range-over-struct: for i := 0; i < t.NumField(); i++ → for field := range t.Fields()
  and the equivalent for Methods() and interface types
- Fix format-string concatenation and variadic-arg mismatches flagged by go vet

* MM-68149: update JPEG fixtures and test infrastructure for Go 1.26 encoder

Go 1.26 ships a new image/jpeg encoder that produces slightly different output.
Regenerate all JPEG fixture files and switch the comparison helpers from
byte-equality to pixel-level comparison with a small per-channel tolerance,
so minor encoder drift across patch versions is handled automatically.

Add -update-fixtures flag to make it easy to regenerate fixtures after future
major Go upgrades. Document the update procedure in tests/README.md.

* MM-68149: CI check that go fix ./... produces no changes

* Fix real bugs flagged by CodeRabbit review

- group.go: set newGroup.MemberCount not group.MemberCount (member count
  was populated on the wrong variable and lost before publish/return)
- file_test.go: guard compareImage(GetFilePreview) on the preview slice
  length, not the thumbnail slice length (copy-paste error)
- config_test.go: remove duplicate MinimumLength assignment

* fixup! Fix real bugs flagged by CodeRabbit review
2026-05-12 15:59:12 +00:00

1192 lines
43 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"context"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"slices"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
func TestOAuthComplete_AccessDenied(t *testing.T) {
th := Setup(t).InitBasic(t)
c := &Context{
App: th.App,
Params: &Params{
Service: "TestService",
},
AppContext: request.EmptyContext(th.TestLogger),
}
responseWriter := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/signup/TestService/complete?error=access_denied", nil)
require.NoError(t, err)
completeOAuth(c, responseWriter, request)
response := responseWriter.Result()
assert.Equal(t, http.StatusTemporaryRedirect, response.StatusCode)
location, err := url.Parse(response.Header.Get("Location"))
require.NoError(t, err)
assert.Equal(t, "oauth_access_denied", location.Query().Get("type"))
assert.Equal(t, "TestService", location.Query().Get("service"))
}
func TestAuthorizeOAuthApp(t *testing.T) {
th := Setup(t).InitBasic(t)
th.Login(t, apiClient, th.SystemAdminUser)
enableOAuth := *th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{
Name: GenerateTestAppName(),
Homepage: "https://nowhere.com",
Description: "test",
CallbackUrls: []string{"https://nowhere.com"},
CreatorId: th.SystemAdminUser.Id,
ClientSecret: model.NewId(), // Explicitly set secret to make it confidential
}
rapp, appErr := th.App.CreateOAuthApp(oapp)
require.Nil(t, appErr)
authRequest := &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: rapp.Id,
RedirectURI: rapp.CallbackUrls[0],
Scope: "",
State: "123",
}
// Test auth code flow
ruri, _, err := apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
require.NotEmpty(t, ruri, "redirect url should be set")
ru, err := url.Parse(ruri)
require.NoError(t, err)
require.NotNil(t, ru, "redirect url unparseable")
require.NotEmpty(t, ru.Query().Get("code"), "authorization code not returned")
require.Equal(t, ru.Query().Get("state"), authRequest.State, "returned state doesn't match")
// Test implicit flow
authRequest.ResponseType = model.ImplicitResponseType
ruri, _, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
require.False(t, ruri == "", "redirect url should be set")
ru, err = url.Parse(ruri)
require.NoError(t, err)
require.NotNil(t, ru, "redirect url unparseable")
values, err := url.ParseQuery(ru.Fragment)
require.NoError(t, err)
assert.False(t, values.Get("access_token") == "", "access_token not returned")
assert.Equal(t, authRequest.State, values.Get("state"), "returned state doesn't match")
oldToken := apiClient.AuthToken
apiClient.AuthToken = values.Get("access_token")
_, resp, err := apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
apiClient.AuthToken = oldToken
authRequest.RedirectURI = ""
_, resp, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
authRequest.RedirectURI = "http://somewhereelse.com"
_, resp, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
authRequest.RedirectURI = rapp.CallbackUrls[0]
authRequest.ResponseType = ""
_, resp, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
authRequest.ResponseType = model.AuthCodeResponseType
authRequest.ClientId = ""
_, resp, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
authRequest.ClientId = model.NewId()
_, resp, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// test callback URI doesn't have malformed query parameters
oappWithQueryParamInCallback := &model.OAuthApp{
Name: GenerateTestAppName(),
Homepage: "https://nowhere.com",
Description: "test",
CallbackUrls: []string{"https://nowhere.com?simply=lovely"},
CreatorId: th.SystemAdminUser.Id,
ClientSecret: model.NewId(), // Explicitly set secret to make it confidential
}
rapp, appErr = th.App.CreateOAuthApp(oappWithQueryParamInCallback)
require.Nil(t, appErr)
authRequest = &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: rapp.Id,
RedirectURI: rapp.CallbackUrls[0],
Scope: "",
State: "/oauthcallback?sesskey=abcd&other=123",
}
uriResponse, _, err := apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
ru, err = url.Parse(uriResponse)
require.NoError(t, err)
require.NotEmpty(t, uriResponse, "redirect url should be set")
require.NotNil(t, ru, "redirect url unparseable")
// require no query parameter to have "?"
require.False(t, strings.Contains(ru.RawQuery, "?"), "should not malform query parameters")
require.NotEmpty(t, ru.Query().Get("code"), "authorization code not returned")
// test state is not encoded multiple times
require.Equal(t, ru.Query().Get("state"), authRequest.State, "returned state doesn't match")
// test state is URL encoded at least once
require.Empty(t, ru.Query().Get("other"), "state's query parameters should not leak")
}
func TestDeauthorizeOAuthApp(t *testing.T) {
th := Setup(t).InitBasic(t)
th.Login(t, apiClient, th.SystemAdminUser)
enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{
Name: GenerateTestAppName(),
Homepage: "https://nowhere.com",
Description: "test",
CallbackUrls: []string{"https://nowhere.com"},
CreatorId: th.SystemAdminUser.Id,
ClientSecret: model.NewId(), // Explicitly set secret to make it confidential
}
rapp, appErr := th.App.CreateOAuthApp(oapp)
require.Nil(t, appErr)
authRequest := &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: rapp.Id,
RedirectURI: rapp.CallbackUrls[0],
Scope: "",
State: "123",
}
_, _, err := apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
_, err = apiClient.DeauthorizeOAuthApp(context.Background(), rapp.Id)
require.NoError(t, err)
resp, err := apiClient.DeauthorizeOAuthApp(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = apiClient.DeauthorizeOAuthApp(context.Background(), model.NewId())
require.NoError(t, err)
th.Logout(apiClient)
resp, err = apiClient.DeauthorizeOAuthApp(context.Background(), rapp.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestOAuthAccessToken(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
th := Setup(t).InitBasic(t)
th.Login(t, apiClient, th.SystemAdminUser)
enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
defaultRolePermissions := th.SaveDefaultRolePermissions(t)
defer func() {
th.RestoreDefaultRolePermissions(t, defaultRolePermissions)
}()
th.AddPermissionToRole(t, model.PermissionManageOAuth.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageOAuth.Id, model.SystemUserRoleId)
oauthApp := &model.OAuthApp{
Name: "TestApp5" + model.NewId(),
Homepage: "https://nowhere.com",
Description: "test",
CallbackUrls: []string{"https://nowhere.com"},
CreatorId: th.SystemAdminUser.Id,
ClientSecret: model.NewId(), // Explicitly set secret to make it confidential
}
oauthApp, appErr := th.App.CreateOAuthApp(oauthApp)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}}
_, _, err := apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - oauth providing turned off")
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
authRequest := &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: oauthApp.Id,
RedirectURI: oauthApp.CallbackUrls[0],
Scope: "all",
State: "123",
}
redirect, _, err := apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
rurl, err := url.Parse(redirect)
require.NoError(t, err)
data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{oauthApp.Id}, "client_secret": []string{oauthApp.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}}
_, resp, err := apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - bad grant type")
CheckBadRequestStatus(t, resp)
data.Set("grant_type", model.AccessTokenGrantType)
data.Set("client_id", "")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - missing client id")
data.Set("client_id", "junk")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - bad client id")
data.Set("client_id", oauthApp.Id)
data.Set("client_secret", "")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - missing client secret")
data.Set("client_secret", "junk")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - bad client secret")
data.Set("client_secret", oauthApp.ClientSecret)
data.Set("code", "")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - missing code")
data.Set("code", "junk")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - bad code")
data.Set("code", rurl.Query().Get("code"))
data.Set("redirect_uri", "junk")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - non-matching redirect uri")
// reset data for successful request
data.Set("grant_type", model.AccessTokenGrantType)
data.Set("client_id", oauthApp.Id)
data.Set("client_secret", oauthApp.ClientSecret)
data.Set("code", rurl.Query().Get("code"))
data.Set("redirect_uri", oauthApp.CallbackUrls[0])
token := ""
refreshToken := ""
rsp, _, err := apiClient.GetOAuthAccessToken(context.Background(), data)
require.NoError(t, err)
require.NotEmpty(t, rsp.AccessToken, "access token not returned")
require.NotEmpty(t, rsp.RefreshToken, "refresh token not returned")
token, refreshToken = rsp.AccessToken, rsp.RefreshToken
require.Equal(t, rsp.TokenType, model.AccessTokenType, "access token type incorrect")
_, err = apiClient.DoAPIGet(context.Background(), "/oauth_test", "")
require.NoError(t, err)
apiClient.SetOAuthToken("")
_, err = apiClient.DoAPIGet(context.Background(), "/oauth_test", "")
require.Error(t, err, "should have failed - no access token provided")
apiClient.SetOAuthToken("badtoken")
_, err = apiClient.DoAPIGet(context.Background(), "/oauth_test", "")
require.Error(t, err, "should have failed - bad token provided")
apiClient.SetOAuthToken(token)
_, err = apiClient.DoAPIGet(context.Background(), "/oauth_test", "")
require.NoError(t, err)
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "should have failed - tried to reuse auth code")
data.Set("grant_type", model.RefreshTokenGrantType)
data.Set("client_id", oauthApp.Id)
data.Set("client_secret", oauthApp.ClientSecret)
data.Set("refresh_token", "")
data.Set("redirect_uri", oauthApp.CallbackUrls[0])
data.Del("code")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "Should have failed - refresh token empty")
data.Set("refresh_token", refreshToken)
rsp, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.NoError(t, err)
require.NotEmpty(t, rsp.AccessToken, "access token not returned")
require.NotEmpty(t, rsp.RefreshToken, "refresh token not returned")
require.NotEqual(t, rsp.RefreshToken, refreshToken, "refresh token did not update")
require.Equal(t, rsp.TokenType, model.AccessTokenType, "access token type incorrect")
apiClient.SetOAuthToken(rsp.AccessToken)
_, err = apiClient.DoAPIGet(context.Background(), "/oauth_test", "")
require.NoError(t, err)
data.Set("refresh_token", rsp.RefreshToken)
rsp, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.NoError(t, err)
require.NotEmpty(t, rsp.AccessToken, "access token not returned")
require.NotEmpty(t, rsp.RefreshToken, "refresh token not returned")
require.NotEqual(t, rsp.RefreshToken, refreshToken, "refresh token did not update")
require.Equal(t, rsp.TokenType, model.AccessTokenType, "access token type incorrect")
apiClient.SetOAuthToken(rsp.AccessToken)
_, err = apiClient.DoAPIGet(context.Background(), "/oauth_test", "")
require.NoError(t, err)
authData := &model.AuthData{ClientId: oauthApp.Id, RedirectUri: oauthApp.CallbackUrls[0], UserId: th.BasicUser.Id, Code: model.NewId(), ExpiresIn: -1}
_, err = th.App.Srv().Store().OAuth().SaveAuthData(authData)
require.NoError(t, err)
data.Set("grant_type", model.AccessTokenGrantType)
data.Set("client_id", oauthApp.Id)
data.Set("client_secret", oauthApp.ClientSecret)
data.Set("redirect_uri", oauthApp.CallbackUrls[0])
data.Set("code", authData.Code)
data.Del("refresh_token")
_, _, err = apiClient.GetOAuthAccessToken(context.Background(), data)
require.Error(t, err, "Should have failed - code is expired")
apiClient.ClearOAuthToken()
}
func TestMobileLoginWithOAuth(t *testing.T) {
th := Setup(t).InitBasic(t)
c := &Context{
App: th.App,
AppContext: th.Context,
Logger: th.TestLogger,
Params: &Params{
Service: "gitlab",
},
}
siteURL := "http://localhost:8065"
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = siteURL
*cfg.GitLabSettings.Enable = true
})
provider := &MattermostTestProvider{}
einterfaces.RegisterOAuthProvider(model.ServiceGitlab, provider)
t.Run("Should redirect to the SSO login page when valid URL Scheme is passed as redirect_to parameter", func(t *testing.T) {
responseWriter := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("mmauth://"), nil)
require.NoError(t, err)
mobileLoginWithOAuth(c, responseWriter, request)
assert.Equal(t, responseWriter.Code, 302)
assert.NotContains(t, responseWriter.Body.String(), siteURL)
})
t.Run("Should include SiteURL in the output when invalid URL Scheme is passed", func(t *testing.T) {
einterfaces.RegisterOAuthProvider(model.ServiceGitlab, provider)
responseWriter := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("randomScheme://"), nil)
require.NoError(t, err)
mobileLoginWithOAuth(c, responseWriter, request)
body := responseWriter.Body.String()
assert.NotContains(t, body, "randomScheme://")
assert.Contains(t, body, siteURL)
})
t.Run("Should not include the redirect URL consisting of javascript protocol", func(t *testing.T) {
responseWriter := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("javascript:alert('hello')"), nil)
require.NoError(t, err)
mobileLoginWithOAuth(c, responseWriter, request)
body := responseWriter.Body.String()
assert.NotContains(t, body, "javascript:alert('hello')")
assert.Contains(t, body, siteURL)
})
t.Run("Should not include the redirect URL consisting of javascript protocol in mixed case", func(t *testing.T) {
responseWriter := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("JaVasCript:alert('hello')"), nil)
require.NoError(t, err)
mobileLoginWithOAuth(c, responseWriter, request)
body := responseWriter.Body.String()
assert.NotContains(t, body, "JaVasCript:alert('hello')")
assert.Contains(t, body, siteURL)
})
}
func TestOAuthComplete(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
th := Setup(t).InitBasic(t)
th.Login(t, apiClient, th.SystemAdminUser)
gitLabSettingsEnable := th.App.Config().GitLabSettings.Enable
gitLabSettingsAuthEndpoint := th.App.Config().GitLabSettings.AuthEndpoint
gitLabSettingsId := th.App.Config().GitLabSettings.Id
gitLabSettingsSecret := th.App.Config().GitLabSettings.Secret
gitLabSettingsTokenEndpoint := th.App.Config().GitLabSettings.TokenEndpoint
gitLabSettingsUserAPIEndpoint := th.App.Config().GitLabSettings.UserAPIEndpoint
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Enable = gitLabSettingsEnable })
th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.AuthEndpoint = gitLabSettingsAuthEndpoint })
th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Id = gitLabSettingsId })
th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.Secret = gitLabSettingsSecret })
th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.TokenEndpoint = gitLabSettingsTokenEndpoint })
th.App.UpdateConfig(func(cfg *model.Config) { cfg.GitLabSettings.UserAPIEndpoint = gitLabSettingsUserAPIEndpoint })
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
r, err := HTTPGet(apiClient.URL+"/login/gitlab/complete?code=123", apiClient.HTTPClient, "", true)
assert.Error(t, err)
closeBody(r)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = true })
r, err = HTTPGet(apiClient.URL+"/login/gitlab/complete?code=123&state=!#$#F@#Yˆ&~ñ", apiClient.HTTPClient, "", true)
assert.Error(t, err)
closeBody(r)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.AuthEndpoint = apiClient.URL + "/oauth/authorize" })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Id = model.NewId() })
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionLogin
stateProps["team_id"] = th.BasicTeam.Id
stateProps["redirect_to"] = *th.App.Config().GitLabSettings.AuthEndpoint
state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps)))
r, err = HTTPGet(apiClient.URL+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), apiClient.HTTPClient, "", true)
assert.Error(t, err)
closeBody(r)
stateProps["hash"] = utils.HashSha256(*th.App.Config().GitLabSettings.Id)
state = base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps)))
r, err = HTTPGet(apiClient.URL+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), apiClient.HTTPClient, "", true)
assert.Error(t, err)
closeBody(r)
// We are going to use mattermost as the provider emulating gitlab
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
defaultRolePermissions := th.SaveDefaultRolePermissions(t)
defer func() {
th.RestoreDefaultRolePermissions(t, defaultRolePermissions)
}()
th.AddPermissionToRole(t, model.PermissionManageOAuth.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageOAuth.Id, model.SystemUserRoleId)
oauthApp := &model.OAuthApp{
Name: "TestApp5" + model.NewId(),
Homepage: "https://nowhere.com",
Description: "test",
ClientSecret: model.NewId(), // Explicitly set secret to make it confidential
CallbackUrls: []string{
apiClient.URL + "/signup/" + model.ServiceGitlab + "/complete",
apiClient.URL + "/login/" + model.ServiceGitlab + "/complete",
},
CreatorId: th.SystemAdminUser.Id,
IsTrusted: true,
}
oauthApp, appErr := th.App.CreateOAuthApp(oauthApp)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Id = oauthApp.Id })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Secret = oauthApp.ClientSecret })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.AuthEndpoint = apiClient.URL + "/oauth/authorize" })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.TokenEndpoint = apiClient.URL + "/oauth/access_token" })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.UserAPIEndpoint = apiClient.APIURL + "/users/me" })
provider := &MattermostTestProvider{}
authRequest := &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: oauthApp.Id,
RedirectURI: oauthApp.CallbackUrls[0],
Scope: "all",
State: "123",
}
redirect, _, err := apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
rurl, err := url.Parse(redirect)
require.NoError(t, err)
code := rurl.Query().Get("code")
stateProps["action"] = model.OAuthActionEmailToSSO
delete(stateProps, "team_id")
stateProps["redirect_to"] = *th.App.Config().GitLabSettings.AuthEndpoint
stateProps["hash"] = utils.HashSha256(*th.App.Config().GitLabSettings.Id)
stateProps["redirect_to"] = "/oauth/authorize"
state = base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps)))
r, err = HTTPGet(apiClient.URL+"/login/"+model.ServiceGitlab+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), apiClient.HTTPClient, "", false)
if err == nil {
closeBody(r)
}
einterfaces.RegisterOAuthProvider(model.ServiceGitlab, provider)
redirect, _, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
rurl, _ = url.Parse(redirect)
code = rurl.Query().Get("code")
r, err = HTTPGet(apiClient.URL+"/login/"+model.ServiceGitlab+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), apiClient.HTTPClient, "", false)
if err == nil {
closeBody(r)
}
_, nErr := th.App.Srv().Store().User().UpdateAuthData(
th.BasicUser.Id, model.ServiceGitlab, &th.BasicUser.Email, th.BasicUser.Email, true)
require.NoError(t, nErr)
redirect, _, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
rurl, _ = url.Parse(redirect)
code = rurl.Query().Get("code")
stateProps["action"] = model.OAuthActionLogin
state = base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps)))
if r, err = HTTPGet(apiClient.URL+"/login/"+model.ServiceGitlab+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), apiClient.HTTPClient, "", false); err == nil {
closeBody(r)
}
redirect, _, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
rurl, _ = url.Parse(redirect)
code = rurl.Query().Get("code")
delete(stateProps, "action")
state = base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps)))
if r, err = HTTPGet(apiClient.URL+"/login/"+model.ServiceGitlab+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), apiClient.HTTPClient, "", false); err == nil {
closeBody(r)
}
redirect, _, err = apiClient.AuthorizeOAuthApp(context.Background(), authRequest)
require.NoError(t, err)
rurl, _ = url.Parse(redirect)
code = rurl.Query().Get("code")
stateProps["action"] = model.OAuthActionSignup
state = base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps)))
if r, err := HTTPGet(apiClient.URL+"/login/"+model.ServiceGitlab+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), apiClient.HTTPClient, "", false); err == nil {
closeBody(r)
}
}
func TestOAuthComplete_ErrorMessages(t *testing.T) {
th := Setup(t).InitBasic(t)
c := &Context{
App: th.App,
AppContext: th.Context,
Logger: th.TestLogger,
Params: &Params{
Service: "gitlab",
},
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
provider := &MattermostTestProvider{}
einterfaces.RegisterOAuthProvider(model.ServiceGitlab, provider)
responseWriter := httptest.NewRecorder()
// Renders for web & mobile app with webview
request, err := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/signup/gitlab/complete?code=1234", nil)
require.NoError(t, err)
completeOAuth(c, responseWriter, request)
assert.Contains(t, responseWriter.Body.String(), "<!-- web error message -->")
// Renders for mobile app with redirect url
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionMobile
stateProps["redirect_to"] = th.App.Config().NativeAppSettings.AppCustomURLSchemes[0]
state := base64.StdEncoding.EncodeToString([]byte(model.MapToJSON(stateProps)))
request2, err := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/signup/gitlab/complete?code=1234&state="+url.QueryEscape(state), nil)
require.NoError(t, err)
completeOAuth(c, responseWriter, request2)
assert.Contains(t, responseWriter.Body.String(), "<!-- mobile app message -->")
}
func HTTPGet(url string, httpClient *http.Client, authToken string, followRedirect bool) (*http.Response, error) {
rq, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
rq.Close = true
if authToken != "" {
rq.Header.Set(model.HeaderAuth, authToken)
}
if !followRedirect {
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
rp, err := httpClient.Do(rq)
if err != nil {
return nil, err
} else if rp.StatusCode == 304 {
return rp, nil
} else if rp.StatusCode == 307 {
return rp, nil
} else if rp.StatusCode >= 300 {
defer closeBody(rp)
return rp, model.AppErrorFromJSON(rp.Body)
}
return rp, nil
}
func closeBody(r *http.Response) {
if r != nil && r.Body != nil {
_, _ = io.ReadAll(r.Body) // Discard and ignore errors - just draining the body
r.Body.Close()
}
}
type MattermostTestProvider struct {
}
func (m *MattermostTestProvider) GetUserFromJSON(_ request.CTX, data io.Reader, tokenUser *model.User, settings *model.SSOSettings) (*model.User, error) {
var user model.User
if err := json.NewDecoder(data).Decode(&user); err != nil {
return nil, err
}
user.AuthData = &user.Email
return &user, nil
}
func (m *MattermostTestProvider) GetSSOSettings(_ request.CTX, config *model.Config, service string) (*model.SSOSettings, error) {
return &config.GitLabSettings, nil
}
func (m *MattermostTestProvider) GetUserFromIdToken(_ request.CTX, token string) (*model.User, error) {
return nil, nil
}
func (m *MattermostTestProvider) IsSameUser(_ request.CTX, dbUser, oauthUser *model.User) bool {
return dbUser.AuthData == oauthUser.AuthData
}
func GenerateTestAppName() string {
return "fakeoauthapp" + model.NewRandomString(10)
}
func checkHTTPStatus(t *testing.T, resp *model.Response, expectedStatus int) {
t.Helper()
require.NotNilf(t, resp, "Unexpected nil response, expected http status:%v", expectedStatus)
require.Equalf(t, expectedStatus, resp.StatusCode, "Expected http status:%v, got %v", expectedStatus, resp.StatusCode)
}
func CheckForbiddenStatus(t *testing.T, resp *model.Response) {
t.Helper()
checkHTTPStatus(t, resp, http.StatusForbidden)
}
func CheckUnauthorizedStatus(t *testing.T, resp *model.Response) {
t.Helper()
checkHTTPStatus(t, resp, http.StatusUnauthorized)
}
func CheckNotFoundStatus(t *testing.T, resp *model.Response) {
t.Helper()
checkHTTPStatus(t, resp, http.StatusNotFound)
}
func CheckBadRequestStatus(t *testing.T, resp *model.Response) {
t.Helper()
checkHTTPStatus(t, resp, http.StatusBadRequest)
}
func (th *TestHelper) Login(tb testing.TB, client *model.Client4, user *model.User) {
tb.Helper()
session := &model.Session{
UserId: user.Id,
Roles: user.GetRawRoles(),
IsOAuth: false,
}
session, appErr := th.App.CreateSession(th.Context, session)
require.Nil(tb, appErr)
client.AuthToken = session.Token
client.AuthType = model.HeaderBearer
}
func (th *TestHelper) Logout(client *model.Client4) {
client.AuthToken = ""
}
func (th *TestHelper) SaveDefaultRolePermissions(tb testing.TB) map[string][]string {
results := make(map[string][]string)
for _, roleName := range []string{
"system_user",
"system_admin",
"team_user",
"team_admin",
"channel_user",
"channel_admin",
} {
role, appErr := th.App.GetRoleByName(th.Context, roleName)
require.Nil(tb, appErr)
results[roleName] = role.Permissions
}
return results
}
func (th *TestHelper) RestoreDefaultRolePermissions(tb testing.TB, data map[string][]string) {
for roleName, permissions := range data {
role, appErr := th.App.GetRoleByName(th.Context, roleName)
require.Nil(tb, appErr)
if strings.Join(role.Permissions, " ") == strings.Join(permissions, " ") {
continue
}
role.Permissions = permissions
_, appErr = th.App.UpdateRole(role)
require.Nil(tb, appErr)
}
}
// func (th *TestHelper) RemovePermissionFromRole(permission string, roleName string) {
// utils.DisableDebugLogForTest()
// role, err1 := th.App.GetRoleByName(roleName)
// if err1 != nil {
// utils.EnableDebugLogForTest()
// panic(err1)
// }
// var newPermissions []string
// for _, p := range role.Permissions {
// if p != permission {
// newPermissions = append(newPermissions, p)
// }
// }
// if strings.Join(role.Permissions, " ") == strings.Join(newPermissions, " ") {
// utils.EnableDebugLogForTest()
// return
// }
// role.Permissions = newPermissions
// _, err2 := th.App.UpdateRole(role)
// if err2 != nil {
// utils.EnableDebugLogForTest()
// panic(err2)
// }
// utils.EnableDebugLogForTest()
// }
func (th *TestHelper) AddPermissionToRole(tb testing.TB, permission string, roleName string) {
role, appErr := th.App.GetRoleByName(th.Context, roleName)
require.Nil(tb, appErr)
if slices.Contains(role.Permissions, permission) {
return
}
role.Permissions = append(role.Permissions, permission)
_, appErr = th.App.UpdateRole(role)
require.Nil(tb, appErr)
}
// TestOAuthMobileCallbackIncludesSrvParameter verifies that mobile OAuth callbacks
// include the 'srv' parameter for origin verification
func TestOAuthMobileCallbackIncludesSrvParameter(t *testing.T) {
// The 'srv' parameter is added to mobile callbacks to allow the client
// to verify the server origin
t.Run("srv parameter should be included in mobile callback URL construction", func(t *testing.T) {
// Verify the pattern: when we construct a redirect URL for mobile OAuth,
// it should include "srv" parameter with the server's site URL
siteURL := "https://mattermost.example.com"
sessionToken := "test-session-token"
csrfToken := "test-csrf-token"
// Simulate what the code does when constructing the callback
params := map[string]string{
model.SessionCookieToken: sessionToken,
model.SessionCookieCsrf: csrfToken,
"srv": siteURL,
}
// Verify all expected parameters are present
assert.Equal(t, sessionToken, params[model.SessionCookieToken])
assert.Equal(t, csrfToken, params[model.SessionCookieCsrf])
assert.Equal(t, siteURL, params["srv"])
})
t.Run("srv parameter detects OAuth server mismatch", func(t *testing.T) {
// Scenario: The srv parameter from callback doesn't match expected server
// Mobile should detect the mismatch
expectedServer := "https://server-a.example.com"
actualSrvFromCallback := "https://server-b.example.com"
// This is the check that should happen in mobile
isMismatch := expectedServer != actualSrvFromCallback
assert.True(t, isMismatch, "Should detect server mismatch")
})
t.Run("srv parameter allows legitimate OAuth login", func(t *testing.T) {
// Scenario: Normal OAuth login to legitimate.com
// Server adds srv=legitimate.com to callback
// Mobile verifies: expected (legitimate.com) == srv (legitimate.com)
expectedServer := "https://legitimate.example.com"
actualSrvFromCallback := "https://legitimate.example.com"
// This is the check that should happen in mobile
isLegitimate := expectedServer == actualSrvFromCallback
assert.True(t, isLegitimate, "Should allow legitimate OAuth login")
})
}
func TestFullyQualifiedRedirectURL(t *testing.T) {
const siteURL = "https://xxx.yyy/mm"
for target, expected := range map[string]string{
"": siteURL,
"/": siteURL + "/",
"some-path": siteURL + "/some-path",
"/some-path": siteURL + "/some-path",
"/some-path/": siteURL + "/some-path/",
"/some-path?foo=bar": siteURL + "/some-path?foo=bar",
"/some-path#section": siteURL + "/some-path#section",
"../bad-path": siteURL,
"/index.html": siteURL + "/index.html",
"//evil.com": siteURL,
"https://xxx.yyy/mm": siteURL,
"https://xxx.yyy/mm//double-concat": siteURL + "//double-concat",
"https://xxx.yyy/other-path/": siteURL,
"https://xxx.yyy/mm/some-path": siteURL + "/some-path",
"https://yyy.zzz/mm/some-path": siteURL,
"https://xxx.yyy/mm/some-path?foo=bar": siteURL + "/some-path?foo=bar",
"https://xxx.yyy/mm/some-path#section": siteURL + "/some-path#section",
"https://xxx.yyy/mm/../malicious-path": siteURL,
":foo": siteURL,
"mmauth://callback": "mmauth://callback",
"mmauth://xxx.yyy/mm": siteURL, // invalid mobile URL (wrong host)
} {
t.Run(target, func(t *testing.T) {
require.Equal(t, expected, fullyQualifiedRedirectURL(siteURL, target, []string{"mmauth://"}))
})
}
}
func TestAuthorizeOAuthPage(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableOAuthServiceProvider = true
})
// Helper function to create context with session
createContext := func() *Context {
c := &Context{
App: th.App,
AppContext: th.Context,
Logger: th.TestLogger,
}
session := &model.Session{
UserId: th.BasicUser.Id,
Token: model.NewId(),
}
c.AppContext = c.AppContext.WithSession(session)
return c
}
t.Run("PublicClient_PKCERequired", 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.BasicUser.Id)
require.Nil(t, appErr)
c := createContext()
responseWriter := httptest.NewRecorder()
requestURL := "/oauth/authorize?response_type=code&client_id=" + publicApp.Id + "&redirect_uri=" + url.QueryEscape(publicApp.CallbackUrls[0]) + "&state=test_state"
request := httptest.NewRequest(http.MethodGet, requestURL, nil)
authorizeOAuthPage(c, responseWriter, request)
assert.Contains(t, responseWriter.Body.String(), "PKCE+is+required+for+public+clients")
})
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.BasicUser.Id)
require.Nil(t, appErr)
publicApp.IsTrusted = true
existingApp, getErr := th.App.GetOAuthApp(publicApp.Id)
require.Nil(t, getErr)
_, updateErr := th.App.UpdateOAuthApp(existingApp, publicApp)
require.Nil(t, updateErr)
c := createContext()
responseWriter := httptest.NewRecorder()
codeChallenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
codeChallengeMethod := model.PKCECodeChallengeMethodS256
request := httptest.NewRequest(http.MethodGet,
"/oauth/authorize?response_type=code&client_id="+publicApp.Id+"&redirect_uri="+url.QueryEscape(publicApp.CallbackUrls[0])+
"&state=test_state&code_challenge="+codeChallenge+"&code_challenge_method="+codeChallengeMethod, nil)
authorizeOAuthPage(c, responseWriter, request)
assert.Equal(t, http.StatusFound, responseWriter.Code)
location := responseWriter.Header().Get("Location")
assert.Contains(t, location, "https://example.com/callback")
assert.Contains(t, location, "code=")
assert.Contains(t, location, "state=test_state")
})
t.Run("ConfidentialClient_PKCEOptional", func(t *testing.T) {
confidentialApp := &model.OAuthApp{
Name: "Confidential Client Test",
CreatorId: th.BasicUser.Id,
Homepage: "https://example.com",
Description: "test confidential client",
CallbackUrls: []string{"https://example.com/callback"},
ClientSecret: "test-secret",
}
confidentialApp, appErr := th.App.CreateOAuthApp(confidentialApp)
require.Nil(t, appErr)
confidentialApp.IsTrusted = true
existingApp, getErr := th.App.GetOAuthApp(confidentialApp.Id)
require.Nil(t, getErr)
_, updateErr := th.App.UpdateOAuthApp(existingApp, confidentialApp)
require.Nil(t, updateErr)
c := createContext()
responseWriter := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet,
"/oauth/authorize?response_type=code&client_id="+confidentialApp.Id+"&redirect_uri="+url.QueryEscape(confidentialApp.CallbackUrls[0])+"&state=test_state", nil)
authorizeOAuthPage(c, responseWriter, request)
assert.Equal(t, http.StatusFound, responseWriter.Code)
location := responseWriter.Header().Get("Location")
assert.Contains(t, location, "https://example.com/callback")
assert.Contains(t, location, "code=")
assert.Contains(t, location, "state=test_state")
})
}
func TestAuthorizeOAuthAppHandler(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableOAuthServiceProvider = true
})
// Helper function to create context with session
createContext := func() *Context {
c := &Context{
App: th.App,
AppContext: th.Context,
Logger: th.TestLogger,
}
session := &model.Session{
UserId: th.BasicUser.Id,
Token: model.NewId(),
}
c.AppContext = c.AppContext.WithSession(session)
return c
}
t.Run("PublicClient_PKCEParameters", 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.BasicUser.Id)
require.Nil(t, appErr)
c := createContext()
authRequest := &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: publicApp.Id,
RedirectURI: publicApp.CallbackUrls[0],
State: "test_state",
Scope: "user",
CodeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
CodeChallengeMethod: model.PKCECodeChallengeMethodS256,
}
requestBodyBytes, err := json.Marshal(authRequest)
require.NoError(t, err)
requestBody := strings.NewReader(string(requestBodyBytes))
responseWriter := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/oauth/authorize", requestBody)
request.Header.Set("Content-Type", "application/json")
authorizeOAuthApp(c, responseWriter, request)
assert.Equal(t, http.StatusOK, responseWriter.Code)
var response map[string]string
err = json.Unmarshal(responseWriter.Body.Bytes(), &response)
require.NoError(t, err)
redirectURL := response["redirect"]
assert.NotEmpty(t, redirectURL)
assert.Contains(t, redirectURL, "code=")
})
}
func TestGetAccessToken(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableOAuthServiceProvider = true
})
// Helper function to create context
createContext := func() *Context {
return &Context{
App: th.App,
AppContext: th.Context,
Logger: th.TestLogger,
}
}
t.Run("PublicClient_NoClientSecret", 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.BasicUser.Id)
require.Nil(t, appErr)
codeVerifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
codeChallenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
authRequest := &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: publicApp.Id,
RedirectURI: publicApp.CallbackUrls[0],
State: "test_state",
Scope: "user",
CodeChallenge: codeChallenge,
CodeChallengeMethod: model.PKCECodeChallengeMethodS256,
}
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)
c := createContext()
formData := url.Values{}
formData.Set("grant_type", model.AccessTokenGrantType)
formData.Set("code", code)
formData.Set("redirect_uri", publicApp.CallbackUrls[0])
formData.Set("client_id", publicApp.Id)
formData.Set("code_verifier", codeVerifier)
responseWriter := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/oauth/access_token", strings.NewReader(formData.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
getAccessToken(c, responseWriter, request)
assert.Equal(t, http.StatusOK, responseWriter.Code)
var tokenResponse model.AccessResponse
err = json.Unmarshal(responseWriter.Body.Bytes(), &tokenResponse)
require.NoError(t, err)
assert.NotEmpty(t, tokenResponse.AccessToken)
assert.Equal(t, model.AccessTokenType, tokenResponse.TokenType)
assert.Empty(t, tokenResponse.RefreshToken)
})
}