mirror of https://github.com/grafana/grafana
Okta OAuth provider (team sync support) (#22972)
* Okta OAuth support * Chore: fix linter error * Chore: move IsEmailAllowed to SocialBase * Chore: move IsSignupAllowed to SocialBase * Chore: review fixes * Okta: support allowed_groups * Okta: default config * Chore: move extractEmail() to OktaClaims struct * Chore: review fixes * generic_oauth_test: Handle error cases Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * generic_oauth_test: Handle error cases Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Docs: Okta OAuth * Chore: don't return expected errors from searchJSONForAttr * Docs: role mapping * Chore: review fixes (searchJSONForAttr) * Docs: review fixes * Update docs/sources/auth/okta.md Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com> * Update docs/sources/auth/okta.md Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com> * Chore: log error if searchJSONForAttr failed * Docs: add Okta login link * Docs: review fixes * Docs: add reference to the org roles Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>pull/23299/head
parent
703476b3ae
commit
7afdfd2ef4
@ -0,0 +1,89 @@ |
||||
+++ |
||||
title = "Okta OAuth2 authentication" |
||||
description = "Grafana Okta OAuth Guide " |
||||
keywords = ["grafana", "configuration", "documentation", "oauth"] |
||||
type = "docs" |
||||
[menu.docs] |
||||
name = "Okta" |
||||
identifier = "okta_oauth2" |
||||
parent = "authentication" |
||||
weight = 3 |
||||
+++ |
||||
|
||||
# Okta OAuth2 authentication |
||||
|
||||
> Only available in Grafana v7.0+ |
||||
|
||||
The Okta authentication allows your Grafana users to log in by using an external Okta authorization server. |
||||
|
||||
## Create an Okta application |
||||
|
||||
Before you can sign a user in, you need to create an Okta application from the Okta Developer Console. |
||||
|
||||
1. Log in to the [Okta portal](https://login.okta.com/). |
||||
|
||||
1. Go to Admin and then select **Developer Console**. |
||||
|
||||
1. Select **Applications**, then **Add Application**. |
||||
|
||||
1. Pick **Web** as the platform. |
||||
|
||||
1. Enter a name for your application (or leave the default value). |
||||
|
||||
1. Add the **Base URI** of your application, such as https://grafana.example.com. |
||||
|
||||
1. Enter values for the **Login redirect URI**. Use **Base URI** and append it with `/login/okta`, for example: https://grafana.example.com/login/okta. |
||||
|
||||
1. Click **Done** to finish creating the Okta application. |
||||
|
||||
## Enable Okta Oauth in Grafana |
||||
|
||||
1. Add the following to the [Grafana configuration file]({{< relref "../installation/configuration.md#config-file-locations" >}}): |
||||
|
||||
```ini |
||||
[auth.okta] |
||||
name = Okta |
||||
enabled = true |
||||
allow_sign_up = true |
||||
client_id = some_id |
||||
client_secret = some_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 |
||||
api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo |
||||
allowed_domains = |
||||
allowed_groups = |
||||
role_attribute_path = |
||||
``` |
||||
|
||||
### Configure allowed groups and domains |
||||
|
||||
To limit access to authenticated users that are members of one or more groups, set `allowed_groups` |
||||
to a comma- or space-separated list of Okta groups. |
||||
|
||||
```ini |
||||
allowed_groups = Developers, Admins |
||||
``` |
||||
|
||||
The `allowed_domains` option limits access to the users belonging to the specific domains. Domains should be separated by space or comma. |
||||
|
||||
```ini |
||||
allowed_domains = mycompany.com mycompany.org |
||||
``` |
||||
|
||||
### Map roles |
||||
|
||||
Grafana can attempt to do role mapping through Okta OAuth. In order to achieve this, Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. |
||||
|
||||
Grafana uses JSON obtained from querying the `/userinfo` endpoint for the path lookup. The result after evaluating the `role_attribute_path` JMESPath expression needs to be a valid Grafana role, i.e. `Viewer`, `Editor` or `Admin`. Refer to [Organization roles]({{< relref "../permissions/organization_roles.md" >}}) for more information about roles and permissions in Grafana. |
||||
|
||||
Read about how to [add custom claims](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/add-custom-claim/) to the user info in Okta. Also, check Generic OAuth page for [JMESPath examples]({{< relref "generic-oauth.md/#jmespath-examples" >}}). |
||||
|
||||
### Team Sync (Enterprise only) |
||||
|
||||
Map your Okta groups to teams in Grafana so that your users will automatically be added to |
||||
the correct teams. |
||||
|
||||
Okta groups can be referenced by group name, like `Admins`. |
||||
|
||||
[Learn more about Team Sync]({{< relref "../enterprise/team-sync.md" >}}) |
||||
@ -0,0 +1,153 @@ |
||||
package social |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/util/errutil" |
||||
"golang.org/x/oauth2" |
||||
"gopkg.in/square/go-jose.v2/jwt" |
||||
) |
||||
|
||||
type SocialOkta struct { |
||||
*SocialBase |
||||
apiUrl string |
||||
allowedGroups []string |
||||
roleAttributePath string |
||||
} |
||||
|
||||
type OktaUserInfoJson struct { |
||||
Name string `json:"name"` |
||||
DisplayName string `json:"display_name"` |
||||
Login string `json:"login"` |
||||
Username string `json:"username"` |
||||
Email string `json:"email"` |
||||
Upn string `json:"upn"` |
||||
Attributes map[string][]string `json:"attributes"` |
||||
Groups []string `json:"groups"` |
||||
rawJSON []byte |
||||
} |
||||
|
||||
type OktaClaims struct { |
||||
ID string `json:"sub"` |
||||
Email string `json:"email"` |
||||
PreferredUsername string `json:"preferred_username"` |
||||
Name string `json:"name"` |
||||
} |
||||
|
||||
func (claims *OktaClaims) extractEmail() string { |
||||
if claims.Email == "" && claims.PreferredUsername != "" { |
||||
return claims.PreferredUsername |
||||
} |
||||
|
||||
return claims.Email |
||||
} |
||||
|
||||
func (s *SocialOkta) Type() int { |
||||
return int(models.OKTA) |
||||
} |
||||
|
||||
func (s *SocialOkta) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { |
||||
idToken := token.Extra("id_token") |
||||
if idToken == nil { |
||||
return nil, fmt.Errorf("no id_token found") |
||||
} |
||||
|
||||
parsedToken, err := jwt.ParseSigned(idToken.(string)) |
||||
if err != nil { |
||||
return nil, errutil.Wrapf(err, "error parsing id token") |
||||
} |
||||
|
||||
var claims OktaClaims |
||||
if err := parsedToken.UnsafeClaimsWithoutVerification(&claims); err != nil { |
||||
return nil, errutil.Wrapf(err, "error getting claims from id token") |
||||
} |
||||
|
||||
email := claims.extractEmail() |
||||
if email == "" { |
||||
return nil, errors.New("error getting user info: no email found in access token") |
||||
} |
||||
|
||||
var data OktaUserInfoJson |
||||
err = s.extractAPI(&data, client) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
role, err := s.extractRole(&data) |
||||
if err != nil { |
||||
s.log.Error("Failed to extract role", "error", err) |
||||
} |
||||
|
||||
groups := s.GetGroups(&data) |
||||
if !s.IsGroupMember(groups) { |
||||
return nil, ErrMissingGroupMembership |
||||
} |
||||
|
||||
return &BasicUserInfo{ |
||||
Id: claims.ID, |
||||
Name: claims.Name, |
||||
Email: email, |
||||
Login: email, |
||||
Role: role, |
||||
Groups: groups, |
||||
}, nil |
||||
} |
||||
|
||||
func (s *SocialOkta) extractAPI(data *OktaUserInfoJson, client *http.Client) error { |
||||
rawUserInfoResponse, err := HttpGet(client, s.apiUrl) |
||||
if err != nil { |
||||
s.log.Debug("Error getting user info response", "url", s.apiUrl, "error", err) |
||||
return errutil.Wrapf(err, "error getting user info response") |
||||
} |
||||
data.rawJSON = rawUserInfoResponse.Body |
||||
|
||||
err = json.Unmarshal(data.rawJSON, data) |
||||
if err != nil { |
||||
s.log.Debug("Error decoding user info response", "raw_json", data.rawJSON, "error", err) |
||||
data.rawJSON = []byte{} |
||||
return errutil.Wrapf(err, "error decoding user info response") |
||||
} |
||||
|
||||
s.log.Debug("Received user info response", "raw_json", string(data.rawJSON), "data", data) |
||||
return nil |
||||
} |
||||
|
||||
func (s *SocialOkta) extractRole(data *OktaUserInfoJson) (string, error) { |
||||
if s.roleAttributePath == "" { |
||||
return "", nil |
||||
} |
||||
|
||||
role, err := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return role, nil |
||||
} |
||||
|
||||
func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string { |
||||
groups := make([]string, 0) |
||||
if len(data.Groups) > 0 { |
||||
groups = data.Groups |
||||
} |
||||
return groups |
||||
} |
||||
|
||||
func (s *SocialOkta) IsGroupMember(groups []string) bool { |
||||
if len(s.allowedGroups) == 0 { |
||||
return true |
||||
} |
||||
|
||||
for _, allowedGroup := range s.allowedGroups { |
||||
for _, group := range groups { |
||||
if group == allowedGroup { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
After Width: | Height: | Size: 110 KiB |
Loading…
Reference in new issue