Files
mattermost/app/oauth_test.go
Doug Lauder 7f64199a37 MM-27184 deprecate model.SetExpireInDays (#15165)
Mobile users were having their sessions unexpectedly expired, despite having ServiceSettings.ExtendSessionLengthWithActivity enabled. 

Every time a mobile app is opened it called `/api/v4/sessions/device` which calls attachDeviceId which calls `(*Session)SetExpireInDays`. This code above assumed the expiry should be relative to CreateAt which is incorrect when ExtendSessionLengthWithActivity is enabled. Therefore, every time the mobile app was opened, the maximum expiry was set in memory to CreateAt + session_length, even if the session was extended.

(*Session)SetExpireInDays is now deprecated and replaced with (*App)SetSessionExpireInDays which takes into account the ExtendSessionLengthWithActivity setting.
2020-08-04 16:10:37 -04:00

580 lines
18 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetOAuthAccessTokenForImplicitFlow(t *testing.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, err := th.App.CreateOAuthApp(oapp)
require.Nil(t, err)
authRequest := &model.AuthorizeRequest{
ResponseType: model.IMPLICIT_RESPONSE_TYPE,
ClientId: oapp.Id,
RedirectUri: oapp.CallbackUrls[0],
Scope: "",
State: "123",
}
session, err := th.App.GetOAuthAccessTokenForImplicitFlow(th.BasicUser.Id, authRequest)
assert.Nil(t, err)
assert.NotNil(t, session)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
session, err = th.App.GetOAuthAccessTokenForImplicitFlow(th.BasicUser.Id, authRequest)
assert.NotNil(t, err, "should fail - oauth2 disabled")
assert.Nil(t, session)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
authRequest.ClientId = "junk"
session, err = th.App.GetOAuthAccessTokenForImplicitFlow(th.BasicUser.Id, authRequest)
assert.NotNil(t, err, "should fail - bad client id")
assert.Nil(t, session)
authRequest.ClientId = oapp.Id
session, err = th.App.GetOAuthAccessTokenForImplicitFlow("junk", authRequest)
assert.NotNil(t, err, "should fail - bad user id")
assert.Nil(t, session)
}
func TestOAuthRevokeAccessToken(t *testing.T) {
th := Setup(t)
defer th.TearDown()
err := th.App.RevokeAccessToken(model.NewRandomString(16))
require.NotNil(t, err, "Should have failed bad token")
session := &model.Session{}
session.CreateAt = model.GetMillis()
session.UserId = model.NewId()
session.Token = model.NewId()
session.Roles = model.SYSTEM_USER_ROLE_ID
th.App.SetSessionExpireInDays(session, 1)
session, _ = th.App.CreateSession(session)
err = th.App.RevokeAccessToken(session.Token)
require.NotNil(t, err, "Should have failed does not have an access token")
accessData := &model.AccessData{}
accessData.Token = session.Token
accessData.UserId = session.UserId
accessData.RedirectUri = "http://example.com"
accessData.ClientId = model.NewId()
accessData.ExpiresAt = session.ExpiresAt
_, nErr := th.App.Srv().Store.OAuth().SaveAccessData(accessData)
require.Nil(t, nErr)
err = th.App.RevokeAccessToken(accessData.Token)
require.Nil(t, err)
}
func TestOAuthDeleteApp(t *testing.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"
var err *model.AppError
a1, err = th.App.CreateOAuthApp(a1)
require.Nil(t, err)
session := &model.Session{}
session.CreateAt = model.GetMillis()
session.UserId = model.NewId()
session.Token = model.NewId()
session.Roles = model.SYSTEM_USER_ROLE_ID
session.IsOAuth = true
th.App.SetSessionExpireInDays(session, 1)
session, _ = th.App.CreateSession(session)
accessData := &model.AccessData{}
accessData.Token = session.Token
accessData.UserId = session.UserId
accessData.RedirectUri = "http://example.com"
accessData.ClientId = a1.Id
accessData.ExpiresAt = session.ExpiresAt
_, nErr := th.App.Srv().Store.OAuth().SaveAccessData(accessData)
require.Nil(t, nErr)
err = th.App.DeleteOAuthApp(a1.Id)
require.Nil(t, err)
_, err = th.App.GetSession(session.Token)
require.NotNil(t, err, "should not get session from cache or db")
}
func TestAuthorizeOAuthUser(t *testing.T) {
setup := func(t *testing.T, enable, tokenEndpoint, userEndpoint bool, serverURL string) *TestHelper {
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, _ := th.App.CreateOAuthStateToken(generateOAuthStateTokenExtra("", "", cookie))
return token
}
makeRequest := func(t *testing.T, cookie string) *http.Request {
request, _ := http.NewRequest(http.MethodGet, "https://mattermost.example.com", nil)
if cookie != "" {
request.AddCookie(&http.Cookie{
Name: COOKIE_OAUTH,
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(nil, nil, model.SERVICE_GITLAB, "", "", "")
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(nil, nil, model.SERVICE_GITLAB, "", 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(nil, nil, model.SERVICE_GITLAB, "", state, "")
require.NotNil(t, err)
assert.Equal(t, "api.oauth.invalid_state_token.app_error", err.Id)
assert.NotEqual(t, "", err.DetailedError)
})
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.Nil(t, th.App.Srv().Store.Token().Save(token))
state := makeState(token)
_, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", 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.OAUTH_ACTION_EMAIL_TO_SSO
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(nil, nil, model.SERVICE_GITLAB, "", 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(t, "")
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(nil, request, model.SERVICE_GITLAB, "", 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(t, cookie)
state := makeState(token)
_, _, _, err = th.App.AuthorizeOAuthUser(nil, request, model.SERVICE_GITLAB, "", 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(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", 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(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", 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) {
w.Write([]byte("invalid"))
}))
defer server.Close()
th := setup(t, true, true, true, server.URL)
defer th.TearDown()
cookie := model.NewId()
request := makeRequest(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", 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) {
json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: model.NewId(),
TokenType: "",
})
}))
defer server.Close()
th := setup(t, true, true, true, server.URL)
defer th.TearDown()
cookie := model.NewId()
request := makeRequest(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", 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) {
json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: "",
TokenType: model.ACCESS_TOKEN_TYPE,
})
}))
defer server.Close()
th := setup(t, true, true, true, server.URL)
defer th.TearDown()
cookie := model.NewId()
request := makeRequest(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", 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) {
json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: model.NewId(),
TokenType: model.ACCESS_TOKEN_TYPE,
})
}))
defer server.Close()
th := setup(t, true, true, false, server.URL)
defer th.TearDown()
cookie := model.NewId()
request := makeRequest(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", 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")
json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: model.NewId(),
TokenType: model.ACCESS_TOKEN_TYPE,
})
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(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", 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")
json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: model.NewId(),
TokenType: model.ACCESS_TOKEN_TYPE,
})
case "/user":
t.Log("hit user")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Terms of Service"))
}
}))
defer server.Close()
th := setup(t, true, true, true, server.URL)
defer th.TearDown()
cookie := model.NewId()
request := makeRequest(t, cookie)
state := makeState(makeToken(th, cookie))
_, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "")
require.NotNil(t, err)
assert.Equal(t, "oauth.gitlab.tos.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":
json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: model.NewId(),
TokenType: model.ACCESS_TOKEN_TYPE,
})
case "/user":
w.WriteHeader(http.StatusOK)
w.Write([]byte(userData))
}
}))
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(t, 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, receivedTeamId, receivedStateProps, err := th.App.AuthorizeOAuthUser(&recorder, request, model.SERVICE_GITLAB, "", state, "")
require.NotNil(t, body)
bodyBytes, bodyErr := ioutil.ReadAll(body)
require.Nil(t, bodyErr)
assert.Equal(t, userData, string(bodyBytes))
assert.Equal(t, stateProps["team_id"], receivedTeamId)
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) {
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(nil, nil, model.SERVICE_GITLAB, map[string]string{}, "")
require.NotNil(t, err)
assert.Equal(t, "api.user.get_authorization_code.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(&recorder, request, model.SERVICE_GITLAB, stateProps, "")
require.Nil(t, err)
assert.NotEmpty(t, url)
cookies := recorder.Header().Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
})
}
})
}