@aginix/adonis-ally-oidc
v0.1.0
Published
OpenID Connect (OIDC) driver for AdonisJS Ally — discovery, ID-token verification, PKCE, nonce, and RP-initiated logout. Built for Keycloak and any OIDC provider.
Downloads
46
Maintainers
Readme
@aginix/adonis-ally-oidc
Generic OpenID Connect driver for AdonisJS Ally — discovery, ID-token verification, PKCE, nonce, and RP-initiated logout. Built for Keycloak and any OIDC-compliant provider.
@adonisjs/ally ships drivers for Google, GitHub, Discord, and friends, but no generic OpenID Connect driver. This package fills that gap: give it an issuer and it discovers the endpoints, verifies the id_token (signature + claims + nonce), runs PKCE, and exposes RP-initiated logout — all behind ally's familiar ally.use('...') API. Works with Keycloak (first-class), Auth0, Okta, Microsoft Entra ID, Zitadel, Google, and any spec-compliant provider.
Features
A bare OAuth2 driver can call a userinfo endpoint, but it never proves who issued the token. This driver does the OIDC-specific work:
| Feature | What it does |
|---|---|
| Discovery | issuer → .well-known/openid-configuration → endpoints + JWKS, resolved once at boot |
| ID-token verification | RS256/ES256/… signature via remote JWKS, plus iss, aud/azp, exp/iat/nbf (clock skew), and nonce — strict by default |
| PKCE | S256, on by default |
| Nonce | issued in an encrypted cookie, verified against the id_token (replay protection) |
| RP-Initiated Logout | redirect to the provider's end_session_endpoint with id_token_hint + post_logout_redirect_uri |
| Typed | ally.use('keycloak') is fully typed, including the returned user and tokens |
Requirements
- Node.js >= 24
@adonisjs/ally^6.3.0 and@adonisjs/core^7 (peer dependencies)
Installation
npm i @aginix/adonis-ally-oidc
node ace configure @aginix/adonis-ally-oidcInstall and configure @adonisjs/ally first, if you haven't:
node ace add @adonisjs/allynode ace configure adds env validations and prints the config snippet. Set your secrets:
# .env
OIDC_ISSUER=https://sso.example.com/realms/myrealm
OIDC_CLIENT_ID=my-app
OIDC_CLIENT_SECRET=super-secretConfigure the provider
Register an oidc(...) provider in config/ally.ts:
import env from '#start/env'
import { defineConfig } from '@adonisjs/ally'
import { oidc } from '@aginix/adonis-ally-oidc'
export default defineConfig({
keycloak: oidc({
issuer: env.get('OIDC_ISSUER'),
clientId: env.get('OIDC_CLIENT_ID'),
clientSecret: env.get('OIDC_CLIENT_SECRET'),
callbackUrl: 'http://localhost:3333/auth/keycloak/callback',
postLogoutRedirectUri: 'http://localhost:3333',
}),
})Discovery and the JWKS are resolved once at boot, so a wrong or unreachable issuer fails startup with a clear error.
Usage
import router from '@adonisjs/core/services/router'
// 1) Send the user to the provider
router.get('/auth/keycloak/redirect', ({ ally }) => {
return ally.use('keycloak').redirect()
})
// 2) Handle the callback
router.get('/auth/keycloak/callback', async ({ ally, auth, response, session }) => {
const kc = ally.use('keycloak')
if (kc.accessDenied()) return 'You cancelled the login'
if (kc.stateMisMatch()) return 'Request expired, retry'
if (kc.hasError()) return kc.getError()
const oidcUser = await kc.user()
// oidcUser.id -> the "sub" claim
// oidcUser.email, .name, .nickName (preferred_username)
// oidcUser.original -> full verified claims (e.g. Keycloak realm_access.roles)
// oidcUser.token.idToken -> raw id_token, store it for logout
// `externalId` is your own column; linking by the stable `sub` survives
// email changes. (The demo app links by email — either works.)
const user = await User.firstOrCreate(
{ externalId: oidcUser.id },
{ email: oidcUser.email!, fullName: oidcUser.name }
)
await auth.use('web').login(user)
session.put('id_token', oidcUser.token.idToken)
return response.redirect('/dashboard')
})
// 3) Log out locally and at the provider (RP-initiated logout)
router.get('/auth/keycloak/logout', async ({ ally, auth, session }) => {
const idTokenHint = session.get('id_token')
await auth.use('web').logout()
session.forget('id_token')
return ally.use('keycloak').logout({ idTokenHint })
})logout(...) redirects to the provider's end_session_endpoint. Register your postLogoutRedirectUri with the provider (Keycloak: client → Valid post logout redirect URIs). Keycloak v18+ requires the id_token_hint (or a registered post-logout URI + client_id, which this driver always sends).
How id-token verification works
On the callback, user() exchanges the code at the token endpoint and then verifies the returned id_token with jose: the signature against the provider's JWKS (asymmetric algorithms only — none and symmetric HS* are rejected), plus iss, aud/azp, exp/iat/nbf (with optional clock skew), and the nonce issued during the redirect. Identity is taken from these verified claims, never from an unverified source.
Example app
See apps/demo in the repo for a complete Inertia/React integration: a "Login with Keycloak" button, the callback wiring, and RP-initiated logout.
Keycloak notes
issuerishttps://{host}/realms/{realm}(Keycloak ≥ 17 — older builds use/auth/realms/{realm}).- Roles live in
realm_access.roles/resource_access.{client}.roleson the verified claims — read them fromoidcUser.original. - Map extra claims (picture, groups) with Keycloak client scopes / protocol mappers so they land in the id_token.
Configuration reference
| Option | Default | Description |
|---|---|---|
| clientId, clientSecret, callbackUrl | — | Standard OAuth2 client credentials |
| issuer | — | Issuer URL for discovery |
| scopes | ['openid','profile','email'] | Requested scopes |
| usePKCE | true | PKCE (S256) |
| postLogoutRedirectUri | — | Default redirect after logout |
| fetchUserInfo | false | Also call userinfo and merge (its sub must match the id_token) |
| clockTolerance | 0 | Skew tolerance for id_token time claims (seconds or '30s') |
| cookieName | 'oidc' | Cookie-name prefix (set per provider if you register several) |
| extraAuthParams | — | Extra static auth-request params, e.g. { prompt: 'login' } |
Escape hatches
- Skip discovery — pass
authorizeUrl+accessTokenUrl+jwksUri(+ optionaluserInfoUrl,endSessionEndpoint) explicitly. - Restrict / pin algorithms —
idTokenSigningAlgs: ['RS256']. - Disable verification —
verifyIdToken: falsedecodes the id_token without checking its signature. Only do this behind a fully trusted channel; it removes the package's core security guarantee.
Contributing
This package lives in the adonis-vulcan monorepo (pnpm, Node >= 24). Issues and PRs welcome.
pnpm install
pnpm --filter @aginix/adonis-ally-oidc test # lint + japa + coverage
pnpm --filter @aginix/adonis-ally-oidc buildLicense
MIT © Aginix. See LICENSE.md.
