-
Notifications
You must be signed in to change notification settings - Fork 288
Expand file tree
/
Copy pathapiKeyMiddleware.ts
More file actions
170 lines (153 loc) · 5.79 KB
/
Copy pathapiKeyMiddleware.ts
File metadata and controls
170 lines (153 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/**
* @module apiKeyMiddleware
* @description Express middleware for API key authentication.
*
* Provides middleware for authenticating requests using API keys.
* API keys should be provided in the `X-API-Key` header.
*
* Usage:
* app.get('/api/v1/internal', authenticateApiKey, requireApiKeyScope('contracts', 'read'), handler);
*
* Security notes:
* - Validates API key against stored hash
* - Updates last used timestamp for audit purposes
* - Checks for expired keys
* - Responds with 401 for missing/invalid keys
* - Responds with 403 for insufficient scope
*/
import { Request, Response, NextFunction } from 'express';
import { validateApiKey, ApiKeyInfo } from './apiKeys';
import { authenticateMiddleware } from './authenticate';
import { sendAuthUnauthorized, sendAuthForbidden, sendAuthError } from './errorResponses';
/** Express request extended with API key info. */
export interface ApiKeyAuthenticatedRequest extends Request {
apiKey?: ApiKeyInfo;
}
/**
* Express middleware that extracts and validates the API key from the
* `X-API-Key` request header.
*
* On success, attaches `req.apiKey` with the resolved {@link ApiKeyInfo} and
* delegates to `next()`.
*
* Error paths (never leak internal detail):
* - **401** — `X-API-Key` header is absent.
* - **401** — Header is present but `validateApiKey` returns `null`
* (unknown key, wrong hash, expired, or deactivated).
* - **500** — `validateApiKey` rejects unexpectedly (e.g. database error).
* The raw error is written to `console.error` only; the response body
* contains only `{ error: 'Internal server error' }`.
*
* @param req - Express request (extended with optional `apiKey` field).
* @param res - Express response.
* @param next - Express next function; called only on successful validation.
*/
export function authenticateApiKey(
req: ApiKeyAuthenticatedRequest,
res: Response,
next: NextFunction,
): void {
const apiKey = req.headers['x-api-key'] as string;
if (!apiKey) {
sendAuthUnauthorized(res, 'Missing X-API-Key header');
return;
}
validateApiKey(apiKey)
.then(keyInfo => {
if (!keyInfo) {
sendAuthUnauthorized(res, 'Invalid API key');
return;
}
req.apiKey = keyInfo;
next();
})
.catch(err => {
// eslint-disable-next-line no-console
console.error('API key validation error:', err);
sendAuthError(res, 500, 'internal_error', 'Internal server error');
});
}
/**
* Factory that returns Express middleware enforcing a specific API key scope.
*
* Scope matching rules (evaluated in order):
* 1. **Exact match** — e.g. `contracts:read` satisfies `contracts:read`.
* 2. **Wildcard action** — e.g. `contracts:*` satisfies `contracts:read`.
* 3. **Wildcard resource** — e.g. `*:read` satisfies `contracts:read`.
* 4. **Full wildcard** — `*` satisfies any scope.
*
* Error paths:
* - **401** — `req.apiKey` is not set (caller skipped `authenticateApiKey`).
* - **403** — Key is present but none of its scopes match the requirement.
* The response includes `required` and `provided` for debugging by the
* key owner; no internal implementation detail is exposed.
*
* @param resource - The resource being accessed (e.g. `'contracts'`).
* @param action - The action being performed (e.g. `'read'`).
* @returns Express middleware function.
*/
export function requireApiKeyScope(resource: string, action: string) {
return (req: ApiKeyAuthenticatedRequest, res: Response, next: NextFunction): void => {
if (!req.apiKey) {
sendAuthUnauthorized(res, 'Not authenticated with API key');
return;
}
const requiredScope = `${resource}:${action}`;
const hasScope = req.apiKey.scope.some(scope => {
// Exact match
if (scope === requiredScope) return true;
// Wildcard action (e.g., "contracts:*")
if (scope.endsWith(':*') && scope.startsWith(`${resource}:`)) return true;
// Wildcard resource (e.g., "*:read")
if (scope.startsWith('*:') && scope.endsWith(`:${action}`)) return true;
// Full wildcard
if (scope === '*') return true;
return false;
});
if (!hasScope) {
sendAuthForbidden(res, 'Forbidden: insufficient API key scope');
return;
}
next();
};
}
/**
* Middleware that accepts either JWT Bearer token OR API key authentication.
*
* Resolution order:
* 1. If `Authorization: Bearer <token>` is present, delegates entirely to
* {@link authenticateMiddleware} (JWT path). `req.user` is populated on
* success.
* 2. If `X-API-Key` is present (without a Bearer header), delegates to
* {@link authenticateApiKey}. `req.apiKey` is populated on success.
* 3. If neither credential is provided, responds immediately with **401**.
*
* Use this on endpoints that must be accessible by both human users (JWT) and
* automated internal services (API key).
*
* @param req - Express request supporting both `user` and `apiKey` fields.
* @param res - Express response.
* @param next - Called by the delegated middleware on success.
*/
export function authenticateEither(
req: any, // Using any to support both AuthenticatedRequest and ApiKeyAuthenticatedRequest
res: Response,
next: NextFunction,
): void {
// Check for JWT token first
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
// Let the existing JWT middleware handle this
return authenticateMiddleware(req, res, next);
}
// Check for API key
const apiKey = req.headers['x-api-key'] as string;
if (apiKey) {
return authenticateApiKey(req as ApiKeyAuthenticatedRequest, res, next);
}
// Neither authentication method found
sendAuthUnauthorized(
res,
'Authentication required. Provide either Authorization: Bearer <token> or X-API-Key header',
);
}