# Authentication Patterns for APOPHIS APOPHIS generates requests automatically. For authenticated routes, you need to inject auth tokens, session cookies, or API keys into those requests. The cleanest way is via an auth extension. --- ## The Pattern: `createAuthExtension` Use `createAuthExtension` from `apophis-fastify` to inject credentials into every request: ```javascript import { createAuthExtension } from 'apophis-fastify' const jwtAuth = createAuthExtension({ name: 'jwt', getToken: async () => { const res = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ client_id: 'test', client_secret: 'secret' }), }) const { access_token } = await res.json() return access_token }, }) await fastify.register(apophis, { extensions: [jwtAuth] }) ``` `getToken` is called for every request. Return a token string; APOPHIS writes `${prefix}${token}` to `headerName`, defaulting to `authorization: Bearer `. --- ## JWT Bearer Token Standard OAuth 2.1 / OIDC pattern: ```javascript const jwtAuth = createAuthExtension({ name: 'jwt', getToken: async () => { // Fetch fresh token per request const { access_token } = await fetchToken() return access_token }, // Default: headerName='authorization', prefix='Bearer ' }) ``` --- ## API Key No prefix, custom header: ```javascript const apiKeyAuth = createAuthExtension({ name: 'apikey', getToken: () => { if (!process.env.API_KEY) throw new Error('API_KEY is required') return process.env.API_KEY }, headerName: 'x-api-key', prefix: '', }) ``` --- ## Session Cookie ```javascript const sessionAuth = createAuthExtension({ name: 'session', getToken: async () => { const cookie = await loginAndGetCookie() return cookie }, headerName: 'cookie', prefix: 'session=', }) ``` --- ## Conditional Auth (Skip Public Routes) Skip auth for health checks or public endpoints: ```javascript const auth = createAuthExtension({ name: 'conditional', getToken: () => 'token', matcher: (route) => !route.path.startsWith('/public/'), }) ``` Routes matching the matcher get the header. Others proceed unmodified. --- ## Multiple Auth Schemes Register multiple extensions. They run in order: ```javascript await fastify.register(apophis, { extensions: [ createAuthExtension({ name: 'jwt', getToken: fetchJwt }), // Authorization: Bearer ... createAuthExtension({ name: 'apikey', getToken: getApiKey, headerName: 'x-api-key', prefix: '' }), ] }) ``` --- ## Per-Route Auth Config Some routes need different validation (e.g., verify vs parse-only): ```javascript fastify.get('/wimse/wit', { schema: { 'x-category': 'observer', 'x-extension-config': { jwt: { verify: false, extractFrom: 'body' } }, 'x-ensures': [ 'jwt_claims(this).sub != null', 'jwt_claims(this).cnf.jwk != null' ] } }) ``` See `docs/protocol-extensions-spec.md` for full JWT extension configuration. --- ## Refresh Logic `getToken` runs per request. Handle refresh inline: ```javascript let cachedToken = null const auth = createAuthExtension({ name: 'jwt-with-refresh', getToken: async () => { if (cachedToken && !isExpired(cachedToken)) { return cachedToken } const { access_token } = await refreshToken() cachedToken = access_token return access_token }, }) ``` --- ## Testing Without Auth For routes that don't need auth, omit the extension or use a matcher: ```javascript // Only auth for /api/* routes const auth = createAuthExtension({ name: 'api-only', getToken: () => 'token', matcher: (route) => route.path.startsWith('/api/'), }) ``` --- ## Summary | Pattern | `headerName` | `prefix` | `matcher` | |---------|-------------|----------|-----------| | JWT Bearer | `authorization` (default) | `Bearer ` (default) | optional | | API Key | `x-api-key` | `''` | optional | | Session Cookie | `cookie` | `session=` | optional | | Conditional | any | any | required | The auth extension is the standard way to test authenticated routes in APOPHIS. It keeps auth logic out of your route handlers and tests, and centralizes it where it belongs.