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. 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 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. 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+. **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. 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. 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 e-mail address is found in steps (1-4), then the e-mail address of the user is set to the empty string. 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. 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: 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 == "" { if userInfo.Role == "" {
role, err := s.extractRole(data) role, err := s.extractRole(data)
if err != nil { if err != nil {
s.log.Error("Failed to extract role", "error", err) s.log.Warn("Failed to extract role", "error", err)
} else if role != "" { } else if role != "" {
s.log.Debug("Setting user info role from extracted role") s.log.Debug("Setting user info role from extracted role")
userInfo.Role = role userInfo.Role = role
} }
} }
groups, err := s.extractGroups(data) if userInfo.Groups != nil && len(userInfo.Groups) == 0 {
if err != nil { groups, err := s.extractGroups(data)
s.log.Error("Failed to extract groups", "error", err) if err != nil {
} else if len(groups) > 0 { s.log.Warn("Failed to extract groups", "err", err)
s.log.Debug("Setting user info groups from extracted groups") } else if len(groups) > 0 {
userInfo.Groups = groups 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", ExpectedEmail: "john.doe@example.com",
ExpectedRole: "FromResponse", 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 { for _, test := range tests {