# 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.



---

<!-- source: en/plugins/auth-oidc.md -->
## 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 `matched`s (or one `failed`s).

```ts
// 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](/docs/authentication/strategies)):

```ts
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).

## See also

- [External identity providers](/docs/authentication/external-idp)
- [Auth plugin](/docs/plugins/auth)
