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-oidc

Wiring

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; nullfailed.
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).

See also