Generic OAuth: customize login and id_token attributes (#26577)

* OAuth: add login_attribute_path to generic oauth

* OAuth: remove default client_secret values (able to use empty client_secret)

* OAuth: allow to customize id_token attribute name

* Docs: describe how login_attribute_path and id_token_attribute_name params work

* Docs: review fixes

* Docs: review fixes

* Chore: fix go linter error

* Tests: fix test code style
This commit is contained in:
Alexander Zobnin 2020-08-03 17:33:27 +03:00 committed by GitHub
parent fa7c4d91aa
commit df11cdad62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 135 additions and 9 deletions

View File

@ -326,7 +326,7 @@ hide_version = false
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_id client_id = some_id
client_secret = some_secret client_secret =
scopes = user:email,read:org scopes = user:email,read:org
auth_url = https://github.com/login/oauth/authorize auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token token_url = https://github.com/login/oauth/access_token
@ -340,7 +340,7 @@ allowed_organizations =
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_id client_id = some_id
client_secret = some_secret client_secret =
scopes = api scopes = api
auth_url = https://gitlab.com/oauth/authorize auth_url = https://gitlab.com/oauth/authorize
token_url = https://gitlab.com/oauth/token token_url = https://gitlab.com/oauth/token
@ -353,7 +353,7 @@ allowed_groups =
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_client_id client_id = some_client_id
client_secret = some_client_secret client_secret =
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
auth_url = https://accounts.google.com/o/oauth2/auth auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token token_url = https://accounts.google.com/o/oauth2/token
@ -367,7 +367,7 @@ hosted_domain =
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_id client_id = some_id
client_secret = some_secret client_secret =
scopes = user:email scopes = user:email
allowed_organizations = allowed_organizations =
@ -375,7 +375,7 @@ allowed_organizations =
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_id client_id = some_id
client_secret = some_secret client_secret =
scopes = user:email scopes = user:email
allowed_organizations = allowed_organizations =
@ -385,7 +385,7 @@ name = Azure AD
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_client_id client_id = some_client_id
client_secret = some_client_secret client_secret =
scopes = openid email profile scopes = openid email profile
auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
@ -398,7 +398,7 @@ name = Okta
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_id client_id = some_id
client_secret = some_secret client_secret =
scopes = openid profile email groups scopes = openid profile email groups
auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
token_url = https://<tenant-id>.okta.com/oauth2/v1/token token_url = https://<tenant-id>.okta.com/oauth2/v1/token
@ -413,11 +413,13 @@ name = OAuth
enabled = false enabled = false
allow_sign_up = true allow_sign_up = true
client_id = some_id client_id = some_id
client_secret = some_secret client_secret =
scopes = user:email scopes = user:email
email_attribute_name = email:primary email_attribute_name = email:primary
email_attribute_path = email_attribute_path =
login_attribute_path =
role_attribute_path = role_attribute_path =
id_token_attribute_name =
auth_url = auth_url =
token_url = token_url =
api_url = api_url =

View File

@ -407,6 +407,8 @@
;scopes = user:email,read:org ;scopes = user:email,read:org
;email_attribute_name = email:primary ;email_attribute_name = email:primary
;email_attribute_path = ;email_attribute_path =
;login_attribute_path =
;id_token_attribute_name =
;auth_url = https://foo.bar/login/oauth/authorize ;auth_url = https://foo.bar/login/oauth/authorize
;token_url = https://foo.bar/login/oauth/access_token ;token_url = https://foo.bar/login/oauth/access_token
;api_url = https://foo.bar/user ;api_url = https://foo.bar/user

View File

@ -59,6 +59,15 @@ Check for the presence of a role using the [JMESPath](http://jmespath.org/exampl
See [JMESPath examples](#jmespath-examples) for more information. See [JMESPath examples](#jmespath-examples) for more information.
> Only available in Grafana v7.2+.
Customize user login using `login_attribute_path` configuration option. Order of operations is as follows:
1. Grafana evaluates the `login_attribute_path` JMESPath expression against the ID token.
1. If Grafana finds no value, then Grafana evaluates expression against the JSON data obtained from UserInfo endpoint. The UserInfo endpoint URL is specified in the `api_url` configuration option.
You can customize the attribute name used to extract the ID token from the returned OAuth token with the `id_token_attribute_name` option.
## Set up OAuth2 with Auth0 ## Set up OAuth2 with Auth0
1. Create a new Client in Auth0 1. Create a new Client in Auth0

View File

@ -21,7 +21,9 @@ type SocialGenericOAuth struct {
apiUrl string apiUrl string
emailAttributeName string emailAttributeName string
emailAttributePath string emailAttributePath string
loginAttributePath string
roleAttributePath string roleAttributePath string
idTokenAttributeName string
teamIds []int teamIds []int
} }
@ -148,7 +150,13 @@ func (s *SocialGenericOAuth) fillUserInfo(userInfo *BasicUserInfo, data *UserInf
func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Token) bool { func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Token) bool {
var err error var err error
idToken := token.Extra("id_token") idTokenAttribute := "id_token"
if s.idTokenAttributeName != "" {
idTokenAttribute = s.idTokenAttributeName
s.log.Debug("Using custom id_token attribute name", "attribute_name", idTokenAttribute)
}
idToken := token.Extra(idTokenAttribute)
if idToken == nil { if idToken == nil {
s.log.Debug("No id_token found", "token", token) s.log.Debug("No id_token found", "token", token)
return false return false
@ -244,6 +252,15 @@ func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string {
return data.Login return data.Login
} }
if s.loginAttributePath != "" {
login, err := s.searchJSONForAttr(s.loginAttributePath, data.rawJSON)
if err != nil {
s.log.Error("Failed to search JSON for attribute", "error", err)
} else if login != "" {
return login
}
}
if data.Username != "" { if data.Username != "" {
return data.Username return data.Username
} }

View File

@ -331,3 +331,97 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
} }
}) })
} }
func TestUserInfoSearchesForLogin(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{
SocialBase: &SocialBase{
log: log.New("generic_oauth_test"),
},
loginAttributePath: "login",
}
tests := []struct {
Name string
APIURLResponse interface{}
OAuth2Extra interface{}
LoginAttributePath string
ExpectedLogin string
}{
{
Name: "Given a valid id_token, a valid login path, no api response, use id_token",
OAuth2Extra: map[string]interface{}{
// { "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]interface{}{
// { "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",
APIURLResponse: map[string]interface{}{
"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",
APIURLResponse: map[string]interface{}{
"login": "johndoe",
},
LoginAttributePath: "",
ExpectedLogin: "johndoe",
},
{
Name: "Given no id_token, a login path, a valid api response without a login, use api response",
APIURLResponse: map[string]interface{}{
"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) {
response, err := json.Marshal(test.APIURLResponse)
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(response)
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(ts.Client(), token)
require.NoError(t, err)
require.Equal(t, test.ExpectedLogin, actualResult.Login)
})
}
})
}

View File

@ -174,6 +174,8 @@ func NewOAuthService() {
emailAttributeName: info.EmailAttributeName, emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath, emailAttributePath: info.EmailAttributePath,
roleAttributePath: info.RoleAttributePath, roleAttributePath: info.RoleAttributePath,
loginAttributePath: sec.Key("login_attribute_path").String(),
idTokenAttributeName: sec.Key("id_token_attribute_name").String(),
teamIds: sec.Key("team_ids").Ints(","), teamIds: sec.Key("team_ids").Ints(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
} }