mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
fa7c4d91aa
commit
df11cdad62
@ -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 =
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
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()),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user