Files
grafana/pkg/services/oauthserver/oasimpl/token_test.go
Gabriel MABILLE edf1775d49 AuthN: Embed an OAuth2 server for external service authentication (#68086)
* Moving POC files from #64283 to a new branch

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Adding missing permission definition

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Force the service instantiation while client isn't merged

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Merge conf with main

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Leave go-sqlite3 version unchanged

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* tidy

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* User SearchUserPermissions instead of SearchUsersPermissions

* Replace DummyKeyService with signingkeys.Service

* Use user🆔<id> as subject

* Fix introspection endpoint issue

* Add X-Grafana-Org-Id to get_resources.bash script

* Regenerate toggles_gen.go
* Fix basic.go

* Add GetExternalService tests

* Add GetPublicKeyScopes tests

* Add GetScopesOnUser tests

* Add GetScopes tests

* Add ParsePublicKeyPem tests

* Add database test for GetByName

* re-add comments

* client tests added

* Add GetExternalServicePublicKey tests

* Add other test case to GetExternalServicePublicKey

* client_credentials grant test

* Add test to jwtbearer grant

* Test Comments

* Add handleKeyOptions tests

* Add RSA key generation test

* Add ECDSA by default to EmbeddedSigningKeysService

* Clean up org id scope and audiences

* Add audiences to the DB

* Fix check on Audience

* Fix double import

* Add AC Store mock and align oauthserver tests

* Fix test after rebase

* Adding missing store function to mock

* Fix double import

* Add CODEOWNER

* Fix some linting errors

* errors don't need type assertion

* Typo codeowners

* use mockery for oauthserver store

* Add feature toggle check

* Fix db tests to handle the feature flag

* Adding call to DeleteExternalServiceRole

* Fix flaky test

* Re-organize routes comments and plan futur work

* Add client_id check to Extended JWT client

* Clean up

* Fix

* Remove background service registry instantiation of the OAuth server

* Comment cleanup

* Remove unused client function

* Update go.mod to use the latest ory/fosite commit

* Remove oauth2_server related configs from defaults.ini

* Add audiences to DTO

* Fix flaky test

* Remove registration endpoint and demo scripts. Document code

* Rename packages

* Remove the OAuthService vs OAuthServer confusion

* fix incorrect import ext_jwt_test

* Comments and order

* Comment basic auth

* Remove unecessary todo

* Clean api

* Moving ParsePublicKeyPem to utils

* re ordering functions in service.go

* Fix comment

* comment on the redirect uri

* Add RBAC actions, not only scopes

* Fix tests

* re-import featuremgmt in migrations

* Fix wire

* Fix scopes in test

* Fix flaky test

* Remove todo, the intersection should always return the minimal set

* Remove unecessary check from intersection code

* Allow env overrides on settings

* remove the term app name

* Remove app keyword for client instead and use Name instead of ExternalServiceName

* LogID remove ExternalService ref

* Use Name instead of ExternalServiceName

* Imports order

* Inline

* Using ExternalService and ExternalServiceDTO

* Remove xorm tags

* comment

* Rename client files

* client -> external service

* comments

* Move test to correct package

* slimmer test

* cachedUser -> cachedExternalService

* Fix aggregate store test

* PluginAuthSession -> AuthSession

* Revert the nil cehcks

* Remove unecessary extra

* Removing custom session

* fix typo in test

* Use constants for tests

* Simplify HandleToken tests

* Refactor the HandleTokenRequest test

* test message

* Review test

* Prevent flacky test on client as well

* go imports

* Revert changes from 526e48ad45

* AuthN: Change the External Service registration form (#68649)

* AuthN: change the External Service registration form

* Gen default permissions

* Change demo script registration form

* Remove unecessary comment

* Nit.

* Reduce cyclomatic complexity

* Remove demo_scripts

* Handle case with no service account

* Comments

* Group key gen

* Nit.

* Check the SaveExternalService test

* Rename cachedUser to cachedClient in test

* One more test case to database test

* Comments

* Remove last org scope

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Update pkg/services/oauthserver/utils/utils_test.go

* Update pkg/services/sqlstore/migrations/oauthserver/migrations.go

Remove comment

* Update pkg/setting/setting.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

---------

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>
2023-05-25 15:38:30 +02:00

748 lines
24 KiB
Go

package oasimpl
import (
"context"
"crypto/rsa"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/ory/fosite"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
"golang.org/x/exp/maps"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
"github.com/grafana/grafana/pkg/models/roletype"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/oauthserver"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
)
func TestOAuth2ServiceImpl_handleClientCredentials(t *testing.T) {
client1 := &oauthserver.ExternalService{
Name: "testapp",
ClientID: "RANDOMID",
GrantTypes: string(fosite.GrantTypeClientCredentials),
ServiceAccountID: 2,
SignedInUser: &user.SignedInUser{
UserID: 2,
Name: "Test App",
Login: "testapp",
OrgRole: roletype.RoleViewer,
Permissions: map[int64]map[string][]string{
oauthserver.TmpOrgID: {
"dashboards:read": {"dashboards:*", "folders:*"},
"dashboards:write": {"dashboards:uid:1"},
},
},
},
}
tests := []struct {
name string
scopes []string
client *oauthserver.ExternalService
expectedClaims map[string]interface{}
wantErr bool
}{
{
name: "no claim without client_credentials grant type",
client: &oauthserver.ExternalService{
Name: "testapp",
ClientID: "RANDOMID",
GrantTypes: string(fosite.GrantTypeJWTBearer),
ServiceAccountID: 2,
SignedInUser: &user.SignedInUser{},
},
},
{
name: "no claims without scopes",
client: client1,
},
{
name: "profile claims",
client: client1,
scopes: []string{"profile"},
expectedClaims: map[string]interface{}{"name": "Test App", "login": "testapp"},
},
{
name: "email claims should be empty",
client: client1,
scopes: []string{"email"},
},
{
name: "groups claims should be empty",
client: client1,
scopes: []string{"groups"},
},
{
name: "entitlements claims",
client: client1,
scopes: []string{"entitlements"},
expectedClaims: map[string]interface{}{"entitlements": map[string][]string{
"dashboards:read": {"dashboards:*", "folders:*"},
"dashboards:write": {"dashboards:uid:1"},
}},
},
{
name: "scoped entitlements claims",
client: client1,
scopes: []string{"entitlements", "dashboards:write"},
expectedClaims: map[string]interface{}{"entitlements": map[string][]string{
"dashboards:write": {"dashboards:uid:1"},
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
env := setupTestEnv(t)
session := &fosite.DefaultSession{}
requester := fosite.NewAccessRequest(session)
requester.GrantTypes = fosite.Arguments(strings.Split(tt.client.GrantTypes, ","))
requester.RequestedScope = fosite.Arguments(tt.scopes)
sessionData := NewAuthSession()
err := env.S.handleClientCredentials(ctx, requester, sessionData, tt.client)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
if tt.expectedClaims == nil {
require.Empty(t, sessionData.JWTClaims.Extra)
return
}
require.Len(t, sessionData.JWTClaims.Extra, len(tt.expectedClaims))
for claimsKey, claimsValue := range tt.expectedClaims {
switch expected := claimsValue.(type) {
case []string:
require.ElementsMatch(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
case map[string][]string:
actual, ok := sessionData.JWTClaims.Extra[claimsKey].(map[string][]string)
require.True(t, ok, "expected map[string][]string")
require.ElementsMatch(t, maps.Keys(expected), maps.Keys(actual))
for expKey, expValue := range expected {
require.ElementsMatch(t, expValue, actual[expKey])
}
default:
require.Equal(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
}
}
})
}
}
func TestOAuth2ServiceImpl_handleJWTBearer(t *testing.T) {
now := time.Now()
client1 := &oauthserver.ExternalService{
Name: "testapp",
ClientID: "RANDOMID",
GrantTypes: string(fosite.GrantTypeJWTBearer),
ServiceAccountID: 2,
SignedInUser: &user.SignedInUser{
UserID: 2,
OrgID: oauthserver.TmpOrgID,
Name: "Test App",
Login: "testapp",
OrgRole: roletype.RoleViewer,
Permissions: map[int64]map[string][]string{
oauthserver.TmpOrgID: {
"users:impersonate": {"users:*"},
},
},
},
}
user56 := &user.User{
ID: 56,
Email: "user56@example.org",
Login: "user56",
Name: "User 56",
Updated: now,
}
teams := []*team.TeamDTO{
{ID: 1, Name: "Team 1", OrgID: 1},
{ID: 2, Name: "Team 2", OrgID: 1},
}
client1WithPerm := func(perms []ac.Permission) *oauthserver.ExternalService {
client := *client1
client.ImpersonatePermissions = perms
return &client
}
tests := []struct {
name string
initEnv func(*TestEnv)
scopes []string
client *oauthserver.ExternalService
subject string
expectedClaims map[string]interface{}
wantErr bool
}{
{
name: "no claim without jwtbearer grant type",
client: &oauthserver.ExternalService{
Name: "testapp",
ClientID: "RANDOMID",
GrantTypes: string(fosite.GrantTypeClientCredentials),
ServiceAccountID: 2,
},
},
{
name: "err invalid subject",
client: client1,
subject: "invalid_subject",
wantErr: true,
},
{
name: "err client is not allowed to impersonate",
client: &oauthserver.ExternalService{
Name: "testapp",
ClientID: "RANDOMID",
GrantTypes: string(fosite.GrantTypeJWTBearer),
ServiceAccountID: 2,
SignedInUser: &user.SignedInUser{
UserID: 2,
Name: "Test App",
Login: "testapp",
OrgRole: roletype.RoleViewer,
Permissions: map[int64]map[string][]string{oauthserver.TmpOrgID: {}},
},
},
subject: "user:id:56",
wantErr: true,
},
{
name: "err subject not found",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedError = user.ErrUserNotFound
},
client: client1,
subject: "user:id:56",
wantErr: true,
},
{
name: "no claim without scope",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
},
client: client1,
subject: "user:id:56",
},
{
name: "profile claims",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
},
client: client1,
subject: "user:id:56",
scopes: []string{"profile"},
expectedClaims: map[string]interface{}{
"name": "User 56",
"login": "user56",
"updated_at": now.Unix(),
},
},
{
name: "email claim",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
},
client: client1,
subject: "user:id:56",
scopes: []string{"email"},
expectedClaims: map[string]interface{}{
"email": "user56@example.org",
},
},
{
name: "groups claim",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
env.TeamService.ExpectedTeamsByUser = teams
},
client: client1,
subject: "user:id:56",
scopes: []string{"groups"},
expectedClaims: map[string]interface{}{
"groups": []string{"Team 1", "Team 2"},
},
},
{
name: "no entitlement without permission intersection",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
56: {"Viewer"}}, nil)
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
56: {{Action: "dashboards:read", Scope: "dashboards:uid:1"}},
}, nil)
},
client: client1WithPerm([]ac.Permission{
{Action: "datasources:read", Scope: "datasources:*"},
}),
subject: "user:id:56",
expectedClaims: map[string]interface{}{
"entitlements": map[string][]string{},
},
scopes: []string{"entitlements"},
},
{
name: "entitlements contains only the intersection of permissions",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
56: {"Viewer"}}, nil)
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
56: {
{Action: "dashboards:read", Scope: "dashboards:uid:1"},
{Action: "datasources:read", Scope: "datasources:uid:1"},
},
}, nil)
},
client: client1WithPerm([]ac.Permission{
{Action: "datasources:read", Scope: "datasources:*"},
}),
subject: "user:id:56",
expectedClaims: map[string]interface{}{
"entitlements": map[string][]string{
"datasources:read": {"datasources:uid:1"},
},
},
scopes: []string{"entitlements"},
},
{
name: "entitlements have correctly translated users:self permissions",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
56: {"Viewer"}}, nil)
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
56: {
{Action: "users:read", Scope: "global.users:id:*"},
{Action: "users.permissions:read", Scope: "users:id:*"},
}}, nil)
},
client: client1WithPerm([]ac.Permission{
{Action: "users:read", Scope: "global.users:self"},
{Action: "users.permissions:read", Scope: "users:self"},
}),
subject: "user:id:56",
expectedClaims: map[string]interface{}{
"entitlements": map[string][]string{
"users:read": {"global.users:id:56"},
"users.permissions:read": {"users:id:56"},
},
},
scopes: []string{"entitlements"},
},
{
name: "entitlements have correctly translated teams:self permissions",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
env.TeamService.ExpectedTeamsByUser = teams
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
56: {"Viewer"}}, nil)
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
56: {{Action: "teams:read", Scope: "teams:*"}}}, nil)
},
client: client1WithPerm([]ac.Permission{
{Action: "teams:read", Scope: "teams:self"},
}),
subject: "user:id:56",
expectedClaims: map[string]interface{}{
"entitlements": map[string][]string{"teams:read": {"teams:id:1", "teams:id:2"}},
},
scopes: []string{"entitlements"},
},
{
name: "entitlements are correctly filtered based on scopes",
initEnv: func(env *TestEnv) {
env.UserService.ExpectedUser = user56
env.TeamService.ExpectedTeamsByUser = teams
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
56: {"Viewer"}}, nil)
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
56: {
{Action: "users:read", Scope: "global.users:id:*"},
{Action: "datasources:read", Scope: "datasources:uid:1"},
}}, nil)
},
client: client1WithPerm([]ac.Permission{
{Action: "users:read", Scope: "global.users:*"},
{Action: "datasources:read", Scope: "datasources:*"},
}),
subject: "user:id:56",
expectedClaims: map[string]interface{}{
"entitlements": map[string][]string{"users:read": {"global.users:id:*"}},
},
scopes: []string{"entitlements", "users:read"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
env := setupTestEnv(t)
session := &fosite.DefaultSession{}
requester := fosite.NewAccessRequest(session)
requester.GrantTypes = fosite.Arguments(strings.Split(tt.client.GrantTypes, ","))
requester.RequestedScope = fosite.Arguments(tt.scopes)
requester.GrantedScope = fosite.Arguments(tt.scopes)
sessionData := NewAuthSession()
sessionData.Subject = tt.subject
if tt.initEnv != nil {
tt.initEnv(env)
}
err := env.S.handleJWTBearer(ctx, requester, sessionData, tt.client)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
if tt.expectedClaims == nil {
require.Empty(t, sessionData.JWTClaims.Extra)
return
}
require.Len(t, sessionData.JWTClaims.Extra, len(tt.expectedClaims))
for claimsKey, claimsValue := range tt.expectedClaims {
switch expected := claimsValue.(type) {
case []string:
require.ElementsMatch(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
case map[string][]string:
actual, ok := sessionData.JWTClaims.Extra[claimsKey].(map[string][]string)
require.True(t, ok, "expected map[string][]string")
require.ElementsMatch(t, maps.Keys(expected), maps.Keys(actual))
for expKey, expValue := range expected {
require.ElementsMatch(t, expValue, actual[expKey])
}
default:
require.Equal(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
}
}
env.AcStore.AssertExpectations(t)
})
}
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
type claims struct {
jwt.Claims
ClientID string `json:"client_id"`
Groups []string `json:"groups"`
Email string `json:"email"`
Name string `json:"name"`
Login string `json:"login"`
Scopes []string `json:"scope"`
Entitlements map[string][]string `json:"entitlements"`
}
func TestOAuth2ServiceImpl_HandleTokenRequest(t *testing.T) {
tests := []struct {
name string
tweakTestClient func(*oauthserver.ExternalService)
reqParams url.Values
wantCode int
wantScope []string
wantClaims *claims
}{
{
name: "should allow client credentials grant",
reqParams: url.Values{
"grant_type": {string(fosite.GrantTypeClientCredentials)},
"client_id": {"CLIENT1ID"},
"client_secret": {"CLIENT1SECRET"},
"scope": {"profile email groups entitlements"},
"audience": {AppURL},
},
wantCode: http.StatusOK,
wantScope: []string{"profile", "email", "groups", "entitlements"},
wantClaims: &claims{
Claims: jwt.Claims{
Subject: "user:id:2", // From client1.ServiceAccountID
Issuer: AppURL, // From env.S.Config.Issuer
Audience: jwt.Audience{AppURL},
},
ClientID: "CLIENT1ID",
Name: "client-1",
Login: "client-1",
Entitlements: map[string][]string{
"users:impersonate": {"users:*"},
},
},
},
{
name: "should allow jwt-bearer grant",
reqParams: url.Values{
"grant_type": {string(fosite.GrantTypeJWTBearer)},
"client_id": {"CLIENT1ID"},
"client_secret": {"CLIENT1SECRET"},
"assertion": {
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL),
},
"scope": {"profile email groups entitlements"},
},
wantCode: http.StatusOK,
wantScope: []string{"profile", "email", "groups", "entitlements"},
wantClaims: &claims{
Claims: jwt.Claims{
Subject: "user:id:56", // To match the assertion
Issuer: AppURL, // From env.S.Config.Issuer
Audience: jwt.Audience{TokenURL, AppURL},
},
ClientID: "CLIENT1ID",
Email: "user56@example.org",
Name: "User 56",
Login: "user56",
Groups: []string{"Team 1", "Team 2"},
Entitlements: map[string][]string{
"dashboards:read": {"folders:uid:UID1"},
"folders:read": {"folders:uid:UID1"},
"users:read": {"global.users:id:56"},
},
},
},
{
name: "should deny jwt-bearer grant with wrong audience",
reqParams: url.Values{
"grant_type": {string(fosite.GrantTypeJWTBearer)},
"client_id": {"CLIENT1ID"},
"client_secret": {"CLIENT1SECRET"},
"assertion": {
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, "invalid audience"),
},
"scope": {"profile email groups entitlements"},
},
wantCode: http.StatusForbidden,
},
{
name: "should deny jwt-bearer grant for clients without the grant",
reqParams: url.Values{
"grant_type": {string(fosite.GrantTypeJWTBearer)},
"client_id": {"CLIENT1ID"},
"client_secret": {"CLIENT1SECRET"},
"assertion": {
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL),
},
"scope": {"profile email groups entitlements"},
},
tweakTestClient: func(es *oauthserver.ExternalService) {
es.GrantTypes = string(fosite.GrantTypeClientCredentials)
},
wantCode: http.StatusBadRequest,
},
{
name: "should deny client_credentials grant for clients without the grant",
reqParams: url.Values{
"grant_type": {string(fosite.GrantTypeClientCredentials)},
"client_id": {"CLIENT1ID"},
"client_secret": {"CLIENT1SECRET"},
"scope": {"profile email groups entitlements"},
"audience": {AppURL},
},
tweakTestClient: func(es *oauthserver.ExternalService) {
es.GrantTypes = string(fosite.GrantTypeJWTBearer)
},
wantCode: http.StatusBadRequest,
},
{
name: "should deny client_credentials grant with wrong secret",
reqParams: url.Values{
"grant_type": {string(fosite.GrantTypeClientCredentials)},
"client_id": {"CLIENT1ID"},
"client_secret": {"WRONG_SECRET"},
"scope": {"profile email groups entitlements"},
"audience": {AppURL},
},
tweakTestClient: func(es *oauthserver.ExternalService) {
es.GrantTypes = string(fosite.GrantTypeClientCredentials)
},
wantCode: http.StatusUnauthorized,
},
{
name: "should deny jwt-bearer grant with wrong secret",
reqParams: url.Values{
"grant_type": {string(fosite.GrantTypeJWTBearer)},
"client_id": {"CLIENT1ID"},
"client_secret": {"WRONG_SECRET"},
"assertion": {
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL),
},
"scope": {"profile email groups entitlements"},
},
tweakTestClient: func(es *oauthserver.ExternalService) {
es.GrantTypes = string(fosite.GrantTypeJWTBearer)
},
wantCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := setupTestEnv(t)
setupHandleTokenRequestEnv(t, env, tt.tweakTestClient)
req := httptest.NewRequest("POST", "/oauth2/token", strings.NewReader(tt.reqParams.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
env.S.HandleTokenRequest(resp, req)
require.Equal(t, tt.wantCode, resp.Code)
if tt.wantCode != http.StatusOK {
return
}
body := resp.Body.Bytes()
require.NotEmpty(t, body)
var tokenResp tokenResponse
require.NoError(t, json.Unmarshal(body, &tokenResp))
// Check token response
require.NotEmpty(t, tokenResp.Scope)
require.ElementsMatch(t, tt.wantScope, strings.Split(tokenResp.Scope, " "))
require.Positive(t, tokenResp.ExpiresIn)
require.Equal(t, "bearer", tokenResp.TokenType)
require.NotEmpty(t, tokenResp.AccessToken)
// Check access token
parsedToken, err := jwt.ParseSigned(tokenResp.AccessToken)
require.NoError(t, err)
require.Len(t, parsedToken.Headers, 1)
typeHeader := parsedToken.Headers[0].ExtraHeaders["typ"]
require.Equal(t, "at+jwt", strings.ToLower(typeHeader.(string)))
require.Equal(t, "RS256", parsedToken.Headers[0].Algorithm)
// Check access token claims
var claims claims
require.NoError(t, parsedToken.Claims(pk.Public(), &claims))
// Check times and remove them
require.Positive(t, claims.IssuedAt.Time())
require.Positive(t, claims.Expiry.Time())
claims.IssuedAt = jwt.NewNumericDate(time.Time{})
claims.Expiry = jwt.NewNumericDate(time.Time{})
// Check the ID and remove it
require.NotEmpty(t, claims.ID)
claims.ID = ""
// Compare the rest
require.Equal(t, tt.wantClaims, &claims)
})
}
}
func genAssertion(t *testing.T, signKey *rsa.PrivateKey, clientID, sub string, audience ...string) string {
key := jose.SigningKey{Algorithm: jose.RS256, Key: signKey}
assertion := jwt.Claims{
ID: uuid.New().String(),
Issuer: clientID,
Subject: sub,
Audience: audience,
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
}
var signerOpts = jose.SignerOptions{}
signerOpts.WithType("JWT")
rsaSigner, errSigner := jose.NewSigner(key, &signerOpts)
require.NoError(t, errSigner)
builder := jwt.Signed(rsaSigner)
rawJWT, errSign := builder.Claims(assertion).CompactSerialize()
require.NoError(t, errSign)
return rawJWT
}
// setupHandleTokenRequestEnv creates a client and a user and sets all Mocks call for the handleTokenRequest test cases
func setupHandleTokenRequestEnv(t *testing.T, env *TestEnv, opt func(*oauthserver.ExternalService)) {
now := time.Now()
hashedSecret, err := bcrypt.GenerateFromPassword([]byte("CLIENT1SECRET"), bcrypt.DefaultCost)
require.NoError(t, err)
client1 := &oauthserver.ExternalService{
Name: "client-1",
ClientID: "CLIENT1ID",
Secret: string(hashedSecret),
GrantTypes: string(fosite.GrantTypeClientCredentials + "," + fosite.GrantTypeJWTBearer),
ServiceAccountID: 2,
ImpersonatePermissions: []ac.Permission{
{Action: "users:read", Scope: oauthserver.ScopeGlobalUsersSelf},
{Action: "users.permissions:read", Scope: oauthserver.ScopeUsersSelf},
{Action: "teams:read", Scope: oauthserver.ScopeTeamsSelf},
{Action: "folders:read", Scope: "folders:*"},
{Action: "dashboards:read", Scope: "folders:*"},
{Action: "dashboards:read", Scope: "dashboards:*"},
},
SelfPermissions: []ac.Permission{
{Action: "users:impersonate", Scope: "users:*"},
},
Audiences: AppURL,
}
// Apply any option the test case might need
if opt != nil {
opt(client1)
}
sa1 := &serviceaccounts.ServiceAccountProfileDTO{
Id: client1.ServiceAccountID,
Name: client1.Name,
Login: client1.Name,
OrgId: oauthserver.TmpOrgID,
IsDisabled: false,
Created: now,
Updated: now,
Role: "Viewer",
}
user56 := &user.User{
ID: 56,
Email: "user56@example.org",
Login: "user56",
Name: "User 56",
Updated: now,
}
user56Permissions := []ac.Permission{
{Action: "users:read", Scope: "global.users:id:56"},
{Action: "folders:read", Scope: "folders:uid:UID1"},
{Action: "dashboards:read", Scope: "folders:uid:UID1"},
{Action: "datasources:read", Scope: "datasources:uid:DS_UID2"}, // This one should be ignored when impersonating
}
user56Teams := []*team.TeamDTO{
{ID: 1, Name: "Team 1", OrgID: 1},
{ID: 2, Name: "Team 2", OrgID: 1},
}
// To retrieve the Client, its publicKey and its permissions
env.OAuthStore.On("GetExternalService", mock.Anything, client1.ClientID).Return(client1, nil)
env.OAuthStore.On("GetExternalServicePublicKey", mock.Anything, client1.ClientID).Return(&jose.JSONWebKey{Key: Client1Key.Public(), Algorithm: "RS256"}, nil)
env.SAService.On("RetrieveServiceAccount", mock.Anything, oauthserver.TmpOrgID, client1.ServiceAccountID).Return(sa1, nil)
env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return(client1.SelfPermissions, nil)
// To retrieve the user to impersonate, its permissions and its teams
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
user56.ID: user56Permissions}, nil)
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
user56.ID: {"Viewer"}}, nil)
env.TeamService.ExpectedTeamsByUser = user56Teams
env.UserService.ExpectedUser = user56
}