OAuth: clarify role & group paths prefer id_token over userinfo api (#39066)

* OAuth: clarify role & group paths prefer id_token over userinfo api (#39066)

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: Kevin Minehart <kmineh0151@gmail.com>
This commit is contained in:
David Lamb 2021-09-14 02:15:15 +10:00 committed by GitHub
parent ae4900e76f
commit 89878dae1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 63 additions and 15 deletions

View File

@ -55,26 +55,30 @@ You can also specify the SSL/TLS configuration used by the client.
Set `empty_scopes` to true to use an empty scope during authentication. By default, Grafana uses `user:email` as scope.
Grafana will attempt to determine the user's e-mail address by querying the OAuth provider as described below in the following order until an e-mail address is found:
### Email address
Grafana determines a user's email address by querying the OAuth provider until it finds an e-mail address:
1. Check for the presence of an e-mail address via the `email` field encoded in the OAuth `id_token` parameter.
1. Check for the presence of an e-mail address using the [JMESPath](http://jmespath.org/examples.html) specified via the `email_attribute_path` configuration option. The JSON used for the path lookup is the HTTP response obtained from querying the UserInfo endpoint specified via the `api_url` configuration option.
**Note**: Only available in Grafana v6.4+.
1. Check for the presence of an e-mail address in the `attributes` map encoded in the OAuth `id_token` parameter. By default Grafana will perform a lookup into the attributes map using the `email:primary` key, however, this is configurable and can be adjusted by using the `email_attribute_name` configuration option.
1. Query the `/emails` endpoint of the OAuth provider's API (configured with `api_url`) and check for the presence of an e-mail address marked as a primary address.
1. If no e-mail address is found in steps (1-4), then the e-mail address of the user is set to the empty string.
1. Query the `/emails` endpoint of the OAuth provider's API (configured with `api_url`), then check for the presence of an email address marked as a primary address.
1. If no email address is found in steps (1-4), then the email address of the user is set to an empty string.
Grafana will also attempt to do role mapping through OAuth as described below.
### Roles
Check for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. The JSON used for the path lookup is the HTTP response obtained from querying the UserInfo endpoint specified via the `api_url` configuration option. The result after evaluating the `role_attribute_path` JMESPath expression needs to be a valid Grafana role, i.e. `Viewer`, `Editor` or `Admin`.
Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. The JMESPath is applied to the `id_token` first. If there is no match, then the UserInfo endpoint specified via the `api_url` configuration option is tried next. The result after evaluation of the `role_attribute_path` JMESPath expression should be a valid Grafana role, for example, `Viewer`, `Editor` or `Admin`.
Grafana also attempts to map teams through OAuth as described below.
For more information, refer to the [JMESPath examples](#jmespath-examples).
Check for the presence of groups using the [JMESPath](http://jmespath.org/examples.html) specified via the `groups_attribute_path` configuration option. The JSON used for the path lookup is the HTTP response obtained from querying the UserInfo endpoint specified via the `api_url` configuration option. After evaluating the `groups_attribute_path` JMESPath expression, the result should be a string array of groups.
### Groups / Teams
Similarly, group mappings are made using [JMESPath](http://jmespath.org/examples.html) with the `groups_attribute_path` configuration option. The `id_token` is attempted first, followed by the UserInfo from the `api_url`. The result of the JMESPath expression should be a string array of groups.
Furthermore, Grafana will check for the presence of at least one of the teams specified via the `team_ids` configuration option using the [JMESPath](http://jmespath.org/examples.html) specified via the `team_ids_attribute_path` configuration option. The JSON used for the path lookup is the HTTP response obtained from querying the Teams endpoint specified via the `teams_url` configuration option (using `/teams` as a fallback endpoint). The result should be a string array of Grafana Team IDs. Using this setting ensures that only certain teams is allowed to authenticate to Grafana using your OAuth provider.
See [JMESPath examples](#jmespath-examples) for more information.
### Login
Customize user login using `login_attribute_path` configuration option. Order of operations is as follows:

View File

@ -149,19 +149,21 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
if userInfo.Role == "" {
role, err := s.extractRole(data)
if err != nil {
s.log.Error("Failed to extract role", "error", err)
s.log.Warn("Failed to extract role", "error", err)
} else if role != "" {
s.log.Debug("Setting user info role from extracted role")
userInfo.Role = role
}
}
groups, err := s.extractGroups(data)
if err != nil {
s.log.Error("Failed to extract groups", "error", err)
} else if len(groups) > 0 {
s.log.Debug("Setting user info groups from extracted groups")
userInfo.Groups = groups
if userInfo.Groups != nil && len(userInfo.Groups) == 0 {
groups, err := s.extractGroups(data)
if err != nil {
s.log.Warn("Failed to extract groups", "err", err)
} else if len(groups) > 0 {
s.log.Debug("Setting user info groups from extracted groups")
userInfo.Groups = groups
}
}
}

View File

@ -379,6 +379,48 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "FromResponse",
},
{
Name: "Given a valid id_token, a valid advanced JMESPath role path, derive the role",
OAuth2Extra: map[string]interface{}{
// { "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]interface{}{
// { "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
},
ResponseBody: map[string]interface{}{
"info": map[string]interface{}{
"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]interface{}{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
},
ResponseBody: map[string]interface{}{
"info": map[string]interface{}{
"roles": []string{"engineering", "SRE"},
},
},
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Editor",
},
}
for _, test := range tests {