Files
grafana/pkg/login/social/connectors/generic_oauth_test.go
Misi ce1450d4d3 Chore: Configure SkipOrgRoleSync from OAuthInfo for OAuth connectors (#79443)
* Configure SkipOrgRoleSync from OAuthInfo

* Remove skipOrgRoleSync from socialbase and connectors

* Add test to socialimpl.ProvideService

* Deprecate AuthSettings' fields

* clean up misleading init of frontendsettings.Auth
2023-12-15 10:58:08 +01:00

918 lines
31 KiB
Go

package connectors
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
func TestSearchJSONForEmail(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
UserInfoJSONResponse []byte
EmailAttributePath string
ExpectedResult string
ExpectedError string
}{
{
Name: "Given an invalid user info JSON response",
UserInfoJSONResponse: []byte("{"),
EmailAttributePath: "attributes.email",
ExpectedResult: "",
ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input",
},
{
Name: "Given an empty user info JSON response and empty JMES path",
UserInfoJSONResponse: []byte{},
EmailAttributePath: "",
ExpectedResult: "",
ExpectedError: "no attribute path specified",
},
{
Name: "Given an empty user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte{},
EmailAttributePath: "attributes.email",
ExpectedResult: "",
ExpectedError: "empty user info JSON response provided",
},
{
Name: "Given a simple user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte(`{
"attributes": {
"email": "grafana@localhost"
}
}`),
EmailAttributePath: "attributes.email",
ExpectedResult: "grafana@localhost",
},
{
Name: "Given a user info JSON response with e-mails array and valid JMES path",
UserInfoJSONResponse: []byte(`{
"attributes": {
"emails": ["grafana@localhost", "admin@localhost"]
}
}`),
EmailAttributePath: "attributes.emails[0]",
ExpectedResult: "grafana@localhost",
},
{
Name: "Given a nested user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte(`{
"identities": [
{
"userId": "grafana@localhost"
},
{
"userId": "admin@localhost"
}
]
}`),
EmailAttributePath: "identities[0].userId",
ExpectedResult: "grafana@localhost",
},
}
for _, test := range tests {
provider.emailAttributePath = test.EmailAttributePath
t.Run(test.Name, func(t *testing.T) {
actualResult, err := provider.searchJSONForStringAttr(test.EmailAttributePath, test.UserInfoJSONResponse)
if test.ExpectedError == "" {
require.NoError(t, err, "Testing case %q", test.Name)
} else {
require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name)
}
require.Equal(t, test.ExpectedResult, actualResult)
})
}
})
}
func TestSearchJSONForGroups(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
UserInfoJSONResponse []byte
GroupsAttributePath string
ExpectedResult []string
ExpectedError string
}{
{
Name: "Given an invalid user info JSON response",
UserInfoJSONResponse: []byte("{"),
GroupsAttributePath: "attributes.groups",
ExpectedResult: []string{},
ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input",
},
{
Name: "Given an empty user info JSON response and empty JMES path",
UserInfoJSONResponse: []byte{},
GroupsAttributePath: "",
ExpectedResult: []string{},
ExpectedError: "no attribute path specified",
},
{
Name: "Given an empty user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte{},
GroupsAttributePath: "attributes.groups",
ExpectedResult: []string{},
ExpectedError: "empty user info JSON response provided",
},
{
Name: "Given a simple user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte(`{
"attributes": {
"groups": ["foo", "bar"]
}
}`),
GroupsAttributePath: "attributes.groups[]",
ExpectedResult: []string{"foo", "bar"},
},
}
for _, test := range tests {
provider.groupsAttributePath = test.GroupsAttributePath
t.Run(test.Name, func(t *testing.T) {
actualResult, err := provider.searchJSONForStringArrayAttr(test.GroupsAttributePath, test.UserInfoJSONResponse)
if test.ExpectedError == "" {
require.NoError(t, err, "Testing case %q", test.Name)
} else {
require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name)
}
require.Equal(t, test.ExpectedResult, actualResult)
})
}
})
}
func TestSearchJSONForRole(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
UserInfoJSONResponse []byte
RoleAttributePath string
ExpectedResult string
ExpectedError string
}{
{
Name: "Given an invalid user info JSON response",
UserInfoJSONResponse: []byte("{"),
RoleAttributePath: "attributes.role",
ExpectedResult: "",
ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input",
},
{
Name: "Given an empty user info JSON response and empty JMES path",
UserInfoJSONResponse: []byte{},
RoleAttributePath: "",
ExpectedResult: "",
ExpectedError: "no attribute path specified",
},
{
Name: "Given an empty user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte{},
RoleAttributePath: "attributes.role",
ExpectedResult: "",
ExpectedError: "empty user info JSON response provided",
},
{
Name: "Given a simple user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte(`{
"attributes": {
"role": "admin"
}
}`),
RoleAttributePath: "attributes.role",
ExpectedResult: "admin",
},
}
for _, test := range tests {
provider.roleAttributePath = test.RoleAttributePath
t.Run(test.Name, func(t *testing.T) {
actualResult, err := provider.searchJSONForStringAttr(test.RoleAttributePath, test.UserInfoJSONResponse)
if test.ExpectedError == "" {
require.NoError(t, err, "Testing case %q", test.Name)
} else {
require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name)
}
require.Equal(t, test.ExpectedResult, actualResult)
})
}
})
}
func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
provider := NewGenericOAuthProvider(&social.OAuthInfo{
EmailAttributePath: "email",
}, &setting.Cfg{},
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
tests := []struct {
Name string
SkipOrgRoleSync bool
AllowAssignGrafanaAdmin bool
ResponseBody any
OAuth2Extra any
RoleAttributePath string
ExpectedEmail string
ExpectedRole org.RoleType
ExpectedError error
ExpectedGrafanaAdmin *bool
}{
{
Name: "Given a valid id_token, a valid role path, no API response, use id_token",
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
},
{
Name: "Given a valid id_token, no role path, no API response, use id_token",
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
},
RoleAttributePath: "",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Viewer",
},
{
Name: "Given a valid id_token, an invalid role path, no API response, use id_token",
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
},
RoleAttributePath: "invalid_path",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Viewer",
},
{
Name: "Given no id_token, a valid role path, a valid API response, use API response",
ResponseBody: map[string]any{
"role": "Admin",
"email": "john.doe@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
},
{
Name: "Given no id_token, no role path, a valid API response, use API response",
ResponseBody: map[string]any{
"email": "john.doe@example.com",
},
RoleAttributePath: "",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Viewer",
},
{
Name: "Given no id_token, a role path, a valid API response without a role, use API response",
ResponseBody: map[string]any{
"email": "john.doe@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Viewer",
},
{
Name: "Given no id_token, a valid role path, no API response, no data",
RoleAttributePath: "role",
ExpectedEmail: "",
ExpectedRole: "Viewer",
},
{
Name: "Given a valid id_token, a valid role path, a valid API response, prefer id_token",
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
},
ResponseBody: map[string]any{
"role": "FromResponse",
"email": "from_response@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
},
{
Name: "Given a valid id_token and AssignGrafanaAdmin is unchecked, don't grant Server Admin",
AllowAssignGrafanaAdmin: false,
OAuth2Extra: map[string]any{
// { "role": "GrafanaAdmin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI",
},
ResponseBody: map[string]any{
"role": "FromResponse",
"email": "from_response@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
ExpectedGrafanaAdmin: nil,
},
{
Name: "Given a valid id_token and AssignGrafanaAdmin is checked, grant Server Admin",
AllowAssignGrafanaAdmin: true,
OAuth2Extra: map[string]any{
// { "role": "GrafanaAdmin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI",
},
ResponseBody: map[string]any{
"role": "FromResponse",
"email": "from_response@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
ExpectedGrafanaAdmin: trueBoolPtr(),
},
{
Name: "Given a valid id_token, an invalid role path, a valid API response, prefer id_token",
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
},
ResponseBody: map[string]any{
"role": "FromResponse",
"email": "from_response@example.com",
},
RoleAttributePath: "invalid_path",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Viewer",
},
{
Name: "Given a valid id_token with no email, a valid role path, a valid API response with no role, merge",
OAuth2Extra: map[string]any{
// { "role": "Admin" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4ifQ.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
},
ResponseBody: map[string]any{
"email": "from_response@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "from_response@example.com",
ExpectedRole: "Admin",
},
{
Name: "Given a valid id_token with no role, a valid role path, a valid API response with no email, merge",
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
},
ResponseBody: map[string]any{
"role": "FromResponse",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Viewer",
ExpectedError: nil,
},
{
Name: "Given a valid id_token, a valid advanced JMESPath role path, derive the role",
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
},
RoleAttributePath: "contains(info.roles[*], 'dev') && 'Editor'",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Editor",
},
{
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid API response, derive the correct role using the userinfo API response (JMESPath warning on id_token)",
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
},
ResponseBody: map[string]any{
"info": map[string]any{
"roles": []string{"engineering", "SRE"},
},
},
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin'",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
},
{
Name: "Given a valid id_token, a valid advanced JMESPath role path, a valid API response, prefer ID token",
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
},
ResponseBody: map[string]any{
"info": map[string]any{
"roles": []string{"engineering", "SRE"},
},
},
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Editor",
},
{
Name: "Given skip org role sync set to true, with a valid id_token, a valid advanced JMESPath role path, a valid API response, no org role should be set",
SkipOrgRoleSync: true,
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
},
ResponseBody: map[string]any{
"info": map[string]any{
"roles": []string{"engineering", "SRE"},
},
},
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "",
},
}
for _, test := range tests {
provider.roleAttributePath = test.RoleAttributePath
provider.allowAssignGrafanaAdmin = test.AllowAssignGrafanaAdmin
provider.info.SkipOrgRoleSync = test.SkipOrgRoleSync
t.Run(test.Name, func(t *testing.T) {
body, err := json.Marshal(test.ResponseBody)
require.NoError(t, err)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(body)
require.NoError(t, err)
}))
provider.apiUrl = ts.URL
staticToken := oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(test.OAuth2Extra)
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
if test.ExpectedError != nil {
require.ErrorIs(t, err, test.ExpectedError)
return
}
require.NoError(t, err)
require.Equal(t, test.ExpectedEmail, actualResult.Email)
require.Equal(t, test.ExpectedEmail, actualResult.Login)
require.Equal(t, test.ExpectedRole, actualResult.Role)
require.Equal(t, test.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin)
})
}
}
func TestUserInfoSearchesForLogin(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := NewGenericOAuthProvider(&social.OAuthInfo{
Extra: map[string]string{
"login_attribute_path": "login",
},
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
ResponseBody any
OAuth2Extra any
LoginAttributePath string
ExpectedLogin string
}{
{
Name: "Given a valid id_token, a valid login path, no API response, use id_token",
OAuth2Extra: map[string]any{
// { "login": "johndoe", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
},
LoginAttributePath: "role",
ExpectedLogin: "johndoe",
},
{
Name: "Given a valid id_token, no login path, no API response, use id_token",
OAuth2Extra: map[string]any{
// { "login": "johndoe", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
},
LoginAttributePath: "",
ExpectedLogin: "johndoe",
},
{
Name: "Given no id_token, a valid login path, a valid API response, use API response",
ResponseBody: map[string]any{
"user_uid": "johndoe",
"email": "john.doe@example.com",
},
LoginAttributePath: "user_uid",
ExpectedLogin: "johndoe",
},
{
Name: "Given no id_token, no login path, a valid API response, use API response",
ResponseBody: map[string]any{
"login": "johndoe",
},
LoginAttributePath: "",
ExpectedLogin: "johndoe",
},
{
Name: "Given no id_token, a login path, a valid API response without a login, use API response",
ResponseBody: map[string]any{
"username": "john.doe",
},
LoginAttributePath: "login",
ExpectedLogin: "john.doe",
},
{
Name: "Given no id_token, a valid login path, no API response, no data",
LoginAttributePath: "login",
ExpectedLogin: "",
},
}
for _, test := range tests {
provider.loginAttributePath = test.LoginAttributePath
t.Run(test.Name, func(t *testing.T) {
body, err := json.Marshal(test.ResponseBody)
require.NoError(t, err)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
t.Log("Writing fake API response body", "body", test.ResponseBody)
_, err = w.Write(body)
require.NoError(t, err)
}))
provider.apiUrl = ts.URL
staticToken := oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(test.OAuth2Extra)
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
require.NoError(t, err)
require.Equal(t, test.ExpectedLogin, actualResult.Login)
})
}
})
}
func TestUserInfoSearchesForName(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := NewGenericOAuthProvider(&social.OAuthInfo{
Extra: map[string]string{
"name_attribute_path": "name",
},
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
ResponseBody any
OAuth2Extra any
NameAttributePath string
ExpectedName string
}{
{
Name: "Given a valid id_token, a valid name path, no API response, use id_token",
OAuth2Extra: map[string]any{
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
},
NameAttributePath: "name",
ExpectedName: "John Doe",
},
{
Name: "Given a valid id_token, no name path, no API response, use id_token",
OAuth2Extra: map[string]any{
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
},
NameAttributePath: "",
ExpectedName: "John Doe",
},
{
Name: "Given no id_token, a valid name path, a valid API response, use API response",
ResponseBody: map[string]any{
"user_name": "John Doe",
"login": "johndoe",
"email": "john.doe@example.com",
},
NameAttributePath: "user_name",
ExpectedName: "John Doe",
},
{
Name: "Given no id_token, no name path, a valid API response, use API response",
ResponseBody: map[string]any{
"display_name": "John Doe",
"login": "johndoe",
},
NameAttributePath: "",
ExpectedName: "John Doe",
},
{
Name: "Given no id_token, a name path, a valid API response without a name, use API response",
ResponseBody: map[string]any{
"display_name": "John Doe",
"username": "john.doe",
},
NameAttributePath: "name",
ExpectedName: "John Doe",
},
{
Name: "Given no id_token, a valid name path, no API response, no data",
NameAttributePath: "name",
ExpectedName: "",
},
}
for _, test := range tests {
provider.nameAttributePath = test.NameAttributePath
t.Run(test.Name, func(t *testing.T) {
body, err := json.Marshal(test.ResponseBody)
require.NoError(t, err)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
t.Log("Writing fake API response body", "body", test.ResponseBody)
_, err = w.Write(body)
require.NoError(t, err)
}))
provider.apiUrl = ts.URL
staticToken := oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(test.OAuth2Extra)
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
require.NoError(t, err)
require.Equal(t, test.ExpectedName, actualResult.Name)
})
}
})
}
func TestUserInfoSearchesForGroup(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
tests := []struct {
name string
groupsAttributePath string
responseBody any
expectedResult []string
}{
{
name: "If groups are not set, user groups are nil",
groupsAttributePath: "",
expectedResult: nil,
},
{
name: "If groups are empty, user groups are nil",
groupsAttributePath: "info.groups",
responseBody: map[string]any{
"info": map[string]any{
"groups": []string{},
},
},
expectedResult: nil,
},
{
name: "If groups are set, user groups are set",
groupsAttributePath: "info.groups",
responseBody: map[string]any{
"info": map[string]any{
"groups": []string{"foo", "bar"},
},
},
expectedResult: []string{"foo", "bar"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
body, err := json.Marshal(test.responseBody)
require.NoError(t, err)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
t.Log("Writing fake API response body", "body", test.responseBody)
_, err := w.Write(body)
require.NoError(t, err)
}))
provider := NewGenericOAuthProvider(&social.OAuthInfo{
GroupsAttributePath: test.groupsAttributePath,
ApiUrl: ts.URL,
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
token := &oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
userInfo, err := provider.UserInfo(context.Background(), ts.Client(), token)
assert.NoError(t, err)
assert.Equal(t, test.expectedResult, userInfo.Groups)
})
}
})
}
func TestPayloadCompression(t *testing.T) {
provider := NewGenericOAuthProvider(&social.OAuthInfo{
EmailAttributePath: "email",
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
OAuth2Extra any
ExpectedEmail string
}{
{
Name: "Given a valid DEFLATE compressed id_token, return userInfo",
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInppcCI6IkRFRiJ9.eJyrVkrNTczMUbJSysrPyNNLyU91SK1IzC3ISdVLzs9V0lEqys9JBco6puRm5inVAgCFRw_6.XrV4ZKhw19dTcnviXanBD8lwjeALCYtDiESMmGzC-ho",
},
ExpectedEmail: "john.doe@example.com",
},
{
Name: "Given a valid DEFLATE compressed id_token with numeric header, return userInfo",
OAuth2Extra: map[string]any{
// Generated from https://token.dev/
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsInZlciI6NH0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTY0MjUxNjYwNSwiZXhwIjoxNjQyNTIwMjA1LCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.ANndoPWIHNjKPG8na7UUq7nan1RgF8-ze8STU31RXcA",
},
ExpectedEmail: "john.doe@example.com",
},
{
Name: "Given an invalid DEFLATE compressed id_token, return nil",
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInppcCI6IkRFRiJ9.00eJyrVkrNTczMUbJSysrPyNNLyU91SK1IzC3ISdVLzs9V0lEqys9JBco6puRm5inVAgCFRw_6.XrV4ZKhw19dTcnviXanBD8lwjeALCYtDiESMmGzC-ho",
},
ExpectedEmail: "",
},
{
Name: "Given an unsupported GZIP compressed id_token, return nil",
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAKtWSs1NzMxRslLKys_I00vJT3VIrUjMLchJ1UvOz1XSUSrKz0kFyjqm5GbmKdUCANotxTkvAAAA.85AXm3JOF5qflEA0goDFvlbZl2q3eFvqVcehz860W-o",
},
ExpectedEmail: "",
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
staticToken := oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(test.OAuth2Extra)
userInfo := provider.extractFromToken(token)
if test.ExpectedEmail == "" {
require.Nil(t, userInfo, "Testing case %q", test.Name)
} else {
require.NotNil(t, userInfo, "Testing case %q", test.Name)
require.Equal(t, test.ExpectedEmail, userInfo.Email)
}
})
}
}
func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
type settingFields struct {
nameAttributePath string
loginAttributePath string
idTokenAttributeName string
teamIds []string
allowedOrganizations []string
}
testCases := []struct {
name string
settings *social.OAuthInfo
want settingFields
}{
{
name: "nameAttributePath is set",
settings: &social.OAuthInfo{
Extra: map[string]string{
"name_attribute_path": "name",
},
},
want: settingFields{
nameAttributePath: "name",
loginAttributePath: "",
idTokenAttributeName: "",
teamIds: []string{},
allowedOrganizations: []string{},
},
},
{
name: "loginAttributePath is set",
settings: &social.OAuthInfo{
Extra: map[string]string{
"login_attribute_path": "login",
},
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "login",
idTokenAttributeName: "",
teamIds: []string{},
allowedOrganizations: []string{},
},
},
{
name: "idTokenAttributeName is set",
settings: &social.OAuthInfo{
Extra: map[string]string{
"id_token_attribute_name": "id_token",
},
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "",
idTokenAttributeName: "id_token",
teamIds: []string{},
allowedOrganizations: []string{},
},
},
{
name: "teamIds is set",
settings: &social.OAuthInfo{
Extra: map[string]string{
"team_ids": "[\"team1\", \"team2\"]",
},
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "",
idTokenAttributeName: "",
teamIds: []string{"team1", "team2"},
allowedOrganizations: []string{},
},
},
{
name: "allowedOrganizations is set",
settings: &social.OAuthInfo{
Extra: map[string]string{
"allowed_organizations": "org1, org2",
},
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "",
idTokenAttributeName: "",
teamIds: []string{},
allowedOrganizations: []string{"org1", "org2"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGenericOAuthProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
require.Equal(t, tc.want.nameAttributePath, s.nameAttributePath)
require.Equal(t, tc.want.loginAttributePath, s.loginAttributePath)
require.Equal(t, tc.want.idTokenAttributeName, s.idTokenAttributeName)
require.Equal(t, tc.want.teamIds, s.teamIds)
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
})
}
}