chore: crush git history - reborn from consolidation on 2026-03-10
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# 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
|
||||
let cachedToken: string | null = 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.
|
||||
Reference in New Issue
Block a user