expediate 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +4 -4
- package/dist/apis.js +250 -0
- package/dist/git.js +244 -0
- package/dist/jwt-auth.js +575 -0
- package/dist/misc.js +549 -0
- package/dist/router.js +502 -0
- package/dist/static.js +703 -0
- package/package.json +1 -1
- package/src/apis.ts +0 -428
- package/src/git.ts +0 -326
- package/src/index.ts +0 -85
- package/src/jwt-auth.ts +0 -861
- package/src/misc.ts +0 -734
- package/src/router.ts +0 -736
- package/src/static.ts +0 -904
- /package/{src → dist}/mimetypes.json +0 -0
package/src/jwt-auth.ts
DELETED
|
@@ -1,861 +0,0 @@
|
|
|
1
|
-
/* Copyright 2021 Fabien Bavent
|
|
2
|
-
*
|
|
3
|
-
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
4
|
-
* copy of this software and associated documentation files (the "Software"),
|
|
5
|
-
* to deal in the Software without restriction, including without limitation
|
|
6
|
-
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
-
* and/or sell copies of the Software, and to permit persons to whom the
|
|
8
|
-
* Software is furnished to do so, subject to the following conditions:
|
|
9
|
-
*
|
|
10
|
-
* The above copyright notice and this permission notice shall be included
|
|
11
|
-
* in all copies or substantial portions of the Software.
|
|
12
|
-
*
|
|
13
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
14
|
-
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
16
|
-
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
18
|
-
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
-
* DEALINGS IN THE SOFTWARE.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* jwt-auth.ts
|
|
23
|
-
*
|
|
24
|
-
* JWT authentication plugin for the Expediate router.
|
|
25
|
-
*
|
|
26
|
-
* Provides:
|
|
27
|
-
* - Stateless access tokens (HS256 / HS384 / HS512 HMAC-signed JWTs).
|
|
28
|
-
* - Opaque refresh tokens with server-side storage and automatic rotation.
|
|
29
|
-
* - Route handlers for login, token refresh, and logout.
|
|
30
|
-
* - Middleware for token validation, authorisation, role checks, and
|
|
31
|
-
* permission checks.
|
|
32
|
-
*
|
|
33
|
-
* Security notes:
|
|
34
|
-
* - Passwords are hashed with SHA-256 for demonstration purposes only.
|
|
35
|
-
* Replace with bcrypt / argon2 in production.
|
|
36
|
-
* - The default secrets are placeholders — always override them in production.
|
|
37
|
-
* - Refresh token storage defaults to an in-process Map; replace with a
|
|
38
|
-
* persistent store (Redis, database) for multi-instance deployments.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
import crypto from 'crypto';
|
|
42
|
-
import type { RouterRequest, RouterResponse, Middleware } from './router.js';
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Types
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
/** A user record as stored in (or returned by) the user database. */
|
|
49
|
-
export interface UserRecord {
|
|
50
|
-
/** Stable unique identifier (used as JWT `sub` claim when present). */
|
|
51
|
-
id?: string;
|
|
52
|
-
/** Login username. */
|
|
53
|
-
username: string;
|
|
54
|
-
/**
|
|
55
|
-
* SHA-256 hex digest of the user's password.
|
|
56
|
-
* Replace with a bcrypt/argon2 hash in production.
|
|
57
|
-
*/
|
|
58
|
-
passwordHash: string;
|
|
59
|
-
/** Role labels assigned to this user (e.g. `'admin'`, `'editor'`). */
|
|
60
|
-
roles?: string[];
|
|
61
|
-
/**
|
|
62
|
-
* Fine-grained permission strings assigned to this user
|
|
63
|
-
* (e.g. `'read'`, `'write'`, `'delete'`).
|
|
64
|
-
*/
|
|
65
|
-
permissions?: string[];
|
|
66
|
-
/** Any additional fields the application wants to carry. */
|
|
67
|
-
[key: string]: unknown;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The decoded JWT access-token payload attached to `req.user` after
|
|
72
|
-
* successful authentication.
|
|
73
|
-
*/
|
|
74
|
-
export interface TokenPayload {
|
|
75
|
-
/** JWT subject — typically the user's stable ID. */
|
|
76
|
-
sub: string;
|
|
77
|
-
/** Username extracted from the user record. */
|
|
78
|
-
username: string;
|
|
79
|
-
/** Issuer claim, set to `config.issuer`. */
|
|
80
|
-
iss: string;
|
|
81
|
-
/** Issued-at timestamp (Unix seconds). */
|
|
82
|
-
iat: number;
|
|
83
|
-
/** Expiration timestamp (Unix seconds). */
|
|
84
|
-
exp: number;
|
|
85
|
-
/** Roles copied from the user record. */
|
|
86
|
-
roles?: string[];
|
|
87
|
-
/** Permissions copied from the user record. */
|
|
88
|
-
permissions?: string[];
|
|
89
|
-
/** Any additional claims produced by `config.payload`. */
|
|
90
|
-
[key: string]: unknown;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Internal metadata stored alongside each active refresh token. */
|
|
94
|
-
interface RefreshTokenData {
|
|
95
|
-
/** Username the refresh token was issued to. */
|
|
96
|
-
username: string;
|
|
97
|
-
/** Unix ms timestamp of issuance. */
|
|
98
|
-
issuedAt: number;
|
|
99
|
-
/** Unix ms timestamp after which the token must be rejected. */
|
|
100
|
-
expiresAt: number;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Minimal interface for the refresh-token store.
|
|
105
|
-
* Any object implementing these four methods is accepted (Map, Redis client
|
|
106
|
-
* adapter, database wrapper, etc.).
|
|
107
|
-
*/
|
|
108
|
-
export interface TokenStore {
|
|
109
|
-
set(key: string, value: RefreshTokenData): void;
|
|
110
|
-
get(key: string): RefreshTokenData | undefined;
|
|
111
|
-
delete(key: string): void;
|
|
112
|
-
has(key: string): boolean;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Supported HMAC-SHA signing algorithms for JWT.
|
|
117
|
-
* RS*, ES*, and PS* families are not yet implemented.
|
|
118
|
-
*/
|
|
119
|
-
export type JwtAlgorithm = 'HS256' | 'HS384' | 'HS512';
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Full configuration object for {@link createJwtPlugin}.
|
|
123
|
-
* All fields have defaults; override only what you need.
|
|
124
|
-
*/
|
|
125
|
-
export interface JwtConfig {
|
|
126
|
-
/** HMAC secret used to sign access tokens. **Change in production.** */
|
|
127
|
-
accessTokenSecret: string;
|
|
128
|
-
/** HMAC secret used to sign refresh tokens (currently unused — refresh *
|
|
129
|
-
* tokens are opaque random strings, not JWTs). Reserved for future use. */
|
|
130
|
-
refreshTokenSecret: string;
|
|
131
|
-
/** Access token lifetime in **seconds**. Defaults to 15 minutes. */
|
|
132
|
-
accessTokenExpiry: number;
|
|
133
|
-
/** Refresh token lifetime in **seconds**. Defaults to 7 days. */
|
|
134
|
-
refreshTokenExpiry: number;
|
|
135
|
-
/** Value placed in the JWT `iss` claim. */
|
|
136
|
-
issuer: string;
|
|
137
|
-
/**
|
|
138
|
-
* When `true`, the `authenticate` middleware rejects tokens whose `iss`
|
|
139
|
-
* claim does not match `config.issuer`.
|
|
140
|
-
* Defaults to `false` (issuer not checked).
|
|
141
|
-
*/
|
|
142
|
-
checkIssuer: boolean;
|
|
143
|
-
/** JWT signing algorithm. Defaults to `'HS256'`. */
|
|
144
|
-
alg: JwtAlgorithm;
|
|
145
|
-
/**
|
|
146
|
-
* Extract the login username from a user record.
|
|
147
|
-
* Defaults to `(user) => user.username`.
|
|
148
|
-
*/
|
|
149
|
-
username: (user: UserRecord) => string;
|
|
150
|
-
/**
|
|
151
|
-
* Fetch a user record by username.
|
|
152
|
-
* Return `undefined` (or any falsy value) when the user does not exist.
|
|
153
|
-
*/
|
|
154
|
-
fetchUser: (username: string) => UserRecord | undefined;
|
|
155
|
-
/**
|
|
156
|
-
* Return `true` when the supplied plain-text `password` is valid for
|
|
157
|
-
* `user`, `false` otherwise.
|
|
158
|
-
*
|
|
159
|
-
* The default implementation compares SHA-256 hashes; replace with a
|
|
160
|
-
* timing-safe bcrypt/argon2 check in production.
|
|
161
|
-
*/
|
|
162
|
-
isPasswordValid: (user: UserRecord, password: string) => boolean;
|
|
163
|
-
/**
|
|
164
|
-
* Build the JWT payload for a user.
|
|
165
|
-
* The `iss`, `iat`, `exp`, and `sub` claims are added automatically.
|
|
166
|
-
* Returning a partial object is fine — the plugin merges the rest.
|
|
167
|
-
*/
|
|
168
|
-
payload: (user: UserRecord) => Partial<TokenPayload>;
|
|
169
|
-
/**
|
|
170
|
-
* Active refresh-token store.
|
|
171
|
-
* Defaults to an in-process `Map` (lost on restart; not suitable for
|
|
172
|
-
* multi-instance deployments).
|
|
173
|
-
*/
|
|
174
|
-
refreshTokenStore: TokenStore;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Result returned by internal authentication / token-issue functions. */
|
|
178
|
-
type AuthResult =
|
|
179
|
-
| { success: true; accessToken: string; refreshToken: string;
|
|
180
|
-
expiresIn: number; tokenType: 'Bearer' }
|
|
181
|
-
| { success: false; error: string };
|
|
182
|
-
|
|
183
|
-
/** Result returned by {@link verifyToken}. */
|
|
184
|
-
type VerifyResult =
|
|
185
|
-
| { valid: true; payload: TokenPayload }
|
|
186
|
-
| { valid: false; error: string };
|
|
187
|
-
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
// Sample user database (replace or extend for real applications)
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Hash a plain-text password with SHA-256 and return the hex digest.
|
|
194
|
-
*
|
|
195
|
-
* > **⚠ Warning:** SHA-256 is fast and therefore unsuitable for password
|
|
196
|
-
* > hashing in production. Use bcrypt or argon2 instead.
|
|
197
|
-
*
|
|
198
|
-
* @param password - The plain-text password to hash.
|
|
199
|
-
* @returns A 64-character lowercase hex string.
|
|
200
|
-
*/
|
|
201
|
-
export function hashPassword(password: string): string {
|
|
202
|
-
return crypto.createHash('sha256').update(password).digest('hex');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Default in-memory user database used when no custom `fetchUser` is
|
|
207
|
-
* provided. Contains three demo accounts: alice (admin), bob (editor),
|
|
208
|
-
* charlie (viewer).
|
|
209
|
-
*
|
|
210
|
-
* Replace or ignore this map entirely when you supply your own `fetchUser`.
|
|
211
|
-
*/
|
|
212
|
-
export const userDatabase = new Map<string, UserRecord>([
|
|
213
|
-
['alice', {
|
|
214
|
-
id: 'usr_001',
|
|
215
|
-
username: 'alice',
|
|
216
|
-
passwordHash: hashPassword('password123'),
|
|
217
|
-
roles: ['admin', 'editor'],
|
|
218
|
-
permissions: ['read', 'write', 'delete', 'manage_users'],
|
|
219
|
-
}],
|
|
220
|
-
['bob', {
|
|
221
|
-
id: 'usr_002',
|
|
222
|
-
username: 'bob',
|
|
223
|
-
passwordHash: hashPassword('secret456'),
|
|
224
|
-
roles: ['editor'],
|
|
225
|
-
permissions: ['read', 'write'],
|
|
226
|
-
}],
|
|
227
|
-
['charlie', {
|
|
228
|
-
id: 'usr_003',
|
|
229
|
-
username: 'charlie',
|
|
230
|
-
passwordHash: hashPassword('pass789'),
|
|
231
|
-
roles: ['viewer'],
|
|
232
|
-
permissions: ['read'],
|
|
233
|
-
}],
|
|
234
|
-
]);
|
|
235
|
-
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
// Default configuration
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
const DEFAULT_CONFIG: JwtConfig = {
|
|
241
|
-
accessTokenSecret: 'access-secret-change-in-production',
|
|
242
|
-
refreshTokenSecret: 'refresh-secret-change-in-production',
|
|
243
|
-
accessTokenExpiry: 15 * 60, // 15 minutes
|
|
244
|
-
refreshTokenExpiry: 7 * 24 * 3600, // 7 days
|
|
245
|
-
issuer: 'jwt-auth',
|
|
246
|
-
checkIssuer: false,
|
|
247
|
-
alg: 'HS256',
|
|
248
|
-
|
|
249
|
-
username: (user) => user.username,
|
|
250
|
-
fetchUser: (username) => userDatabase.get(username),
|
|
251
|
-
|
|
252
|
-
// BUG FIX: the original callback was named `checkPassword` and returned
|
|
253
|
-
// `true` when the password was WRONG (inverted logic). Renamed to
|
|
254
|
-
// `isPasswordValid` and inverted so it returns `true` on a match.
|
|
255
|
-
isPasswordValid: (user, password) => user.passwordHash === hashPassword(password),
|
|
256
|
-
|
|
257
|
-
payload: (user) => ({
|
|
258
|
-
sub: user.id,
|
|
259
|
-
username: user.username,
|
|
260
|
-
roles: user.roles,
|
|
261
|
-
permissions: user.permissions,
|
|
262
|
-
}),
|
|
263
|
-
|
|
264
|
-
refreshTokenStore: new Map<string, RefreshTokenData>(),
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
// JWT utilities — manual Base64URL implementation
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Encode an arbitrary object as a Base64URL-encoded JSON string, suitable
|
|
273
|
-
* for use as a JWT header or payload segment.
|
|
274
|
-
*
|
|
275
|
-
* @param data - Any JSON-serialisable value.
|
|
276
|
-
* @returns A Base64URL string with no padding characters.
|
|
277
|
-
*/
|
|
278
|
-
function base64UrlEncode(data: object): string {
|
|
279
|
-
return Buffer.from(JSON.stringify(data))
|
|
280
|
-
.toString('base64')
|
|
281
|
-
.replace(/=/g, '')
|
|
282
|
-
.replace(/\+/g, '-')
|
|
283
|
-
.replace(/\//g, '_');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Decode a Base64URL-encoded JWT segment and parse it as JSON.
|
|
288
|
-
*
|
|
289
|
-
* @param str - A Base64URL string (padding optional).
|
|
290
|
-
* @returns The parsed JSON value.
|
|
291
|
-
* @throws When the string is not valid Base64URL JSON.
|
|
292
|
-
*/
|
|
293
|
-
function base64UrlDecode(str: string): unknown {
|
|
294
|
-
const padded = str + '='.repeat((4 - (str.length % 4)) % 4);
|
|
295
|
-
return JSON.parse(
|
|
296
|
-
Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Compute the Base64URL-encoded HMAC signature for a JWT.
|
|
302
|
-
*
|
|
303
|
-
* Receives the **already-encoded** header and payload strings (i.e. the
|
|
304
|
-
* first two dot-separated segments of the token) and signs the
|
|
305
|
-
* `"<header>.<payload>"` string with the given secret.
|
|
306
|
-
*
|
|
307
|
-
* Currently supports the HS (HMAC-SHA) family: `HS256`, `HS384`, `HS512`.
|
|
308
|
-
*
|
|
309
|
-
* @param encodedHeader - Base64URL-encoded JWT header string.
|
|
310
|
-
* @param encodedPayload - Base64URL-encoded JWT payload string.
|
|
311
|
-
* @param secret - HMAC secret key.
|
|
312
|
-
* @param alg - Algorithm identifier (must be HS256, HS384, or HS512).
|
|
313
|
-
* @returns A Base64URL-encoded signature string.
|
|
314
|
-
* @throws {Error} When `alg` is not a supported HS algorithm.
|
|
315
|
-
*/
|
|
316
|
-
function createSignature(
|
|
317
|
-
encodedHeader: string,
|
|
318
|
-
encodedPayload: string,
|
|
319
|
-
secret: string,
|
|
320
|
-
alg: JwtAlgorithm,
|
|
321
|
-
): string {
|
|
322
|
-
// BUG FIX: the original code called `header.alg` where `header` was already
|
|
323
|
-
// a Base64URL *string*, not a decoded object. Accessing `.alg` on a string
|
|
324
|
-
// always returns `undefined`, causing every signature to fail. The algorithm
|
|
325
|
-
// is now passed explicitly as a parameter instead of being read from the header.
|
|
326
|
-
const shaVariant = `sha${alg.substring(2)}`; // 'sha256', 'sha384', 'sha512'
|
|
327
|
-
return crypto
|
|
328
|
-
.createHmac(shaVariant, secret)
|
|
329
|
-
.update(`${encodedHeader}.${encodedPayload}`)
|
|
330
|
-
.digest('base64')
|
|
331
|
-
.replace(/=/g, '')
|
|
332
|
-
.replace(/\+/g, '-')
|
|
333
|
-
.replace(/\//g, '_');
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Sign a payload object and return a compact JWT string.
|
|
338
|
-
*
|
|
339
|
-
* Automatically adds the `iat` (issued-at) and `exp` (expiration) claims.
|
|
340
|
-
* Any claims already present in `payload` are preserved and take precedence
|
|
341
|
-
* over `iat`/`exp` (use this to override expiry if needed).
|
|
342
|
-
*
|
|
343
|
-
* @param payload - JWT payload claims (must be JSON-serialisable).
|
|
344
|
-
* @param secret - HMAC secret used to sign the token.
|
|
345
|
-
* @param expiresIn - Validity window in **seconds** from the current time.
|
|
346
|
-
* @param alg - Signing algorithm. Defaults to `'HS256'`.
|
|
347
|
-
* @returns A compact JWT string in the form `header.payload.signature`.
|
|
348
|
-
*/
|
|
349
|
-
function signToken(
|
|
350
|
-
payload: Partial<TokenPayload>,
|
|
351
|
-
secret: string,
|
|
352
|
-
expiresIn: number,
|
|
353
|
-
alg: JwtAlgorithm = 'HS256',
|
|
354
|
-
): string {
|
|
355
|
-
const now = Math.floor(Date.now() / 1000);
|
|
356
|
-
const encodedHeader = base64UrlEncode({ alg, typ: 'JWT' });
|
|
357
|
-
const fullPayload = base64UrlEncode({ ...payload, iat: now, exp: now + expiresIn });
|
|
358
|
-
const signature = createSignature(encodedHeader, fullPayload, secret, alg);
|
|
359
|
-
return `${encodedHeader}.${fullPayload}.${signature}`;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Verify a compact JWT string and return its decoded payload on success.
|
|
364
|
-
*
|
|
365
|
-
* Performs the following checks in order:
|
|
366
|
-
* 1. Structural validity (exactly three dot-separated segments).
|
|
367
|
-
* 2. Algorithm consistency (header `alg` matches the expected `alg`).
|
|
368
|
-
* 3. Signature integrity (timing-safe HMAC comparison).
|
|
369
|
-
* 4. Expiration (`exp` claim is in the future).
|
|
370
|
-
*
|
|
371
|
-
* All errors are returned as `{ valid: false, error }` — no exception is
|
|
372
|
-
* thrown to the caller.
|
|
373
|
-
*
|
|
374
|
-
* @param token - The compact JWT string to verify.
|
|
375
|
-
* @param secret - HMAC secret that was used to sign the token.
|
|
376
|
-
* @param alg - Expected signing algorithm.
|
|
377
|
-
* @returns A {@link VerifyResult} discriminated union.
|
|
378
|
-
*/
|
|
379
|
-
function verifyToken(token: string, secret: string, alg: JwtAlgorithm): VerifyResult {
|
|
380
|
-
try {
|
|
381
|
-
const parts = token.split('.');
|
|
382
|
-
if (parts.length !== 3)
|
|
383
|
-
return { valid: false, error: 'Invalid token format' };
|
|
384
|
-
|
|
385
|
-
const [encodedHeader, encodedPayload, signature] = parts;
|
|
386
|
-
|
|
387
|
-
// BUG FIX: the original code accessed `header.alg` where `header` is a
|
|
388
|
-
// Base64URL *string*. `.alg` on a string is always `undefined`, so the
|
|
389
|
-
// algorithm check always failed and every token was rejected. The header
|
|
390
|
-
// must be decoded first.
|
|
391
|
-
const decodedHeader = base64UrlDecode(encodedHeader) as { alg?: string; typ?: string };
|
|
392
|
-
if (decodedHeader.alg !== alg)
|
|
393
|
-
return { valid: false, error: 'Unauthorised signing algorithm' };
|
|
394
|
-
|
|
395
|
-
const expectedSig = createSignature(encodedHeader, encodedPayload, secret, alg);
|
|
396
|
-
|
|
397
|
-
// BUG FIX: `timingSafeEqual` requires both Buffers to have the same
|
|
398
|
-
// length. When a forged token has a signature of a different length,
|
|
399
|
-
// Node throws a RangeError instead of returning `false`. Guard against
|
|
400
|
-
// this by checking lengths before calling timingSafeEqual.
|
|
401
|
-
const sigBuf = Buffer.from(signature);
|
|
402
|
-
const expectedBuf = Buffer.from(expectedSig);
|
|
403
|
-
if (
|
|
404
|
-
sigBuf.length !== expectedBuf.length ||
|
|
405
|
-
!crypto.timingSafeEqual(sigBuf, expectedBuf)
|
|
406
|
-
) {
|
|
407
|
-
return { valid: false, error: 'Invalid signature' };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const payload = base64UrlDecode(encodedPayload) as TokenPayload;
|
|
411
|
-
const now = Math.floor(Date.now() / 1000);
|
|
412
|
-
|
|
413
|
-
if (payload.exp && payload.exp < now)
|
|
414
|
-
return { valid: false, error: 'Token expired' };
|
|
415
|
-
|
|
416
|
-
return { valid: true, payload };
|
|
417
|
-
} catch {
|
|
418
|
-
return { valid: false, error: 'Malformed token' };
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Generate a cryptographically secure opaque refresh token.
|
|
424
|
-
*
|
|
425
|
-
* The token is 128 hex characters (64 random bytes), providing 512 bits of
|
|
426
|
-
* entropy — far beyond any brute-force threat.
|
|
427
|
-
*
|
|
428
|
-
* @returns A 128-character lowercase hex string.
|
|
429
|
-
*/
|
|
430
|
-
function generateRefreshToken(): string {
|
|
431
|
-
return crypto.randomBytes(64).toString('hex');
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// ---------------------------------------------------------------------------
|
|
435
|
-
// Business logic
|
|
436
|
-
// ---------------------------------------------------------------------------
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Authenticate a user by username and password and, on success, issue a new
|
|
440
|
-
* access + refresh token pair.
|
|
441
|
-
*
|
|
442
|
-
* @param username - The username supplied by the client.
|
|
443
|
-
* @param password - The plain-text password supplied by the client.
|
|
444
|
-
* @param config - Resolved plugin configuration.
|
|
445
|
-
* @returns An {@link AuthResult} discriminated union.
|
|
446
|
-
*/
|
|
447
|
-
function authenticateUser(username: string, password: string, config: JwtConfig): AuthResult {
|
|
448
|
-
const user = config.fetchUser(username);
|
|
449
|
-
if (!user) return { success: false, error: 'User not found' };
|
|
450
|
-
|
|
451
|
-
// BUG FIX: the original used `checkPassword` which returned `true` when
|
|
452
|
-
// the password was WRONG (negated logic). `isPasswordValid` returns `true`
|
|
453
|
-
// when the password is correct.
|
|
454
|
-
if (!config.isPasswordValid(user, password))
|
|
455
|
-
return { success: false, error: 'Incorrect password' };
|
|
456
|
-
|
|
457
|
-
return issueTokenPair(user, config);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Build and store a new access + refresh token pair for the given user.
|
|
462
|
-
*
|
|
463
|
-
* The refresh token is stored in `config.refreshTokenStore` with its
|
|
464
|
-
* expiration timestamp so it can be validated on subsequent renewal requests.
|
|
465
|
-
*
|
|
466
|
-
* @param user - The authenticated user record.
|
|
467
|
-
* @param config - Resolved plugin configuration.
|
|
468
|
-
* @returns An {@link AuthResult} with `success: true`.
|
|
469
|
-
*/
|
|
470
|
-
function issueTokenPair(user: UserRecord, config: JwtConfig): AuthResult {
|
|
471
|
-
const username = config.username(user);
|
|
472
|
-
const claims = config.payload(user);
|
|
473
|
-
|
|
474
|
-
// Inject standard claims; caller-supplied claims take precedence.
|
|
475
|
-
const fullClaims: Partial<TokenPayload> = {
|
|
476
|
-
sub: username, // fallback subject — overridden by payload() if it sets sub
|
|
477
|
-
...claims,
|
|
478
|
-
iss: config.issuer,
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
const accessToken = signToken(fullClaims, config.accessTokenSecret, config.accessTokenExpiry, config.alg);
|
|
482
|
-
const refreshToken = generateRefreshToken();
|
|
483
|
-
|
|
484
|
-
config.refreshTokenStore.set(refreshToken, {
|
|
485
|
-
username,
|
|
486
|
-
issuedAt: Date.now(),
|
|
487
|
-
expiresAt: Date.now() + config.refreshTokenExpiry * 1000,
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
return {
|
|
491
|
-
success: true,
|
|
492
|
-
accessToken,
|
|
493
|
-
refreshToken,
|
|
494
|
-
expiresIn: config.accessTokenExpiry,
|
|
495
|
-
tokenType: 'Bearer',
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Renew an access token using a valid refresh token.
|
|
501
|
-
*
|
|
502
|
-
* Implements **refresh token rotation**: the presented refresh token is
|
|
503
|
-
* always invalidated and a brand-new pair is issued on success. This means
|
|
504
|
-
* a stolen refresh token can only be used once before it is invalidated by
|
|
505
|
-
* the legitimate holder's next renewal.
|
|
506
|
-
*
|
|
507
|
-
* @param username - The username from the renewal request body.
|
|
508
|
-
* @param refreshToken - The opaque refresh token string.
|
|
509
|
-
* @param config - Resolved plugin configuration.
|
|
510
|
-
* @returns An {@link AuthResult} discriminated union.
|
|
511
|
-
*/
|
|
512
|
-
function renewAccessToken(username: string, refreshToken: string, config: JwtConfig): AuthResult {
|
|
513
|
-
const tokenData = config.refreshTokenStore.get(refreshToken);
|
|
514
|
-
|
|
515
|
-
// Verify the token exists and belongs to the claimed user.
|
|
516
|
-
if (!tokenData || tokenData.username !== username)
|
|
517
|
-
return { success: false, error: 'Invalid or revoked refresh token' };
|
|
518
|
-
|
|
519
|
-
if (Date.now() > tokenData.expiresAt) {
|
|
520
|
-
config.refreshTokenStore.delete(refreshToken);
|
|
521
|
-
return { success: false, error: 'Refresh token expired' };
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const user = config.fetchUser(tokenData.username);
|
|
525
|
-
if (!user) {
|
|
526
|
-
config.refreshTokenStore.delete(refreshToken);
|
|
527
|
-
return { success: false, error: 'User not found' };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Rotate: invalidate the used token before issuing a new pair.
|
|
531
|
-
config.refreshTokenStore.delete(refreshToken);
|
|
532
|
-
return issueTokenPair(user, config);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Revoke a refresh token, preventing it from being used to obtain new access
|
|
537
|
-
* tokens. Idempotent — revoking an already-revoked token is not an error.
|
|
538
|
-
*
|
|
539
|
-
* @param refreshToken - The opaque refresh token string to revoke.
|
|
540
|
-
* @param config - Resolved plugin configuration.
|
|
541
|
-
* @returns `true` if the token existed and was removed, `false` if it was
|
|
542
|
-
* already absent.
|
|
543
|
-
*/
|
|
544
|
-
function revokeRefreshToken(refreshToken: string, config: JwtConfig): boolean {
|
|
545
|
-
// BUG FIX: the original function referenced `config` as a free variable but
|
|
546
|
-
// `config` only exists inside `createJwtPlugin`. The function was declared
|
|
547
|
-
// at module level and crashed with a ReferenceError at runtime. `config` is
|
|
548
|
-
// now a required parameter.
|
|
549
|
-
const existed = config.refreshTokenStore.has(refreshToken);
|
|
550
|
-
config.refreshTokenStore.delete(refreshToken);
|
|
551
|
-
return existed;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// ---------------------------------------------------------------------------
|
|
555
|
-
// Plugin factory
|
|
556
|
-
// ---------------------------------------------------------------------------
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* The object returned by {@link createJwtPlugin}.
|
|
560
|
-
*
|
|
561
|
-
* Mount the handlers on your router and apply the middleware to protected
|
|
562
|
-
* routes:
|
|
563
|
-
*
|
|
564
|
-
* ```ts
|
|
565
|
-
* const auth = createJwtPlugin({ accessTokenSecret: process.env.JWT_SECRET! });
|
|
566
|
-
*
|
|
567
|
-
* app.post('/auth/login', json(), auth.login);
|
|
568
|
-
* app.post('/auth/refresh', json(), auth.refresh);
|
|
569
|
-
* app.post('/auth/logout', json(), auth.logout);
|
|
570
|
-
*
|
|
571
|
-
* app.get('/me', auth.authenticate, auth.authorize, getProfile);
|
|
572
|
-
* app.delete('/admin', ...auth.requireRole('admin'), deleteHandler);
|
|
573
|
-
* app.put('/posts', ...auth.requirePermission('write'), updateHandler);
|
|
574
|
-
* ```
|
|
575
|
-
*/
|
|
576
|
-
export interface JwtPlugin {
|
|
577
|
-
/**
|
|
578
|
-
* Route handler for `POST /auth/login`.
|
|
579
|
-
* Expects a JSON body with `{ username, password }`.
|
|
580
|
-
* On success: responds with `{ accessToken, refreshToken, expiresIn, tokenType }`.
|
|
581
|
-
*/
|
|
582
|
-
login: Middleware;
|
|
583
|
-
/**
|
|
584
|
-
* Route handler for `POST /auth/refresh`.
|
|
585
|
-
* Expects a JSON body with `{ username, refreshToken }`.
|
|
586
|
-
* On success: responds with a new `{ accessToken, refreshToken, ... }` pair.
|
|
587
|
-
*/
|
|
588
|
-
refresh: Middleware;
|
|
589
|
-
/**
|
|
590
|
-
* Route handler for `POST /auth/logout`.
|
|
591
|
-
* Expects a JSON body with `{ refreshToken }` (optional).
|
|
592
|
-
* Always responds with 200; revokes the refresh token if provided.
|
|
593
|
-
*/
|
|
594
|
-
logout: Middleware;
|
|
595
|
-
/**
|
|
596
|
-
* Middleware that validates the `Authorization: Bearer <token>` header.
|
|
597
|
-
* Sets `req.user` to the decoded {@link TokenPayload} on success.
|
|
598
|
-
* Calls `next()` silently (without error) when the token is absent or invalid,
|
|
599
|
-
* allowing the route to decide how to handle unauthenticated requests.
|
|
600
|
-
*/
|
|
601
|
-
authenticate: Middleware;
|
|
602
|
-
/**
|
|
603
|
-
* Middleware that rejects unauthenticated requests with 401.
|
|
604
|
-
* Should be placed **after** {@link authenticate}:
|
|
605
|
-
* `router.get('/me', auth.authenticate, auth.authorize, handler)`.
|
|
606
|
-
*/
|
|
607
|
-
authorize: Middleware;
|
|
608
|
-
/**
|
|
609
|
-
* Middleware factory that requires the authenticated user to have at least
|
|
610
|
-
* one of the specified roles. Returns `[authenticate, roleCheck]` so it
|
|
611
|
-
* can be spread directly into a route registration:
|
|
612
|
-
* `router.get('/admin', ...auth.requireRole('admin'), handler)`.
|
|
613
|
-
* Responds with 401 when unauthenticated, 403 when the role is missing.
|
|
614
|
-
*/
|
|
615
|
-
requireRole: (...roles: string[]) => Middleware[];
|
|
616
|
-
/**
|
|
617
|
-
* Middleware factory that requires the authenticated user to have **all**
|
|
618
|
-
* of the specified permissions. Returns `[authenticate, permCheck]`.
|
|
619
|
-
* Responds with 401 when unauthenticated, 403 when any permission is missing.
|
|
620
|
-
*/
|
|
621
|
-
requirePermission: (...permissions: string[]) => Middleware[];
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Create a JWT authentication plugin pre-configured with the given options.
|
|
626
|
-
*
|
|
627
|
-
* All config fields have safe defaults for development. At minimum, set
|
|
628
|
-
* `accessTokenSecret` (and `refreshTokenSecret` if you plan to use it) to
|
|
629
|
-
* random values in production.
|
|
630
|
-
*
|
|
631
|
-
* @param userConfig - Partial {@link JwtConfig} overrides.
|
|
632
|
-
* @returns A {@link JwtPlugin} object exposing handlers and middleware.
|
|
633
|
-
*/
|
|
634
|
-
export function createJwtPlugin(userConfig: Partial<JwtConfig> = {}): JwtPlugin {
|
|
635
|
-
const config: JwtConfig = { ...DEFAULT_CONFIG, ...userConfig };
|
|
636
|
-
|
|
637
|
-
// Helper: write a JSON response (our router's res.send() does not add
|
|
638
|
-
// Content-Type automatically, so we set it manually).
|
|
639
|
-
function sendJson(res: RouterResponse, status: number, data: object): void {
|
|
640
|
-
const body = JSON.stringify(data);
|
|
641
|
-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
642
|
-
res.status(status).send(body);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// ── POST /auth/login ────────────────────────────────────────────────────
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Login handler. Reads `{ username, password }` from `req.body` (requires
|
|
649
|
-
* a JSON body-parsing middleware such as `json()` to run first).
|
|
650
|
-
*/
|
|
651
|
-
const login: Middleware = (req: RouterRequest, res: RouterResponse): void => {
|
|
652
|
-
const { username, password } = (req as any).body ?? {};
|
|
653
|
-
|
|
654
|
-
if (!username || !password) {
|
|
655
|
-
sendJson(res, 400, { error: "Fields 'username' and 'password' are required" });
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const result = authenticateUser(username, password, config);
|
|
660
|
-
|
|
661
|
-
if (!result.success) {
|
|
662
|
-
sendJson(res, 401, { error: result.error });
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
sendJson(res, 200, {
|
|
667
|
-
message: 'Authentication successful',
|
|
668
|
-
accessToken: result.accessToken,
|
|
669
|
-
refreshToken: result.refreshToken,
|
|
670
|
-
expiresIn: result.expiresIn,
|
|
671
|
-
tokenType: result.tokenType,
|
|
672
|
-
});
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
// ── POST /auth/refresh ──────────────────────────────────────────────────
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Token-renewal handler. Reads `{ username, refreshToken }` from
|
|
679
|
-
* `req.body`.
|
|
680
|
-
*
|
|
681
|
-
* BUG FIX: the original accepted a renewal request without `username`,
|
|
682
|
-
* allowing `renewAccessToken(undefined, token, config)` to be called.
|
|
683
|
-
* Because `tokenData.username !== undefined` is always `true` for any real
|
|
684
|
-
* token, any holder of a refresh token could silently impersonate its owner.
|
|
685
|
-
* `username` is now validated as a required field.
|
|
686
|
-
*/
|
|
687
|
-
const refresh: Middleware = (req: RouterRequest, res: RouterResponse): void => {
|
|
688
|
-
const { username, refreshToken } = (req as any).body ?? {};
|
|
689
|
-
|
|
690
|
-
if (!username || !refreshToken) {
|
|
691
|
-
sendJson(res, 400, { error: "Fields 'username' and 'refreshToken' are required" });
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const result = renewAccessToken(username, refreshToken, config);
|
|
696
|
-
|
|
697
|
-
if (!result.success) {
|
|
698
|
-
sendJson(res, 401, { error: result.error });
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
sendJson(res, 200, {
|
|
703
|
-
message: 'Token renewed successfully',
|
|
704
|
-
accessToken: result.accessToken,
|
|
705
|
-
refreshToken: result.refreshToken,
|
|
706
|
-
expiresIn: result.expiresIn,
|
|
707
|
-
tokenType: result.tokenType,
|
|
708
|
-
});
|
|
709
|
-
};
|
|
710
|
-
|
|
711
|
-
// ── POST /auth/logout ───────────────────────────────────────────────────
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Logout handler. Optionally reads `{ refreshToken }` from `req.body` and
|
|
715
|
-
* revokes it. Always responds with 200 regardless of whether a token was
|
|
716
|
-
* provided (to avoid leaking information about token existence).
|
|
717
|
-
*/
|
|
718
|
-
const logout: Middleware = (req: RouterRequest, res: RouterResponse): void => {
|
|
719
|
-
const { refreshToken } = (req as any).body ?? {};
|
|
720
|
-
|
|
721
|
-
if (refreshToken) {
|
|
722
|
-
// BUG FIX: the original called `revokeRefreshToken(refreshToken)` without
|
|
723
|
-
// passing `config`, which caused a ReferenceError because `config` is a
|
|
724
|
-
// local variable inside `createJwtPlugin`.
|
|
725
|
-
revokeRefreshToken(refreshToken, config);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
sendJson(res, 200, { message: 'Logged out successfully' });
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
// ── authenticate middleware ─────────────────────────────────────────────
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Validate the `Authorization: Bearer <token>` header and populate
|
|
735
|
-
* `req.user` with the decoded payload.
|
|
736
|
-
*
|
|
737
|
-
* Designed to be **non-blocking**: missing or invalid tokens cause `next()`
|
|
738
|
-
* to be called without error, deferring the authentication decision to the
|
|
739
|
-
* next middleware (typically {@link authorize} or a custom guard).
|
|
740
|
-
*
|
|
741
|
-
* `req.user` is explicitly cleared at the start of each invocation to
|
|
742
|
-
* prevent stale data from leaking across requests in unusual server setups.
|
|
743
|
-
*/
|
|
744
|
-
const authenticate: Middleware = (req: RouterRequest, res: RouterResponse, next: () => void): void => {
|
|
745
|
-
// Always clear any previously set user to prevent cross-request contamination.
|
|
746
|
-
delete (req as any).user;
|
|
747
|
-
|
|
748
|
-
const authHeader = req.headers['authorization'] as string | undefined;
|
|
749
|
-
if (!authHeader?.startsWith('Bearer ')) return next();
|
|
750
|
-
|
|
751
|
-
const token = authHeader.slice(7);
|
|
752
|
-
const result = verifyToken(token, config.accessTokenSecret, config.alg);
|
|
753
|
-
if (!result.valid) return next();
|
|
754
|
-
|
|
755
|
-
// BUG FIX: the original compared `result.payload` (an object) to
|
|
756
|
-
// `config.issuer` (a string) with `!=`, which is always `true`, causing
|
|
757
|
-
// every token to be rejected when `checkIssuer` was enabled.
|
|
758
|
-
// The correct check reads the `iss` claim from the decoded payload.
|
|
759
|
-
if (config.checkIssuer && result.payload.iss !== config.issuer) return next();
|
|
760
|
-
|
|
761
|
-
(req as any).user = result.payload;
|
|
762
|
-
next();
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
// ── authorize middleware ────────────────────────────────────────────────
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* Reject the request with 401 when `req.user` is not set.
|
|
769
|
-
* Always place this **after** {@link authenticate}.
|
|
770
|
-
*/
|
|
771
|
-
const authorize: Middleware = (req: RouterRequest, res: RouterResponse, next: () => void): void => {
|
|
772
|
-
if (!(req as any).user) {
|
|
773
|
-
sendJson(res, 401, { error: 'Authentication required' });
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
next();
|
|
777
|
-
};
|
|
778
|
-
|
|
779
|
-
// ── requireRole factory ─────────────────────────────────────────────────
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* Return a two-element middleware chain `[authenticate, roleCheck]` that
|
|
783
|
-
* allows the request to proceed only when the authenticated user holds at
|
|
784
|
-
* least one of the specified roles.
|
|
785
|
-
*
|
|
786
|
-
* Responds with 401 when unauthenticated, 403 when none of the required
|
|
787
|
-
* roles are present.
|
|
788
|
-
*
|
|
789
|
-
* BUG FIX: the original accessed `req.user.roles` without first checking
|
|
790
|
-
* that `req.user` exists, throwing a TypeError for unauthenticated requests.
|
|
791
|
-
*/
|
|
792
|
-
function requireRole(...roles: string[]): Middleware[] {
|
|
793
|
-
return [
|
|
794
|
-
authenticate,
|
|
795
|
-
(req: RouterRequest, res: RouterResponse, next: () => void): void => {
|
|
796
|
-
const user = (req as any).user as TokenPayload | undefined;
|
|
797
|
-
if (!user) {
|
|
798
|
-
sendJson(res, 401, { error: 'Authentication required' });
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
const userRoles = user.roles ?? [];
|
|
802
|
-
if (!roles.some((r) => userRoles.includes(r))) {
|
|
803
|
-
sendJson(res, 403, {
|
|
804
|
-
error: `Access denied. Required role(s): ${roles.join(', ')}`,
|
|
805
|
-
yourRoles: userRoles,
|
|
806
|
-
});
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
next();
|
|
810
|
-
},
|
|
811
|
-
];
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// ── requirePermission factory ───────────────────────────────────────────
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Return a two-element middleware chain `[authenticate, permCheck]` that
|
|
818
|
-
* allows the request to proceed only when the authenticated user holds
|
|
819
|
-
* **all** of the specified permissions.
|
|
820
|
-
*
|
|
821
|
-
* Responds with 401 when unauthenticated, 403 when any permission is absent.
|
|
822
|
-
*
|
|
823
|
-
* BUG FIX: same `req.user` undefined-access issue as `requireRole`.
|
|
824
|
-
*/
|
|
825
|
-
function requirePermission(...permissions: string[]): Middleware[] {
|
|
826
|
-
return [
|
|
827
|
-
authenticate,
|
|
828
|
-
(req: RouterRequest, res: RouterResponse, next: () => void): void => {
|
|
829
|
-
const user = (req as any).user as TokenPayload | undefined;
|
|
830
|
-
if (!user) {
|
|
831
|
-
sendJson(res, 401, { error: 'Authentication required' });
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
const userPerms = user.permissions ?? [];
|
|
835
|
-
if (!permissions.every((p) => userPerms.includes(p))) {
|
|
836
|
-
sendJson(res, 403, {
|
|
837
|
-
error: `Insufficient permissions. Required: ${permissions.join(', ')}`,
|
|
838
|
-
yourPermissions: userPerms,
|
|
839
|
-
});
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
next();
|
|
843
|
-
},
|
|
844
|
-
];
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
return {
|
|
848
|
-
login,
|
|
849
|
-
refresh,
|
|
850
|
-
logout,
|
|
851
|
-
authenticate,
|
|
852
|
-
authorize,
|
|
853
|
-
requireRole,
|
|
854
|
-
requirePermission,
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
export default createJwtPlugin;
|
|
859
|
-
|
|
860
|
-
// Re-export low-level utilities for testing and advanced use cases.
|
|
861
|
-
export { signToken, verifyToken, hashPassword as _hashPassword };
|