Commit df11cdad by Alexander Zobnin Committed by GitHub

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
parent fa7c4d91
......@@ -326,7 +326,7 @@ hide_version = false
enabled = false
allow_sign_up = true
client_id = some_id
client_secret = some_secret
client_secret =
scopes = user:email,read:org
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
......@@ -340,7 +340,7 @@ allowed_organizations =
enabled = false
allow_sign_up = true
client_id = some_id
client_secret = some_secret
client_secret =
scopes = api
auth_url = https://gitlab.com/oauth/authorize
token_url = https://gitlab.com/oauth/token
......@@ -353,7 +353,7 @@ allowed_groups =
enabled = false
allow_sign_up = true
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
auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token
......@@ -367,7 +367,7 @@ hosted_domain =
enabled = false
allow_sign_up = true
client_id = some_id
client_secret = some_secret
client_secret =
scopes = user:email
allowed_organizations =
......@@ -375,7 +375,7 @@ allowed_organizations =
enabled = false
allow_sign_up = true
client_id = some_id
client_secret = some_secret
client_secret =
scopes = user:email
allowed_organizations =
......@@ -385,7 +385,7 @@ name = Azure AD
enabled = false
allow_sign_up = true
client_id = some_client_id
client_secret = some_client_secret
client_secret =
scopes = openid email profile
auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
......@@ -398,7 +398,7 @@ name = Okta
enabled = false
allow_sign_up = true
client_id = some_id
client_secret = some_secret
client_secret =
scopes = openid profile email groups
auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
token_url = https://<tenant-id>.okta.com/oauth2/v1/token
......@@ -413,11 +413,13 @@ name = OAuth
enabled = false
allow_sign_up = true
client_id = some_id
client_secret = some_secret
client_secret =
scopes = user:email
email_attribute_name = email:primary
email_attribute_path =
login_attribute_path =
role_attribute_path =
id_token_attribute_name =
auth_url =
token_url =
api_url =
......
......@@ -407,6 +407,8 @@
;scopes = user:email,read:org
;email_attribute_name = email:primary
;email_attribute_path =
;login_attribute_path =
;id_token_attribute_name =
;auth_url = https://foo.bar/login/oauth/authorize
;token_url = https://foo.bar/login/oauth/access_token
;api_url = https://foo.bar/user
......
......@@ -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.
> 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
1. Create a new Client in Auth0
......
......@@ -21,7 +21,9 @@ type SocialGenericOAuth struct {
apiUrl string
emailAttributeName string
emailAttributePath string
loginAttributePath string
roleAttributePath string
idTokenAttributeName string
teamIds []int
}
......@@ -148,7 +150,13 @@ func (s *SocialGenericOAuth) fillUserInfo(userInfo *BasicUserInfo, data *UserInf
func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Token) bool {
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 {
s.log.Debug("No id_token found", "token", token)
return false
......@@ -244,6 +252,15 @@ func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string {
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 != "" {
return data.Username
}
......
......@@ -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)
})
}
})
}
......@@ -174,6 +174,8 @@ func NewOAuthService() {
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,
roleAttributePath: info.RoleAttributePath,
loginAttributePath: sec.Key("login_attribute_path").String(),
idTokenAttributeName: sec.Key("id_token_attribute_name").String(),
teamIds: sec.Key("team_ids").Ints(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment