Repository navigation
OIDC
OpenID Connect authorization code flow with PKCE and nonce against any IdP (Keycloak, Google, ...), RP-initiated and front-channel logout, optional enrichment from the Keycloak Admin API. One oidc.Provider per IdP, all on one session.Store.
From examples/oidc/main.go (tested against the in-repo fake issuer, with and without Keycloak):
var onLogin auth.Hook
if options.Keycloak != nil {
enricher, err := keycloak.New(options.Keycloak)
if err != nil {
return nil, err
}
onLogin = enricher.Enrich // replaces OIDCRoles, OIDCClientRoles, Groups and Attributes; 403 for missing Required* roles
}
provider, err := oidc.New(ctx, &oidc.Options{
Name: "sso", // Identity.Provider and the route prefix /auth/oidc/sso
Issuer: options.Issuer,
ClientID: options.ClientID,
ClientSecret: options.ClientSecret,
ExternalURL: options.ExternalURL,
Sessions: store,
OnLogin: onLogin,
})
if err != nil {
return nil, err
}
guard, err := session.NewGuard(&session.GuardOptions{Store: store, LoginURL: provider.LoginURL()})
if err != nil {
return nil, err
}
serveMux := http.NewServeMux()
auth.RegisterRoutes(serveMux, provider.Routes()) // GET /auth/oidc/sso/{login,callback,logout,frontchannel-logout}oidc.New runs discovery with ctx and fails fast when the IdP is unreachable. Register ExternalURL + PathPrefix + "/callback" (here https://app.example.com/auth/oidc/sso/callback) as redirect URI at the IdP.
Prefix PathPrefix, default /auth/oidc/<Name>. All GET.
| Path | Purpose |
|---|---|
/login |
starts the login, 302 to the IdP; ?return_to=<path> is where the user lands afterwards (the Guard's RequireUI sets it) |
/callback |
the IdP's redirect target; rotates the session, stores the identity, 302 to the return path |
/logout |
ends the local session and sends the browser to the IdP's end-session endpoint with the stored ID token as hint; without a stored ID token (the local session had already expired) straight to PostLogoutURL, and the IdP session stays; link to it from your UI |
/frontchannel-logout |
OIDC front-channel logout, loaded by the IdP in an iframe |
| Option | Default | Note |
|---|---|---|
Name |
required |
[a-z0-9-]{1,32}; becomes Identity.Provider; not password, github, apikey, session, jwt
|
Issuer |
required | https (http only on loopback); the endpoints discovery names must be https too |
ClientID, ClientSecret
|
required | confidential client |
ExternalURL |
required | public origin; the redirect URI derives from it |
Scopes |
openid profile email |
|
ClaimMapper |
oidc.DefaultClaimMapper |
oidc.GoogleClaimMapper for Google |
AllowedEmailDomains |
none | requires email_verified; ASCII domains, an internationalized one in its xn-- form; with oidc.GoogleIssuer a listed domain other than gmail.com/googlemail.com also requires the hd claim to equal it (see Google below) |
AllowAnyAccount |
false |
with oidc.GoogleIssuer and neither AllowedEmailDomains nor OnLogin, oidc.New refuses to start; set it to admit every Google account on purpose |
PostLogoutURL |
ExternalURL + "/" |
path or absolute http(s) URL without fragment; a path is made absolute with ExternalURL
|
ErrorURL |
none (JSON answers) | every callback failure redirects to ErrorURL?code=<code> (code=internal for a server-side failure); same URL rules as PostLogoutURL
|
OnLogin, OnLogout
|
none |
OnLogin is where you provision your application user and set UserID
|
Without Keycloak, OIDCRoles (realm_access.roles), OIDCClientRoles (resource_access.<client>.roles) and Groups (groups) come from the access token, per claim from the ID token when the access token lacks it. Only a JWT access token signed by the issuer whose sub equals the verified ID token's sub counts; an opaque token, one that does not verify or one issued to another subject is ignored, and the claims come from the ID token alone.
keycloak.New(&keycloak.Options{...}) returns a client whose Enrich is an auth.Hook; set it as OnLogin (or call it first inside your own hook). It replaces OIDCRoles (composite realm roles), OIDCClientRoles (of ClientRolesClientID), Groups (paths) and Attributes with the Admin API's answer and refuses the login with 403 (the role name stays server-side) when RequiredRealmRoles, RequiredClientRoles or RequiredGroups are missing; RequiredClientRoles needs ClientRolesClientID. It authenticates with the client credentials grant of a service account (ClientID, ClientSecret), which needs the realm-management roles view-users, query-users and query-groups (it reads the user, its role mappings, groups and attributes) and, with ClientRolesClientID, view-clients and query-clients (it looks the client up by its client ID). The password grant (GrantType: keycloak.GrantTypePassword, client admin-cli by default) breaks as soon as the admin user gets an OTP credential. Tested excerpt: oidc/keycloak/example_test.go.
login_expired, state_mismatch (400), login_denied, login_failed (401) on the callback; forbidden (403) for a disallowed email domain, a missing required Keycloak role or group, an OnLogin refusal and a cross-site /logout; internal (500) when the IdP is down. With ErrorURL the same codes arrive as ?code=.
- The IdP rejecting the authorization code (an error answer below 500) is 401
login_failed; a 5xx answer or an IdP that cannot be reached is 500. - An ID token without
subis refused; one that does not verify is 401login_failedwith the fixed ReasonID token expiredorID token verificationon the outcome (an unreachable JWKS reads like an invalid token: go-oidc's message quotes the token and the server's text).
-
oidc.Newrefuses aSessionsstore whose cookie is SameSite Strict: the callback would arrive without it. - OIDC has no
Refresher: roles and groups are read at login and stay until the session ends (StoreOptions.MaximumAge, default 24 h). -
/logoutrefusesSec-Fetch-Site: cross-sitewith 403 and ends nothing: logout links must live on the app's own site. Front-channel logout from the IdP's iframe still works. - Front-channel logout needs
StoreOptions.SameSite: http.SameSiteNoneMode(https only), else the IdP's iframe gets no cookie. - No component follows a redirect of the issuer's endpoints: point
Issuerat the final URL. - Google:
Issuer: oidc.GoogleIssuer(https://accounts.google.com),ClaimMapper: oidc.GoogleClaimMapper, and an admission:AllowedEmailDomains, anOnLoginthat refuses strangers, orAllowAnyAccount: true;oidc.Newrefuses the Google issuer with none of them, because anyone can create a Google account. The rule keys on the issuer, not the claim mapper: a mapper only reads the profile. - Google sets
hdonly for Workspace (Cloud Identity) accounts, and a consumer Google account can carry a verified address of any domain (a former employee can create one with the company address and keep it after leaving the Workspace). So with the Google issuer every allowed domain exceptgmail.comandgooglemail.comalso requireshdto equal the email's domain; a consumer account with a company address is refused 403 (Reasonhosted domain mismatch). Admit such accounts throughOnLogininstead.
Source: oidc/ (provider.go, login.go, logout.go, claims.go), oidc/keycloak/keycloak.go.
Mirror of docs/wiki at v1.3.0 (486bfa3). Edit the repository, not this wiki.