|
1 | | -# fastify-api-key |
| 1 | +<h1 align="center">@fracabu/fastify-api-key</h1> |
| 2 | +<h3 align="center">Complete API Key authentication for Fastify</h3> |
2 | 3 |
|
3 | | -[](https://www.npmjs.com/package/fastify-api-key) |
4 | | -[](https://github.com/fracabu/fastify-api-key/actions/workflows/ci.yml) |
5 | | -[](https://codecov.io/gh/fracabu/fastify-api-key) |
6 | | -[](https://opensource.org/licenses/MIT) |
| 4 | +<p align="center"> |
| 5 | + <em>Scopes, multiple sources, and TypeScript support</em> |
| 6 | +</p> |
7 | 7 |
|
8 | | -Complete API Key authentication for Fastify with scopes, multiple sources, and TypeScript support. |
| 8 | +<p align="center"> |
| 9 | + <a href="https://www.npmjs.com/package/@fracabu/fastify-api-key"><img src="https://img.shields.io/npm/v/@fracabu/fastify-api-key.svg" alt="npm version" /></a> |
| 10 | + <img src="https://github.com/fracabu/fastify-api-key/actions/workflows/ci.yml/badge.svg" alt="CI" /> |
| 11 | + <img src="https://img.shields.io/badge/Fastify-5.x-000000?style=flat-square&logo=fastify" alt="Fastify" /> |
| 12 | + <img src="https://img.shields.io/badge/TypeScript-Ready-blue.svg" alt="TypeScript" /> |
| 13 | +</p> |
9 | 14 |
|
10 | | -## Features |
| 15 | +<p align="center"> |
| 16 | + :gb: <a href="#english">English</a> | :it: <a href="#italiano">Italiano</a> |
| 17 | +</p> |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## Overview |
| 22 | + |
| 23 | +<!--  --> |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +<a name="english"></a> |
| 28 | +## :gb: English |
| 29 | + |
| 30 | +### Features |
11 | 31 |
|
12 | 32 | - **Fastify v5** support |
13 | 33 | - **TypeScript-first** with complete type definitions |
14 | 34 | - **Multiple extraction sources** (header, query, body, cookie) |
15 | | -- **Scopes/permissions system** with `scopes` (all required) and `anyScope` (at least one) |
| 35 | +- **Scopes/permissions system** |
16 | 36 | - **Rate limiting** information support |
17 | 37 | - **Timing-safe** key comparison (prevents timing attacks) |
18 | 38 | - **Custom error handlers** |
19 | | -- **Validation hooks** for logging/audit |
20 | 39 | - **ESM and CJS** dual module support |
21 | 40 |
|
22 | | -## Installation |
| 41 | +### Install |
23 | 42 |
|
24 | 43 | ```bash |
25 | 44 | npm install @fracabu/fastify-api-key |
26 | 45 | ``` |
27 | 46 |
|
28 | | -## Requirements |
29 | | - |
30 | | -- Node.js >= 20.0.0 |
31 | | -- Fastify >= 5.0.0 |
32 | | - |
33 | | -## Quick Start |
| 47 | +### Quick Start |
34 | 48 |
|
35 | 49 | ```typescript |
36 | | -import Fastify from 'fastify'; |
37 | | -import fastifyApiKey from '@fracabu/fastify-api-key'; |
| 50 | +import Fastify from 'fastify' |
| 51 | +import fastifyApiKey from '@fracabu/fastify-api-key' |
38 | 52 |
|
39 | | -const app = Fastify(); |
| 53 | +const app = Fastify() |
40 | 54 |
|
41 | | -// Register the plugin with a validation function |
42 | 55 | await app.register(fastifyApiKey, { |
43 | 56 | validate: async (key) => { |
44 | | - // Your validation logic (database lookup, etc.) |
45 | | - const apiKey = await db.apiKeys.findByKey(key); |
46 | | - |
47 | | - if (!apiKey) { |
48 | | - return { valid: false }; |
49 | | - } |
50 | | - |
51 | | - return { |
52 | | - valid: true, |
53 | | - scopes: apiKey.scopes, |
54 | | - metadata: { userId: apiKey.userId } |
55 | | - }; |
| 57 | + const apiKey = await db.apiKeys.findByKey(key) |
| 58 | + if (!apiKey) return { valid: false } |
| 59 | + return { valid: true, scopes: apiKey.scopes } |
56 | 60 | } |
57 | | -}); |
| 61 | +}) |
58 | 62 |
|
59 | 63 | // Protected route |
60 | 64 | app.get('/api/users', { |
61 | 65 | preHandler: app.apiKey() |
62 | 66 | }, async (request) => { |
63 | | - console.log('Scopes:', request.apiKeyScopes); |
64 | | - console.log('Metadata:', request.apiKey?.metadata); |
65 | | - return { users: [] }; |
66 | | -}); |
| 67 | + return { users: [] } |
| 68 | +}) |
67 | 69 |
|
68 | | -// Route with required scopes (all must be present) |
| 70 | +// Route with required scopes |
69 | 71 | app.delete('/api/users/:id', { |
70 | 72 | preHandler: app.apiKey({ scopes: ['admin', 'users:delete'] }) |
71 | | -}, async () => { |
72 | | - return { deleted: true }; |
73 | | -}); |
74 | | - |
75 | | -// Route with anyScope (at least one must be present) |
76 | | -app.get('/api/reports', { |
77 | | - preHandler: app.apiKey({ anyScope: ['reports:read', 'admin'] }) |
78 | | -}, async () => { |
79 | | - return { reports: [] }; |
80 | | -}); |
81 | | - |
82 | | -await app.listen({ port: 3000 }); |
| 73 | +}, handler) |
83 | 74 | ``` |
84 | 75 |
|
85 | | -## API Reference |
86 | | - |
87 | | -### Plugin Options |
88 | | - |
89 | | -| Option | Type | Default | Description | |
90 | | -|--------|------|---------|-------------| |
91 | | -| `validate` | `ApiKeyValidator` | **required** | Validation function | |
92 | | -| `sources` | `ApiKeySource[]` | `[{ type: 'header', name: 'X-API-Key' }]` | Key extraction sources | |
93 | | -| `errorHandler` | `ApiKeyErrorHandler` | `undefined` | Custom error handler | |
94 | | -| `decoratorName` | `string` | `'apiKey'` | Request decorator name | |
95 | | -| `allowAnonymous` | `boolean` | `false` | Allow unauthenticated requests | |
96 | | -| `onValidation` | `ApiKeyHook` | `undefined` | Post-validation hook | |
97 | | -| `timingSafe` | `boolean` | `true` | Use timing-safe comparison | |
98 | | - |
99 | | -### Validation Function |
100 | | - |
101 | | -The `validate` function receives the API key and request, and should return a validation result: |
| 76 | +### Utilities |
102 | 77 |
|
103 | 78 | ```typescript |
104 | | -interface ApiKeyValidationResult { |
105 | | - valid: boolean; |
106 | | - scopes?: string[]; |
107 | | - rateLimit?: { |
108 | | - limit: number; |
109 | | - remaining: number; |
110 | | - reset: number; |
111 | | - }; |
112 | | - metadata?: Record<string, unknown>; |
113 | | - errorMessage?: string; |
114 | | -} |
115 | | -``` |
116 | | - |
117 | | -### Guard Options |
| 79 | +import { generateApiKey, timingSafeCompare } from '@fracabu/fastify-api-key' |
118 | 80 |
|
119 | | -```typescript |
120 | | -app.apiKey({ |
121 | | - scopes: ['read', 'write'], // All required |
122 | | - anyScope: ['admin', 'superuser'], // At least one required |
123 | | - allowAnonymous: false |
124 | | -}) |
| 81 | +const key = generateApiKey({ prefix: 'myapp', length: 32 }) |
| 82 | +// => 'myapp_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345' |
125 | 83 | ``` |
126 | 84 |
|
127 | | -## Examples |
| 85 | +--- |
128 | 86 |
|
129 | | -### Multiple Sources |
| 87 | +<a name="italiano"></a> |
| 88 | +## :it: Italiano |
130 | 89 |
|
131 | | -Extract API key from multiple locations with priority: |
| 90 | +### Funzionalita |
132 | 91 |
|
133 | | -```typescript |
134 | | -await app.register(fastifyApiKey, { |
135 | | - sources: [ |
136 | | - { type: 'header', name: 'X-API-Key' }, |
137 | | - { type: 'header', name: 'Authorization', prefix: 'ApiKey ' }, |
138 | | - { type: 'query', name: 'api_key' } |
139 | | - ], |
140 | | - validate: async (key) => { |
141 | | - // ... |
142 | | - } |
143 | | -}); |
144 | | -``` |
145 | | - |
146 | | -### Rate Limiting Information |
| 92 | +- Supporto **Fastify v5** |
| 93 | +- **TypeScript-first** con definizioni di tipo complete |
| 94 | +- **Sorgenti di estrazione multiple** (header, query, body, cookie) |
| 95 | +- **Sistema scopes/permessi** |
| 96 | +- Supporto informazioni **rate limiting** |
| 97 | +- Confronto chiavi **timing-safe** (previene timing attacks) |
| 98 | +- **Error handler personalizzati** |
| 99 | +- Supporto modulo duale **ESM e CJS** |
147 | 100 |
|
148 | | -Return rate limit info from your validator: |
| 101 | +### Installazione |
149 | 102 |
|
150 | | -```typescript |
151 | | -await app.register(fastifyApiKey, { |
152 | | - validate: async (key) => { |
153 | | - const keyData = await db.apiKeys.findByKey(key); |
154 | | - const usage = await rateLimiter.getUsage(key); |
155 | | - |
156 | | - return { |
157 | | - valid: true, |
158 | | - scopes: keyData.scopes, |
159 | | - rateLimit: { |
160 | | - limit: keyData.rateLimit, |
161 | | - remaining: keyData.rateLimit - usage.count, |
162 | | - reset: usage.resetAt |
163 | | - } |
164 | | - }; |
165 | | - } |
166 | | -}); |
167 | | - |
168 | | -// Add rate limit headers |
169 | | -app.addHook('onSend', (request, reply, _payload, done) => { |
170 | | - if (request.apiKey?.rateLimit) { |
171 | | - const { limit, remaining, reset } = request.apiKey.rateLimit; |
172 | | - reply.header('X-RateLimit-Limit', limit); |
173 | | - reply.header('X-RateLimit-Remaining', remaining); |
174 | | - reply.header('X-RateLimit-Reset', reset); |
175 | | - } |
176 | | - done(); |
177 | | -}); |
| 103 | +```bash |
| 104 | +npm install @fracabu/fastify-api-key |
178 | 105 | ``` |
179 | 106 |
|
180 | | -### Custom Error Handler |
| 107 | +### Quick Start |
181 | 108 |
|
182 | 109 | ```typescript |
183 | | -await app.register(fastifyApiKey, { |
184 | | - validate: async (key) => { /* ... */ }, |
185 | | - errorHandler: async (error, request, reply) => { |
186 | | - request.log.warn({ err: error }, 'API key validation failed'); |
187 | | - |
188 | | - await reply.status(error.statusCode).send({ |
189 | | - error: error.code, |
190 | | - message: error.message, |
191 | | - timestamp: new Date().toISOString() |
192 | | - }); |
193 | | - } |
194 | | -}); |
195 | | -``` |
| 110 | +import Fastify from 'fastify' |
| 111 | +import fastifyApiKey from '@fracabu/fastify-api-key' |
196 | 112 |
|
197 | | -### Validation Hook for Audit Logging |
| 113 | +const app = Fastify() |
198 | 114 |
|
199 | | -```typescript |
200 | 115 | await app.register(fastifyApiKey, { |
201 | | - validate: async (key) => { /* ... */ }, |
202 | | - onValidation: async (key, result, request) => { |
203 | | - await auditLog.record({ |
204 | | - timestamp: new Date(), |
205 | | - ip: request.ip, |
206 | | - path: request.url, |
207 | | - method: request.method, |
208 | | - apiKeyPrefix: key.substring(0, 10) + '...', |
209 | | - success: result.valid, |
210 | | - scopes: result.scopes |
211 | | - }); |
| 116 | + validate: async (key) => { |
| 117 | + const apiKey = await db.apiKeys.findByKey(key) |
| 118 | + if (!apiKey) return { valid: false } |
| 119 | + return { valid: true, scopes: apiKey.scopes } |
212 | 120 | } |
213 | | -}); |
214 | | -``` |
215 | | - |
216 | | -### Optional Authentication |
217 | | - |
218 | | -```typescript |
219 | | -// Global anonymous access |
220 | | -await app.register(fastifyApiKey, { |
221 | | - allowAnonymous: true, |
222 | | - validate: async (key) => { /* ... */ } |
223 | | -}); |
| 121 | +}) |
224 | 122 |
|
225 | | -// Route works with or without API key |
226 | | -app.get('/api/posts', { |
| 123 | +// Rotta protetta |
| 124 | +app.get('/api/users', { |
227 | 125 | preHandler: app.apiKey() |
228 | 126 | }, async (request) => { |
229 | | - if (request.apiKey) { |
230 | | - // Authenticated - show all posts |
231 | | - return { posts: await getAllPosts() }; |
232 | | - } |
233 | | - // Anonymous - show only public posts |
234 | | - return { posts: await getPublicPosts() }; |
235 | | -}); |
236 | | - |
237 | | -// This route still requires authentication |
238 | | -app.post('/api/posts', { |
239 | | - preHandler: app.apiKey({ allowAnonymous: false }) |
240 | | -}, async () => { |
241 | | - return { created: true }; |
242 | | -}); |
243 | | -``` |
244 | | - |
245 | | -## Exported Utilities |
246 | | - |
247 | | -The package also exports utility functions: |
248 | | - |
249 | | -```typescript |
250 | | -import { |
251 | | - generateApiKey, |
252 | | - timingSafeCompare, |
253 | | - hasAllScopes, |
254 | | - hasAnyScope |
255 | | -} from '@fracabu/fastify-api-key'; |
256 | | - |
257 | | -// Generate a secure API key |
258 | | -const key = generateApiKey({ prefix: 'myapp', length: 32 }); |
259 | | -// => 'myapp_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345' |
260 | | - |
261 | | -// Timing-safe comparison |
262 | | -const isValid = timingSafeCompare(providedKey, storedKey); |
263 | | - |
264 | | -// Scope helpers |
265 | | -hasAllScopes(['read', 'write', 'admin'], ['read', 'write']); // true |
266 | | -hasAnyScope(['read'], ['admin', 'read']); // true |
267 | | -``` |
268 | | - |
269 | | -## Error Classes |
| 127 | + return { users: [] } |
| 128 | +}) |
270 | 129 |
|
271 | | -```typescript |
272 | | -import { |
273 | | - ApiKeyError, |
274 | | - MissingApiKeyError, |
275 | | - InvalidApiKeyError, |
276 | | - InsufficientScopesError, |
277 | | - RateLimitExceededError |
278 | | -} from '@fracabu/fastify-api-key'; |
279 | | - |
280 | | -// All errors have: code, message, statusCode, toJSON() |
| 130 | +// Rotta con scopes richiesti |
| 131 | +app.delete('/api/users/:id', { |
| 132 | + preHandler: app.apiKey({ scopes: ['admin', 'users:delete'] }) |
| 133 | +}, handler) |
281 | 134 | ``` |
282 | 135 |
|
283 | | -## TypeScript Support |
| 136 | +--- |
284 | 137 |
|
285 | | -The plugin provides full TypeScript support with Fastify module augmentation: |
| 138 | +## Requirements |
286 | 139 |
|
287 | | -```typescript |
288 | | -import type { |
289 | | - FastifyApiKeyOptions, |
290 | | - ApiKeySource, |
291 | | - ApiKeyValidationResult, |
292 | | - ApiKeyValidator, |
293 | | - ApiKeyErrorHandler, |
294 | | - ApiKeyHook, |
295 | | - ApiKeyGuardOptions, |
296 | | - ApiKeyData |
297 | | -} from '@fracabu/fastify-api-key'; |
298 | | - |
299 | | -// request.apiKey and request.apiKeyScopes are properly typed |
300 | | -app.get('/test', { preHandler: app.apiKey() }, async (request) => { |
301 | | - const scopes = request.apiKeyScopes; // string[] | undefined |
302 | | - const metadata = request.apiKey?.metadata; // Record<string, unknown> |
303 | | -}); |
304 | | -``` |
| 140 | +- Node.js >= 20.0.0 |
| 141 | +- Fastify >= 5.0.0 |
305 | 142 |
|
306 | 143 | ## License |
307 | 144 |
|
308 | 145 | MIT |
309 | 146 |
|
| 147 | +--- |
| 148 | + |
| 149 | +<p align="center"> |
| 150 | + <a href="https://github.com/fracabu"> |
| 151 | + <img src="https://img.shields.io/badge/Made_by-fracabu-8B5CF6?style=flat-square" alt="Made by fracabu" /> |
| 152 | + </a> |
| 153 | +</p> |
0 commit comments