3

I am trying to integrate Keycloak as OAuth provider for Grafana. I am successful in authenticating the user but not able to assign the correct role to the user. Following are the configurations that I have used.

Grafana Environment Variable:

- GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH="contains(roles[*], 'admin') && 'USER' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'"

When a POST request is sent to http://{host}:{port}/auth/realms/{realm}/protocol/openid-connect/token I receive fields: access_token, expires_in, refresh_token, refresh_expires_in, token_type, id_token, not-before-policy, session_state, scope

Also id_token when decoded looks like:

{
  "exp": XXXXXXXXXX,
  "iat": XXXXXXXXXX,
  "auth_time": 0,
  "jti": "XXXXXXXXXX-XXXXXXXXXX",
  "iss": "http://{host}:{port}/auth/realms/dev",
  "aud": "grafana",
  "sub": "XXXXXXXXXX-XXXXXXXXXX",
  "typ": "ID",
  "azp": "grafana",
  "session_state": "XXXXXXXXXX-XXXXXXXXXX",
  "at_hash": "XXXXXXXXXX",
  "acr": "1",
  "email_verified": false,
  "roles": [
    "uma_protection",
    "admin"
  ],
  "name": "admin",
  "preferred_username": "admin",
  "given_name": "abcd",
  "family_name": "admin",
  "email": "XXXXXXXXXX@XXXXXXXXXX.com"
}

Following the OAuth implementation for Grafana it looks like id_token is also considered for user info - https://github.com/grafana/grafana/blob/main/pkg/login/social/generic_oauth.go

But eventually jmespath.Search is not able to find the correct value from the role attribute path provided from the token payload. This was cross verified jmespath.org with path and payload and correct results were obtained.

Please help me out here. I am guessing the role attribute path needs some changes as it is passed as an environment variable to Grafana.

-- Logs from Grafana (debug level) --

t=2021-08-11T11:28:00+0000 lvl=dbug msg="OAuthLogin Got token" logger=oauth token="&{AccessToken:{access_token} TokenType:Bearer RefreshToken:{refresh_token} Expiry:2021-08-11 11:33:00.669137197 +0000 UTC m=+421.124688977 raw:map[access_token:{access_token} expires_in:300 id_token:{id_token} not-before-policy:1.628681253e+09 refresh_expires_in:1800 refresh_token:{refresh_token} scope:openid email profile session_state:46f30922-8ec9-40c7-bb00-5f75106b1f35 token_type:Bearer]}"
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Getting user info" logger=oauth.generic_oauth
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Extracting user info from OAuth token" logger=oauth.generic_oauth
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Received id_token" logger=oauth.generic_oauth raw_json="{\"exp\":1628681580,\"iat\":1628681280,\"auth_time\":1628681279,\"jti\":\"b65c1320-c594-4705-a6e4-2b9c6e3f9703\",\"iss\":\"http://10.224.92.202:8052/auth/realms/dev\",\"aud\":\"grafana\",\"sub\":\"3b0c95d5-14ca-4b5e-aa61-3759fd5ee2aa\",\"typ\":\"ID\",\"azp\":\"grafana\",\"session_state\":\"46f30922-8ec9-40c7-bb00-5f75106b1f35\",\"at_hash\":\"c7xrebZq-3lgJUQi0wX0HA\",\"acr\":\"1\",\"email_verified\":false,\"roles\":[\"admin\"],\"name\":\"admin\",\"preferred_username\":\"admin\",\"given_name\":\"abcd\",\"family_name\":\"admin\",\"email\":\"XXXXXXX@XXXXXXX.com\"}" data="Name: admin, Displayname: , Login: , Username: , Email: XXXXXXX@XXXXXXX.com, Upn: , Attributes: map[]"
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Getting user info from API" logger=oauth.generic_oauth
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Error getting user info from API" logger=oauth.generic_oauth url=http://keycloak1:8080/auth/realms/dev/protocol/openid-connect/userinfo error="{\"error\":\"invalid_token\",\"error_description\":\"Token verification failed\"}"
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Processing external user info" logger=oauth.generic_oauth source=token data="Name: admin, Displayname: , Login: , Username: , Email: XXXXXXX@XXXXXXX.com, Upn: , Attributes: map[]"
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Setting user info name from name field" logger=oauth.generic_oauth
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Set user info email from extracted email" logger=oauth.generic_oauth email=manish.agrawal@honeywell.com
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Defaulting to using email for user info login" logger=oauth.generic_oauth email=manish.agrawal@honeywell.com
t=2021-08-11T11:28:00+0000 lvl=dbug msg="User info result" logger=oauth.generic_oauth result="&{Id: Name: admin Email:XXXXXXX@XXXXXXX.com Login:XXXXXXX@XXXXXXX.com Company: Role: Groups:[]}"
t=2021-08-11T11:28:00+0000 lvl=dbug msg="OAuthLogin got user info" logger=oauth userInfo="&{Id: Name:admin Email:XXXXXXX@XXXXXXX.com Login:XXXXXXX@XXXXXXX.com Company: Role: Groups:[]}"
t=2021-08-11T11:28:00+0000 lvl=dbug msg="Building external user info from OAuth user info" logger=oauth

2 Answers2

0

What worked for me was to:

  1. Enable debug logs in grafana (so that you can see content of Oauth replies in grafana logs)
  2. go to Client Scopes > roles > Mappers > client roles Check “Add to ID token”
  3. After that expression like
role_attribute_path = contains(realm_access.roles[*], 'my-custom-admin') && 'Admin' || contains(realm_access.roles[*], 'editor') && 'Editor' || 'Viewer'

began to work for me.

'realm_access' is just an example here, probably you will use some other.

And yes - it works in community version of Grafana.

Piotr
  • 317
  • 1
  • 13
0

You need to create a mapper. For the keycloak version +17.x.x you can do this:

  1. Go to clients and select your client.
  2. Go to Client scopes and click in the one that is called <your-client-name>-dedicated.
  3. Click on Add mapper, then select From predefined mappers.
  4. Create the realm roles mapper with the default configuration.

If you have followed the previous steps correctly you should see something like this:

Final config

Then go to Clients select your client and go to Client scopes > Evaluate > Generated user info. You should see your user roles within the realm_access.roles field of the generated json.

final userinfo.

Now, Grafana can access your realm roles without any problem.

PD: Remember that you need to set the role_attribute_path. For instance:

GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH="contains(realm_access.roles[*], 'system_administrator') && 'Admin' || contains(realm_access.roles[*], 'tenant_administrator') && 'Editor' || 'Viewer'" \

Where:

  1. realm_access.roles is the path to the role info in the previous json. You can set this up where making the mapper.
  2. 'system_administrator' and 'tenant_administrator' are realm roles in keycloak. You need to create them previously in keycloak.
  3. Admin, Editor, and Viewer are the roles in grafana.
A.Casanova
  • 555
  • 4
  • 16