From df11cdad6243cae58583dcc91903f5e8fb4f8644 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 3 Aug 2020 17:33:27 +0300 Subject: [PATCH] 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 --- conf/defaults.ini | 18 ++--- conf/sample.ini | 2 + docs/sources/auth/generic-oauth.md | 9 +++ pkg/login/social/generic_oauth.go | 19 +++++- pkg/login/social/generic_oauth_test.go | 94 ++++++++++++++++++++++++++ pkg/login/social/social.go | 2 + 6 files changed, 135 insertions(+), 9 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index b02ece6da16..5afccfb27df 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -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//oauth2/v2.0/authorize token_url = https://login.microsoftonline.com//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://.okta.com/oauth2/v1/authorize token_url = https://.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 = diff --git a/conf/sample.ini b/conf/sample.ini index 15f18e3ab77..2c960507082 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -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 diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index 80daf75b70f..52f2b49d72b 100755 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -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 diff --git a/pkg/login/social/generic_oauth.go b/pkg/login/social/generic_oauth.go index 505694b78f5..f71e7bd318b 100644 --- a/pkg/login/social/generic_oauth.go +++ b/pkg/login/social/generic_oauth.go @@ -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 } diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index 3929a325462..bdeca63163b 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -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) + }) + } + }) +} diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index fc4b9c9141e..0c20be872f6 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -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()), }