Hello,
I am currently working on setting up OAuth in Grafana (version 9.5.2) using Keycloak as the OAuth provider. While I’ve managed to get the OAuth connection functioning correctly, I am encountering an issue with role mappings that I’m hoping someone might be able to assist with.
To develop, Grafana does not seem to correctly map the roles defined in Keycloak. The roles sent from Keycloak appear to be ignored or misinterpreted by Grafana.
To assist with troubleshooting, here is a snippet of the Grafana debug logs during a login attempt:
grafana-grafana-1 | logger=context userId=0 orgId=0 uname= t=2023-06-07T08:58:35.009466945Z level=info msg="Request Completed" method=GET path=/login/generic_oauth status=302 remote_addr=xx.xx.xx.xx time_ms=0 duration=781.544µs size=318 referer=https://grafana.xxxxxx.xx/ handler=/login/:name
grafana-grafana-1 | logger=oauth t=2023-06-07T08:58:35.115351675Z level=info msg="state check" queryState=xxxxxx cookieState=xxxxxx
grafana-grafana-1 | logger=oauth t=2023-06-07T08:58:35.171689141Z level=debug msg="OAuthLogin: got token" expiry="2023-06-07 09:03:35.171594096 +0000 UTC m=+307.036077925" type=Bearer has_refresh_token=true
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.171737092Z level=debug msg="Getting user info"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.171744537Z level=debug msg="Extracting user info from OAuth token"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.17214966Z level=debug msg="Received id_token" raw_json="{\"exp\":1686128615,\"iat\":1686128315,\"auth_time\":1686123852,\"jti\":\"xxxxxx\",\"iss\":\"https://sso.xxxxxx.xx/realms/xxxxxx\",\"aud\":\"grafana-oauth\",\"sub\":\"xxxxxx\",\"typ\":\"ID\",\"azp\":\"grafana-oauth\",\"session_state\":\"xxxxxx\",\"at_hash\":\"xxxxxx\",\"acr\":\"0\",\"sid\":\"xxxxxx\",\"resource_access\":{\"realm-management\":{\"roles\":[\"view-realm\",\"view-identity-providers\",\"manage-identity-providers\",\"impersonation\",\"realm-admin\",\"create-client\",\"manage-users\",\"query-realms\",\"view-authorization\",\"query-clients\",\"query-users\",\"manage-events\",\"manage-realm\",\"view-events\",\"view-users\",\"view-clients\",\"manage-authorization\",\"manage-clients\",\"query-groups\"]},\"grafana-oauth\":{\"roles\":[\"editor\",\"admin\",\"grafanaadmin\"]},\"dumps-manager\":{\"roles\":[\"uma_protection\"]},\"gitlab\":{\"roles\":[\"uma_protection\"]},\"broker\":{\"roles\":[\"read-token\"]},\"account\":{\"roles\":[\"manage-account\",\"view-applications\",\"view-consent\",\"manage-account-links\",\"manage-consent\",\"delete-account\",\"view-profile\"]}},\"email_verified\":true,\"name\":\"User xxxxxx\",\"groups\":[\"/sysadmin\",\"/xxxxxx-account\",\"/cloud/agenda\",\"/developer/erp\"],\"preferred_username\":\"user.xxxxxx\",\"given_name\":\"User\",\"family_name\":\"xxxxxx\",\"email\":\"user.xxxxxx@xxxxxx.xx\"}" data="Name: User xxxxxx, Displayname: , Login: , Username: , Email: user.xxxxxx@xxxxxx.xx, Upn: , Attributes: map[]"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.172185686Z level=debug msg="Getting user info from API"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.194309866Z level=debug msg="HTTP GET" url=https://sso.xxxxxx.xx/realms/xxxxxx/protocol/openid-connect/userinfo status="200 OK" response_body="{\"sub\":\"xxxxxx\",\"email_verified\":true,\"name\":\"User xxxxxx\",\"groups\":[\"/sysadmin\",\"/xxxxxx-account\",\"/cloud/agenda\",\"/developer/erp\"],\"preferred_username\":\"user.xxxxxx\",\"given_name\":\"User\",\"family_name\":\"xxxxxx\",\"email\":\"user.xxxxxx@xxxxxx.xx\"}"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.194405062Z level=debug msg="Received user info response from API" raw_json="{\"sub\":\"xxxxxx\",\"email_verified\":true,\"name\":\"User xxxxxx\",\"groups\":[\"/sysadmin\",\"/xxxxxx-account\",\"/cloud/agenda\",\"/developer/erp\"],\"preferred_username\":\"user.xxxxxx\",\"given_name\":\"User\",\"family_name\":\"xxxxxx\",\"email\":\"user.xxxxxx@xxxxxx.xx\"}" data="Name: User xxxxxx, Displayname: , Login: , Username: , Email: user.xxxxxx@xxxxxx.xx, Upn: , Attributes: map[]"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.19441919Z level=debug msg="Processing external user info" source=token data="Name: User xxxxxx, Displayname: , Login: , Username: , Email: user.xxxxxx@xxxxxx.xx, Upn: , Attributes: map[]"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.194534307Z level=debug msg="Setting user info name from name field"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.194541042Z level=debug msg="Searching for login among JSON" loginAttributePath=username
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.194613856Z level=debug msg="Set user info email from extracted email" email=user.xxxxxx@xxxxxx.xx
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.194907555Z level=debug msg="RoleAttributeStrict is set, returning no role."
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.19493061Z level=debug msg="Processing external user info" source=API data="Name: User xxxxxx, Displayname: , Login: , Username: , Email: user.xxxxxx@xxxxxx.xx, Upn: , Attributes: map[]"
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.194943653Z level=debug msg="Searching for login among JSON" loginAttributePath=username
grafana-grafana-1 | logger=oauth.generic_oauth t=2023-06-07T08:58:35.195141596Z level=debug msg="RoleAttributeStrict is set, returning no role."
grafana-grafana-1 | logger=context userId=0 orgId=0 uname= t=2023-06-07T08:58:35.195172363Z level=warn msg="Integration requires a valid org role assigned in idP. Assigned role: \" \""
grafana-grafana-1 | logger=context userId=0 orgId=0 uname= t=2023-06-07T08:58:35.215187859Z level=info msg="Request Completed" method=GET path=/login/generic_oauth status=302 remote_addr=xx.xx.xx.xx time_ms=100 duration=100.279138ms size=29 referer=https://grafana.xxxxxx.xx/ handler=/login/:name
As you can see, the ID token (in JSON format) sent by Keycloak clearly includes the correct roles.
"resource_access": {
...
"grafana-oauth": {
"roles": [
"editor",
"admin",
"grafanaadmin"
]
},
...
}
The path seems to be : resource_access.grafana-oauth.roles
Here is my Grafana OAuth configuration:
[log]
level=debug
[server]
root_url=https://grafana.xxxxxx.xx
[auth.generic_oauth]
enabled = true
name = xxxxxx
allow_sign_up = true
client_id = grafana-oauth
client_secret = xxxxxx
scopes = openid email profile offline_access roles
email_attribute_path = email
login_attribute_path = username
name_attribute_path = full_name
auth_url = https://sso.xxxxxx.xx/realms/xxxxxx/protocol/openid-connect/auth
token_url = https://sso.xxxxxx.xx/realms/xxxxxx/protocol/openid-connect/token
api_url = https://sso.xxxxxx.xx/realms/xxxxxx/protocol/openid-connect/userinfo
allow_assign_grafana_admin = true
role_attribute_strict = true
role_attribute_path = contains(resource_access.grafana-oauth.roles[*], 'admin') && 'Admin' || contains(resource_access.grafana-oauth.roles[*], 'editor') && 'Editor' || 'Viewer'
I also tried with some variants like :
role_attribute_path = contains(roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'
I’ve gone through Grafana and Keycloak’s documentation to try and resolve this but to no avail. I would greatly appreciate any insights or suggestions you might have on this matter.
Thank you in advance for your assistance.