2026-03-10 00:00:00 -07:00
# 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 <token>` .
---
## 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
2026-04-30 11:34:00 -07:00
let cachedToken = null
2026-03-10 00:00:00 -07:00
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.