Grafana signs people in through its generic OAuth provider, which works with any OpenID
Connect issuer. With Nauthera the client is an OIDCClient in Grafana's namespace: the
server generates the client secret, the operator writes it into a Secret there, and Grafana reads
it from an environment variable, so the secret never appears in a values file. Roles map
by email, since Nauthera emits no groups claim.
Checked against: Grafana 13.3.0, the latest release on November 4, 2026, its
generic OAuth docs
and configuration docs
at that tag, and the Grafana Helm chart 14.3.1 in
grafana-community/helm-charts,
all read on November 4, 2026, and the OIDCClient definition in nauthera-server. This
page has not recorded an end-to-end test against a running Grafana. To try it locally,
start Nauthera with the Quick Start and add the client below.
1. The OIDCClient
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
name: grafana
namespace: grafana
spec:
clientId: grafana
displayName: Grafana
redirectUris:
- https://grafana.example.com/login/generic_oauth
postLogoutRedirectUris:
- https://grafana.example.com/login
grantTypes: [authorization_code, refresh_token]
responseTypes: [code]
scopes: [openid, profile, email, offline_access]
tokenEndpointAuthMethod: client_secret_basic
pkcePolicy: required_s256
allowRefresh: true
deletionPolicy: Retain
secret:
generate: true
secretName: grafana-oidcThe grafana namespace has to be admitted by an organization: for the main one, list it
in the chart value operator.. The operator then writes the
Secret grafana-oidc with the keys client-id, client-secret and issuer. Those key
names are not environment variable names Grafana reads, so the next step maps them.
2. Grafana's Helm values
grafana.ini:
server:
root_url: https://grafana.example.com
auth.generic_oauth:
enabled: true
name: Nauthera
client_id: grafana
scopes: openid profile email offline_access
auth_url: https://auth.example.com/authorize
token_url: https://auth.example.com/token
api_url: https://auth.example.com/userinfo
use_pkce: true
use_refresh_token: true
login_attribute_path: preferred_username
email_attribute_path: email
name_attribute_path: name
role_attribute_path: "email_verified && contains(['alice@example.com'], email) && 'Admin' || 'Viewer'"
signout_redirect_url: https://auth.example.com/end_session?client_id=grafana&post_logout_redirect_uri=https%3A%2F%2Fgrafana.example.com%2Flogin
envValueFrom:
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET:
secretKeyRef:
name: grafana-oidc
key: client-secret- The secret arrives as
GF_, which overrides the same setting inAUTH_ GENERIC_ OAUTH_ CLIENT_ SECRET grafana.ini. The chart rendersenvValueFromas the container'svalueFrom, so asecretKeyRefworks there. Leaveclient_secretout ofgrafana.ini: the chart'sassertNoLeakedSecretscheck, on by default, fails the render when it is set to a value. root_urldecides the redirect URI Grafana sends,/login/generic_oauthunder it, which has to match the registered one exactly.use_pkceis needed: the client'spkcePolicyisrequired_s256.use_refresh_tokenwithoffline_accesslets Grafana renew access tokens, which Nauthera issues for 10 minutes by default.- The endpoints are the issuer's: for an organization other than the main one, put
/orgs/<slug>after the host in all three. tls_client_canames a CA file to trust when Nauthera's certificate is not from a public CA, as in a closed network. Nauthera's Compose stack sets it for its local certificate.
Without the Helm chart, the same settings go in grafana.ini, with the Secret mounted as
a file and read through Grafana's variable expansion:
[server]
root_url = https://grafana.example.com
[auth.generic_oauth]
enabled = true
name = Nauthera
client_id = grafana
client_secret = $__file{/etc/secrets/nauthera/client-secret}
scopes = openid profile email offline_access
auth_url = https://auth.example.com/authorize
token_url = https://auth.example.com/token
api_url = https://auth.example.com/userinfo
use_pkce = true
use_refresh_token = true
role_attribute_path = email_verified && contains(['alice@example.com'], email) && 'Admin' || 'Viewer'3. Roles by email
Nauthera puts email and email_verified in the ID token when the email scope is
granted, and Grafana evaluates role_attribute_path against the ID token first, then the
userinfo response, then the access token. The expression above makes Alice an Admin only
while her address is verified, and everyone else a Viewer. Add addresses to the list, or
chain more &&/|| pairs for an Editor tier. For people from organizations brokered by
yours, the ID token carries member_org_slug, and Grafana's org_attribute_path can map
each member organization to a Grafana organization of its own; the
identity broker docs
show the expression.
4. Sign-out
signout_redirect_url sends the browser to Nauthera's end_session endpoint, which ends
the Nauthera session and returns to /login, the URI the client registers in
postLogoutRedirectUris. Because the URL carries post_logout_redirect_uri, Grafana
adds the user's ID token as id_token_hint while its OAuth token is still valid
(logout code).
Without a valid ID token hint, Nauthera asks the user to confirm first; see
Sessions, Consent & Logout.
When something goes wrong
| What you see | Why | Fix |
|---|---|---|
helm template fails on auth. | assertNoLeakedSecrets found the secret in grafana.ini | Remove it there; use envValueFrom |
Nauthera refuses the redirect_uri | root_url differs from the registered host | Set server.root_url to the address in the browser |
Nauthera answers invalid_scope | Grafana asks for a scope the client is not registered for | Make scopes a subset of the client's spec.scopes |
invalid_client after the Secret was deleted | The operator rotated the secret; the running pod still has the old one | Restart Grafana's pods; environment variables are read at start |
| Everyone is a Viewer | role_attribute_path matched nobody | Check the email scope is granted and the address verified |
| Sign-in fails on the way back with a state error | Grafana's state cookie did not survive the return from another site | Run both on the same site; Nauthera's Compose stack, whose Grafana is on http://localhost:3000, sets cookie_samesite = none with cookie_secure = true |
The OIDCClient shows NamespaceNotAdmitted | No organization admits grafana | Admit the namespace (step 1) |
Grafana's own docs ask for cookie_samesite = lax, its default, when signing in with
OAuth, so change it only for the cross-site case above.