OIDC discovery
WIPExpose /.well-known/openid-configuration + /.well-known/jwks.json so downstream services treat your auth issuer as an OIDC OP.
Need a full OP (
/authorize,/token,/userinfo,/introspect,/revoke, RFC 7591 dynamic client registration)? See OIDC OP. This page covers the discovery doc + JWKS helper only.
Why OIDC discovery?
When your AuthEngine issues JWTs (via JwtTransport), downstream
services may want to verify those JWTs locally without calling
your auth service. OIDC discovery is the standard way to publish:
- The list of supported algorithms, scopes, claims.
- The JWKS endpoint where verify keys live.
- The token endpoint where refreshes happen.
duck-auth ships an OIDC discovery builder and a JWKS handler. Downstream
RPs (resource providers) point an OIDC client library at your
/.well-known/openid-configuration URL, and verification works
automatically.
Building the discovery document
import { authBuildOidcDiscovery } from '@gentleduck/auth/oidc'
const discovery = authBuildOidcDiscovery({
issuer: 'https://api.example.com',
prefix: '/auth', // default
jwksPath: '/.well-known/jwks.json', // default
signingAlgs: ['ES256'], // must match JwtTransport
scopesSupported: ['openid', 'profile', 'email'],
extraClaims: {
grant_types_supported: ['authorization_code', 'refresh_token'],
response_types_supported: ['code'],
},
})
The result is a plain object. Serve it as JSON at the well-known URL:
// Express
app.get('/.well-known/openid-configuration', (req, res) => {
res.json(discovery)
})
// Hono
app.get('/.well-known/openid-configuration', (c) => c.json(discovery))
// Next.js
export function GET() { return Response.json(discovery) }
JWKS endpoint
For asymmetric JWTs (ES256, RS256, EdDSA), expose the public
verify keys at /.well-known/jwks.json:
import { buildJwksHandler } from '@gentleduck/auth/oidc'
const jwks = buildJwksHandler(transport) // JwtTransport instance
// Express
app.get('/.well-known/jwks.json', async (req, res) => {
res.json(await jwks(req))
})
// Hono
app.get('/.well-known/jwks.json', async (c) => c.json(await jwks(c.req.raw)))
The handler reads transport.verifyKeys and emits a spec-compliant
JWKS document - one kid per key, with the correct alg, kty, and
key material (x / y for EC, n / e for RSA, x for Ed25519).
For HS256 (symmetric), the JWKS endpoint is not safe to publish -
the secret is the verify key, and exposing it lets anyone forge JWTs.
The handler returns an empty keys: [] array for HS256 to defend
against accidental publication.
Rotation behaviour
When you rotate keys (transport.rotateSignKey(...)), the JWKS document
auto-updates on the next request - no caching layer involved:
transport.rotateSignKey({ kid: 'kid-2026-06', key: newKey })
// Next call to GET /.well-known/jwks.json returns both kids.
// Later, after the longest exp has passed:
transport.retireVerifyKey('kid-2026-05')
// JWKS now returns only kid-2026-06.
For downstream RPs, set the OIDC client's JWKS-cache TTL to under your rotation overlap window so they refetch and find the new key in time.
Caching headers
OIDC RPs cache JWKS responses, often for hours. Be deliberate about cache headers:
app.get('/.well-known/jwks.json', async (req, res) => {
res.setHeader('cache-control', 'public, max-age=300, must-revalidate')
res.json(await jwks(req))
})
Short max-age (5-10 minutes) keeps the rotation overlap brief; long max-age reduces load on your auth service but extends the overlap window you need to maintain.
Tying it together
The minimal OIDC issuer:
import { JwtTransport } from '@gentleduck/auth/core/transport'
import { buildJwksHandler, authBuildOidcDiscovery } from '@gentleduck/auth/oidc'
const transport = new JwtTransport({
signKey: { kid: 'k1', key: signKey },
verifyKeys: [{ kid: 'k1', key: verifyKey }],
issuer: 'https://api.example.com',
ttlMs: 15 * 60 * 1000,
})
const discovery = authBuildOidcDiscovery({
issuer: 'https://api.example.com',
signingAlgs: ['ES256'],
})
const jwks = buildJwksHandler(transport)
app.get('/.well-known/openid-configuration', (req, res) => res.json(discovery))
app.get('/.well-known/jwks.json', async (req, res) => res.json(await jwks(req)))
Downstream services can now verify your JWTs by pointing an OIDC client
library at https://api.example.com/.well-known/openid-configuration.