OIDC (generic)
Generic OIDC AuthStrategy — verifies any OpenID-Connect IdP's JWTs via JWKS (Okta, Keycloak, Cognito, Azure AD, Google Workspace). Discovery or explicit JWKS URL; maps a claim → tenantId.
@voltro/plugin-auth-oidc is the generic OIDC adapter: an AuthStrategy (conforming to @voltro/protocol) for any IdP that publishes a standards-compliant OpenID Connect discovery document — Okta, Keycloak, Cognito, Azure AD, Google Workspace, and anything else OIDC-compliant that doesn't ship a first-party plugin. It verifies the IdP's JWTs against its JWKS (public-key verify — no client secret), maps a configured claim to the framework's tenantId, and composes with other strategies (your password cookie, an API key, another IdP). It performs no verification of its own — it adds the JWKS-discovery indirection and delegates the verify pipeline to the shared jwtBearerStrategy.
Install
pnpm add @voltro/plugin-auth-oidcWiring
Add oidcStrategy(...) to auth.strategies in your api's app.config.ts. The built-in signed-cookie password strategy always runs first; your strategies run after it, in order, until one matcheds (or one faileds).
// app.config.ts
import { oidcStrategy } from '@voltro/plugin-auth-oidc'
export default {
type: 'api' as const,
name: 'myApi',
store: 'postgres' as const,
auth: {
strategies: [
oidcStrategy({
id: 'okta', // stable id for logs + metadata.provider
issuer: 'https://acme.okta.com', // also the discovery base
audience: 'api://acme', // expected `aud`
tenantClaim: 'acme/tenant_id', // which claim carries the Voltro tenantId
// jwksUrl: 'https://acme.okta.com/oauth2/v1/keys', // skip discovery roundtrip
// discoveryUrl: '…', // non-standard discovery path
// defaultTenantId: 'public', // fallback when the claim is absent
// scopesFromClaims: (c) => String(c['scope'] ?? '').split(' ').filter(Boolean),
}),
],
},
}Two modes: pass jwksUrl for zero boot-time IO, or omit it and the strategy fetches jwks_uri from ${issuer}/.well-known/openid-configuration on the first request (cached per-URL for the process lifetime). Pass discoveryUrl if your provider hosts the document at a non-standard path.
Tenant resolution: the value at tenantClaim (default 'tenant') becomes tenantId; or supply tenantIdFromClaims(claims) to derive it (returning null is a failed verdict — the strategy owns the request but can't satisfy the tenant invariant). scopesFromClaims maps verified claims onto Subject.scopes so requireScope / hasScope gate handlers off the token with no second lookup.
To accept your own password sessions AND OIDC during a migration, build the chain explicitly with composeAuthStrategies (see auth strategies):
import { composeAuthStrategies } from '@voltro/protocol'
import { voltroPasswordStrategy } from '@voltro/plugin-auth'
import { oidcStrategy } from '@voltro/plugin-auth-oidc'
const resolve = composeAuthStrategies([
voltroPasswordStrategy(),
oidcStrategy({ id: 'keycloak', issuer: 'https://id.acme.com/realms/acme', tenantClaim: 'realm_access.tenant' }),
])Options
| Option | Default | Notes |
|---|---|---|
id |
— (required) | Stable id for logs + metadata.provider. |
issuer |
— (required) | Expected iss; discovery base. |
audience |
(off) | Expected aud. String or array. |
jwksUrl |
(discovery) | Explicit JWKS URL — skips discovery. |
discoveryUrl |
${issuer}/.well-known/openid-configuration |
Override for non-standard paths. |
algorithms |
['RS256'] |
Allowlist; extend with ES256 / PS256 / EdDSA. |
cookieName |
(off) | Cookie carrying the JWT; null disables cookie fallback. |
tenantClaim |
'tenant' |
Claim carrying the Voltro tenantId. |
tenantIdFromClaims |
(uses tenantClaim) |
Derive tenantId; null → failed. |
defaultTenantId |
(none) | Fallback when the claim is absent. |
scopesFromClaims |
(none) | Map claims → Subject.scopes. |
Environment variables
None read directly — pass issuer / audience / jwksUrl explicitly (typically from your own defineEnv-declared vars).
Security
Server-only; fails closed — a forged, expired, or wrong-audience token (or a discovery failure) yields failed, never matched. Asymmetric algorithms only (HS256 is rejected — a shared-secret HMAC over a public JWKS flow is a downgrade vector).