@veloxts/auth 0.6.84 → 0.6.85
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/CHANGELOG.md +9 -0
- package/dist/adapter.d.ts +35 -17
- package/dist/adapter.js +33 -17
- package/dist/adapters/auth0.d.ts +316 -0
- package/dist/adapters/auth0.js +539 -0
- package/dist/adapters/clerk.d.ts +281 -0
- package/dist/adapters/clerk.js +314 -0
- package/dist/adapters/index.d.ts +46 -0
- package/dist/adapters/index.js +44 -0
- package/dist/adapters/utils.d.ts +31 -0
- package/dist/adapters/utils.js +49 -0
- package/dist/rate-limit.js +85 -57
- package/package.json +5 -5
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth0 Adapter for @veloxts/auth
|
|
3
|
+
*
|
|
4
|
+
* Integrates Auth0 (https://auth0.com) with VeloxTS's pluggable
|
|
5
|
+
* authentication system. Auth0 is an identity platform providing
|
|
6
|
+
* authentication and authorization services.
|
|
7
|
+
*
|
|
8
|
+
* This adapter uses JWKS (JSON Web Key Sets) for JWT verification,
|
|
9
|
+
* allowing secure token validation without sharing secrets.
|
|
10
|
+
*
|
|
11
|
+
* @module auth/adapters/auth0
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { createAuthAdapterPlugin } from '@veloxts/auth';
|
|
16
|
+
* import { createAuth0Adapter } from '@veloxts/auth/adapters/auth0';
|
|
17
|
+
*
|
|
18
|
+
* const adapter = createAuth0Adapter({
|
|
19
|
+
* domain: process.env.AUTH0_DOMAIN!,
|
|
20
|
+
* audience: process.env.AUTH0_AUDIENCE!,
|
|
21
|
+
* clientId: process.env.AUTH0_CLIENT_ID,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Simplified API - just pass the adapter
|
|
25
|
+
* const authPlugin = createAuthAdapterPlugin(adapter);
|
|
26
|
+
*
|
|
27
|
+
* app.use(authPlugin);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { AuthAdapterError, BaseAuthAdapter } from '../adapter.js';
|
|
31
|
+
import { extractBearerToken, validateNonEmptyString } from './utils.js';
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Constants
|
|
34
|
+
// ============================================================================
|
|
35
|
+
/** Default clock tolerance in seconds for JWT validation */
|
|
36
|
+
const DEFAULT_CLOCK_TOLERANCE_SECONDS = 5;
|
|
37
|
+
/** Default JWKS cache TTL in milliseconds (1 hour) */
|
|
38
|
+
const DEFAULT_JWKS_CACHE_TTL_MS = 3600000;
|
|
39
|
+
/** Minimum interval between JWKS refresh attempts in milliseconds (5 seconds) */
|
|
40
|
+
const MIN_JWKS_REFRESH_INTERVAL_MS = 5000;
|
|
41
|
+
class JWKSCache {
|
|
42
|
+
keys = new Map();
|
|
43
|
+
lastFetch = 0;
|
|
44
|
+
lastRefreshAttempt = 0;
|
|
45
|
+
refreshPromise = null;
|
|
46
|
+
ttl;
|
|
47
|
+
jwksUrl;
|
|
48
|
+
logger;
|
|
49
|
+
constructor(jwksUrl, ttl = DEFAULT_JWKS_CACHE_TTL_MS, logger) {
|
|
50
|
+
this.jwksUrl = jwksUrl;
|
|
51
|
+
this.ttl = ttl;
|
|
52
|
+
this.logger = logger;
|
|
53
|
+
}
|
|
54
|
+
async getKey(kid) {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
// Refresh cache if expired, but rate limit refresh attempts
|
|
57
|
+
const needsRefresh = now - this.lastFetch > this.ttl || this.keys.size === 0;
|
|
58
|
+
const canRefresh = now - this.lastRefreshAttempt > MIN_JWKS_REFRESH_INTERVAL_MS;
|
|
59
|
+
if (needsRefresh && canRefresh) {
|
|
60
|
+
// Use promise-based locking to prevent concurrent refreshes
|
|
61
|
+
if (!this.refreshPromise) {
|
|
62
|
+
this.lastRefreshAttempt = now;
|
|
63
|
+
this.refreshPromise = this.refresh().finally(() => {
|
|
64
|
+
this.refreshPromise = null;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
await this.refreshPromise;
|
|
68
|
+
}
|
|
69
|
+
return this.keys.get(kid) ?? null;
|
|
70
|
+
}
|
|
71
|
+
async refresh() {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(this.jwksUrl);
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`JWKS fetch failed: ${response.status}`);
|
|
76
|
+
}
|
|
77
|
+
const data = (await response.json());
|
|
78
|
+
this.keys.clear();
|
|
79
|
+
for (const key of data.keys) {
|
|
80
|
+
if (key.kid) {
|
|
81
|
+
this.keys.set(key.kid, key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.lastFetch = Date.now();
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// If we have cached keys, don't fail completely
|
|
88
|
+
if (this.keys.size > 0) {
|
|
89
|
+
this.logger?.('JWKS refresh failed, using cached keys');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Default JWT Verifier
|
|
98
|
+
// ============================================================================
|
|
99
|
+
/**
|
|
100
|
+
* Create a default JWT verifier using JWKS
|
|
101
|
+
*
|
|
102
|
+
* This implementation uses Web Crypto API for JWT verification
|
|
103
|
+
* to avoid external dependencies like 'jose'.
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
function createDefaultVerifier(domain, audience, issuer, clockTolerance, cacheTtl, logger) {
|
|
108
|
+
const jwksUrl = `https://${domain}/.well-known/jwks.json`;
|
|
109
|
+
const cache = new JWKSCache(jwksUrl, cacheTtl, logger);
|
|
110
|
+
return {
|
|
111
|
+
async verify(token) {
|
|
112
|
+
// Parse the JWT
|
|
113
|
+
const parts = token.split('.');
|
|
114
|
+
if (parts.length !== 3) {
|
|
115
|
+
throw new Error('Invalid JWT format');
|
|
116
|
+
}
|
|
117
|
+
// Decode and validate header structure
|
|
118
|
+
const headerJson = base64UrlDecode(parts[0]);
|
|
119
|
+
const header = parseAndValidateHeader(headerJson);
|
|
120
|
+
if (header.alg !== 'RS256') {
|
|
121
|
+
throw new Error(`Unsupported algorithm: ${header.alg}`);
|
|
122
|
+
}
|
|
123
|
+
if (!header.kid) {
|
|
124
|
+
throw new Error('Missing kid in token header');
|
|
125
|
+
}
|
|
126
|
+
// Get the signing key from JWKS
|
|
127
|
+
const key = await cache.getKey(header.kid);
|
|
128
|
+
if (!key) {
|
|
129
|
+
throw new Error(`Unknown signing key: ${header.kid}`);
|
|
130
|
+
}
|
|
131
|
+
// Verify signature using Web Crypto
|
|
132
|
+
const isValid = await verifyRS256Signature(token, key);
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
throw new Error('Invalid token signature');
|
|
135
|
+
}
|
|
136
|
+
// Decode and validate claims structure
|
|
137
|
+
const payloadJson = base64UrlDecode(parts[1]);
|
|
138
|
+
const claims = parseAndValidateClaims(payloadJson);
|
|
139
|
+
const now = Math.floor(Date.now() / 1000);
|
|
140
|
+
// Validate expiration
|
|
141
|
+
if (claims.exp && claims.exp + clockTolerance < now) {
|
|
142
|
+
throw new Error('Token has expired');
|
|
143
|
+
}
|
|
144
|
+
// Validate not before
|
|
145
|
+
if (claims.nbf && claims.nbf - clockTolerance > now) {
|
|
146
|
+
throw new Error('Token not yet valid');
|
|
147
|
+
}
|
|
148
|
+
// Validate issuer
|
|
149
|
+
if (claims.iss !== issuer) {
|
|
150
|
+
throw new Error(`Invalid issuer: expected ${issuer}, got ${claims.iss}`);
|
|
151
|
+
}
|
|
152
|
+
// Validate audience
|
|
153
|
+
const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
154
|
+
if (!audiences.includes(audience)) {
|
|
155
|
+
throw new Error(`Invalid audience: expected ${audience}`);
|
|
156
|
+
}
|
|
157
|
+
return claims;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Base64URL decode
|
|
163
|
+
*
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
function base64UrlDecode(str) {
|
|
167
|
+
// Replace URL-safe characters
|
|
168
|
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
169
|
+
// Pad if necessary
|
|
170
|
+
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
|
171
|
+
// Decode
|
|
172
|
+
return Buffer.from(padded, 'base64').toString('utf-8');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parse and validate JWT header structure
|
|
176
|
+
*
|
|
177
|
+
* @param headerJson - JSON string of the JWT header
|
|
178
|
+
* @returns Validated JWT header
|
|
179
|
+
* @throws Error if header is malformed or missing required fields
|
|
180
|
+
*
|
|
181
|
+
* @internal
|
|
182
|
+
*/
|
|
183
|
+
function parseAndValidateHeader(headerJson) {
|
|
184
|
+
let parsed;
|
|
185
|
+
try {
|
|
186
|
+
parsed = JSON.parse(headerJson);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw new Error('Invalid JWT format - malformed header JSON');
|
|
190
|
+
}
|
|
191
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
192
|
+
throw new Error('Invalid JWT format - header must be an object');
|
|
193
|
+
}
|
|
194
|
+
const header = parsed;
|
|
195
|
+
if (typeof header.alg !== 'string') {
|
|
196
|
+
throw new Error('Invalid JWT format - missing or invalid alg in header');
|
|
197
|
+
}
|
|
198
|
+
if (typeof header.kid !== 'string') {
|
|
199
|
+
throw new Error('Invalid JWT format - missing or invalid kid in header');
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
alg: header.alg,
|
|
203
|
+
kid: header.kid,
|
|
204
|
+
typ: typeof header.typ === 'string' ? header.typ : undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Parse and validate JWT claims structure
|
|
209
|
+
*
|
|
210
|
+
* Validates that required Auth0 JWT claims are present and have correct types.
|
|
211
|
+
*
|
|
212
|
+
* @param payloadJson - JSON string of the JWT payload
|
|
213
|
+
* @returns Validated Auth0 claims
|
|
214
|
+
* @throws Error if payload is malformed or missing required fields
|
|
215
|
+
*
|
|
216
|
+
* @internal
|
|
217
|
+
*/
|
|
218
|
+
function parseAndValidateClaims(payloadJson) {
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(payloadJson);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error('Invalid JWT format - malformed payload JSON');
|
|
225
|
+
}
|
|
226
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
227
|
+
throw new Error('Invalid JWT format - payload must be an object');
|
|
228
|
+
}
|
|
229
|
+
const payload = parsed;
|
|
230
|
+
// Validate required claims
|
|
231
|
+
if (typeof payload.sub !== 'string') {
|
|
232
|
+
throw new Error('Invalid JWT claims - missing or invalid sub');
|
|
233
|
+
}
|
|
234
|
+
if (typeof payload.iat !== 'number') {
|
|
235
|
+
throw new Error('Invalid JWT claims - missing or invalid iat');
|
|
236
|
+
}
|
|
237
|
+
if (typeof payload.exp !== 'number') {
|
|
238
|
+
throw new Error('Invalid JWT claims - missing or invalid exp');
|
|
239
|
+
}
|
|
240
|
+
if (typeof payload.iss !== 'string') {
|
|
241
|
+
throw new Error('Invalid JWT claims - missing or invalid iss');
|
|
242
|
+
}
|
|
243
|
+
if (payload.aud !== undefined && typeof payload.aud !== 'string' && !Array.isArray(payload.aud)) {
|
|
244
|
+
throw new Error('Invalid JWT claims - invalid aud format');
|
|
245
|
+
}
|
|
246
|
+
// Build validated claims object
|
|
247
|
+
const claims = {
|
|
248
|
+
sub: payload.sub,
|
|
249
|
+
iat: payload.iat,
|
|
250
|
+
exp: payload.exp,
|
|
251
|
+
iss: payload.iss,
|
|
252
|
+
aud: payload.aud,
|
|
253
|
+
};
|
|
254
|
+
// Add optional claims if present and valid
|
|
255
|
+
if (typeof payload.nbf === 'number') {
|
|
256
|
+
claims.nbf = payload.nbf;
|
|
257
|
+
}
|
|
258
|
+
if (typeof payload.azp === 'string') {
|
|
259
|
+
claims.azp = payload.azp;
|
|
260
|
+
}
|
|
261
|
+
if (typeof payload.scope === 'string') {
|
|
262
|
+
claims.scope = payload.scope;
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(payload.permissions)) {
|
|
265
|
+
claims.permissions = payload.permissions;
|
|
266
|
+
}
|
|
267
|
+
if (typeof payload.org_id === 'string') {
|
|
268
|
+
claims.org_id = payload.org_id;
|
|
269
|
+
}
|
|
270
|
+
if (typeof payload.org_name === 'string') {
|
|
271
|
+
claims.org_name = payload.org_name;
|
|
272
|
+
}
|
|
273
|
+
if (typeof payload.email === 'string') {
|
|
274
|
+
claims.email = payload.email;
|
|
275
|
+
}
|
|
276
|
+
if (typeof payload.email_verified === 'boolean') {
|
|
277
|
+
claims.email_verified = payload.email_verified;
|
|
278
|
+
}
|
|
279
|
+
if (typeof payload.name === 'string') {
|
|
280
|
+
claims.name = payload.name;
|
|
281
|
+
}
|
|
282
|
+
if (typeof payload.nickname === 'string') {
|
|
283
|
+
claims.nickname = payload.nickname;
|
|
284
|
+
}
|
|
285
|
+
if (typeof payload.picture === 'string') {
|
|
286
|
+
claims.picture = payload.picture;
|
|
287
|
+
}
|
|
288
|
+
if (typeof payload.updated_at === 'string') {
|
|
289
|
+
claims.updated_at = payload.updated_at;
|
|
290
|
+
}
|
|
291
|
+
return claims;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Verify RS256 signature using Web Crypto API
|
|
295
|
+
*
|
|
296
|
+
* @internal
|
|
297
|
+
*/
|
|
298
|
+
async function verifyRS256Signature(token, jwk) {
|
|
299
|
+
const parts = token.split('.');
|
|
300
|
+
const signatureInput = `${parts[0]}.${parts[1]}`;
|
|
301
|
+
const signature = base64UrlToArrayBuffer(parts[2]);
|
|
302
|
+
// Import the public key
|
|
303
|
+
const cryptoKey = await crypto.subtle.importKey('jwk', {
|
|
304
|
+
kty: jwk.kty,
|
|
305
|
+
n: jwk.n,
|
|
306
|
+
e: jwk.e,
|
|
307
|
+
alg: 'RS256',
|
|
308
|
+
use: 'sig',
|
|
309
|
+
}, {
|
|
310
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
311
|
+
hash: 'SHA-256',
|
|
312
|
+
}, false, ['verify']);
|
|
313
|
+
// Verify the signature
|
|
314
|
+
const encoder = new TextEncoder();
|
|
315
|
+
return crypto.subtle.verify('RSASSA-PKCS1-v1_5', cryptoKey, signature, encoder.encode(signatureInput));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Convert base64url string to ArrayBuffer
|
|
319
|
+
*
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
function base64UrlToArrayBuffer(str) {
|
|
323
|
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
324
|
+
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
|
325
|
+
const binary = Buffer.from(padded, 'base64');
|
|
326
|
+
return binary.buffer.slice(binary.byteOffset, binary.byteOffset + binary.byteLength);
|
|
327
|
+
}
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// Auth0 Adapter Implementation
|
|
330
|
+
// ============================================================================
|
|
331
|
+
/**
|
|
332
|
+
* Auth0 Adapter
|
|
333
|
+
*
|
|
334
|
+
* Integrates Auth0 with VeloxTS by:
|
|
335
|
+
* - Verifying Auth0 JWTs using JWKS
|
|
336
|
+
* - Extracting user data from token claims
|
|
337
|
+
* - Supporting Auth0 Organizations and RBAC
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```typescript
|
|
341
|
+
* const adapter = new Auth0Adapter();
|
|
342
|
+
* const plugin = createAuthAdapterPlugin({
|
|
343
|
+
* adapter,
|
|
344
|
+
* config: {
|
|
345
|
+
* name: 'auth0',
|
|
346
|
+
* domain: 'your-tenant.auth0.com',
|
|
347
|
+
* audience: 'https://your-api.example.com',
|
|
348
|
+
* },
|
|
349
|
+
* });
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
export class Auth0Adapter extends BaseAuthAdapter {
|
|
353
|
+
verifier = null;
|
|
354
|
+
domain = '';
|
|
355
|
+
clientId;
|
|
356
|
+
authHeader = 'authorization';
|
|
357
|
+
constructor() {
|
|
358
|
+
super('auth0', '1.0.0');
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Initialize the adapter with Auth0 configuration
|
|
362
|
+
*/
|
|
363
|
+
async initialize(fastify, config) {
|
|
364
|
+
await super.initialize(fastify, config);
|
|
365
|
+
// Validate required configuration using shared utility
|
|
366
|
+
try {
|
|
367
|
+
this.domain = validateNonEmptyString(config.domain, 'Auth0 domain');
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
throw new AuthAdapterError('Auth0 domain is required and cannot be empty', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
371
|
+
}
|
|
372
|
+
let audience;
|
|
373
|
+
try {
|
|
374
|
+
audience = validateNonEmptyString(config.audience, 'Auth0 audience');
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
throw new AuthAdapterError('Auth0 audience is required and cannot be empty', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
378
|
+
}
|
|
379
|
+
this.clientId = config.clientId;
|
|
380
|
+
this.authHeader = config.authHeader ?? 'authorization';
|
|
381
|
+
// Construct issuer URL
|
|
382
|
+
const issuer = config.issuer ?? `https://${this.domain}/`;
|
|
383
|
+
// Use custom verifier or create default
|
|
384
|
+
// Pass bound debug method directly to avoid closure overhead
|
|
385
|
+
this.verifier =
|
|
386
|
+
config.jwtVerifier ??
|
|
387
|
+
createDefaultVerifier(this.domain, audience, issuer, config.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS, config.jwksCacheTtl ?? DEFAULT_JWKS_CACHE_TTL_MS, this.debug.bind(this));
|
|
388
|
+
this.debug(`Initialized with domain: ${this.domain}`);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get session from Auth0 JWT
|
|
392
|
+
*
|
|
393
|
+
* Extracts the Bearer token from the Authorization header
|
|
394
|
+
* and verifies it using JWKS.
|
|
395
|
+
*/
|
|
396
|
+
async getSession(request) {
|
|
397
|
+
if (!this.verifier) {
|
|
398
|
+
throw new AuthAdapterError('Auth0 adapter not initialized', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
399
|
+
}
|
|
400
|
+
// Extract token from Authorization header
|
|
401
|
+
const authHeaderValue = request.headers[this.authHeader];
|
|
402
|
+
if (!authHeaderValue || typeof authHeaderValue !== 'string') {
|
|
403
|
+
this.debug('No authorization header found');
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
// Extract Bearer token
|
|
407
|
+
const token = extractBearerToken(authHeaderValue);
|
|
408
|
+
if (!token) {
|
|
409
|
+
this.debug('No Bearer token found in authorization header');
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
// Verify the token
|
|
414
|
+
const claims = await this.verifier.verify(token);
|
|
415
|
+
this.debug(`Token verified for user: ${claims.sub}`);
|
|
416
|
+
// Validate azp if clientId is configured
|
|
417
|
+
if (this.clientId && claims.azp && claims.azp !== this.clientId) {
|
|
418
|
+
this.debug(`Invalid authorized party: expected ${this.clientId}, got ${claims.azp}`);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
// Transform to VeloxTS format
|
|
422
|
+
return transformAuth0Session(claims);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
// Token verification failed
|
|
426
|
+
if (error instanceof Error) {
|
|
427
|
+
this.debug(`Token verification failed: ${error.message}`);
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get routes for Auth0
|
|
434
|
+
*
|
|
435
|
+
* Auth0 handles auth on the client side via their SDK.
|
|
436
|
+
* Server only needs to verify tokens, not handle auth routes.
|
|
437
|
+
*
|
|
438
|
+
* If you need to handle Auth0 webhooks or Actions callbacks,
|
|
439
|
+
* override this method.
|
|
440
|
+
*/
|
|
441
|
+
getRoutes() {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Clean up adapter resources
|
|
446
|
+
*/
|
|
447
|
+
async cleanup() {
|
|
448
|
+
await super.cleanup();
|
|
449
|
+
this.verifier = null;
|
|
450
|
+
this.debug('Adapter cleaned up');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// Helper Functions
|
|
455
|
+
// ============================================================================
|
|
456
|
+
/**
|
|
457
|
+
* Transform Auth0 claims to VeloxTS format
|
|
458
|
+
*
|
|
459
|
+
* @param claims - Verified JWT claims
|
|
460
|
+
* @returns VeloxTS adapter session result
|
|
461
|
+
*
|
|
462
|
+
* @internal
|
|
463
|
+
*/
|
|
464
|
+
function transformAuth0Session(claims) {
|
|
465
|
+
// Generate a synthetic session ID from sub and iat
|
|
466
|
+
const sessionId = `${claims.sub}:${claims.iat}`;
|
|
467
|
+
return {
|
|
468
|
+
user: {
|
|
469
|
+
id: claims.sub,
|
|
470
|
+
email: claims.email ?? 'unknown',
|
|
471
|
+
name: claims.name ?? claims.nickname,
|
|
472
|
+
emailVerified: claims.email_verified,
|
|
473
|
+
image: claims.picture,
|
|
474
|
+
providerData: {
|
|
475
|
+
// Include permissions (RBAC)
|
|
476
|
+
...(claims.permissions && { permissions: claims.permissions }),
|
|
477
|
+
// Include scopes
|
|
478
|
+
...(claims.scope && { scope: claims.scope.split(' ') }),
|
|
479
|
+
// Include organization data if present
|
|
480
|
+
...(claims.org_id && { organizationId: claims.org_id }),
|
|
481
|
+
...(claims.org_name && { organizationName: claims.org_name }),
|
|
482
|
+
// Include other useful claims
|
|
483
|
+
...(claims.nickname && { nickname: claims.nickname }),
|
|
484
|
+
...(claims.updated_at && { updatedAt: claims.updated_at }),
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
session: {
|
|
488
|
+
sessionId,
|
|
489
|
+
userId: claims.sub,
|
|
490
|
+
expiresAt: claims.exp * 1000, // Convert to Unix ms
|
|
491
|
+
isActive: true,
|
|
492
|
+
providerData: {
|
|
493
|
+
issuedAt: claims.iat * 1000,
|
|
494
|
+
issuer: claims.iss,
|
|
495
|
+
...(claims.azp && { authorizedParty: claims.azp }),
|
|
496
|
+
...(claims.aud && { audience: claims.aud }),
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// ============================================================================
|
|
502
|
+
// Factory Function
|
|
503
|
+
// ============================================================================
|
|
504
|
+
/**
|
|
505
|
+
* Create an Auth0 adapter
|
|
506
|
+
*
|
|
507
|
+
* This is the recommended way to create an Auth0 adapter.
|
|
508
|
+
* It returns an adapter instance with the configuration attached.
|
|
509
|
+
*
|
|
510
|
+
* @param config - Adapter configuration
|
|
511
|
+
* @returns Auth0 adapter with configuration
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* ```typescript
|
|
515
|
+
* import { createAuth0Adapter } from '@veloxts/auth/adapters/auth0';
|
|
516
|
+
* import { createAuthAdapterPlugin } from '@veloxts/auth';
|
|
517
|
+
*
|
|
518
|
+
* const adapter = createAuth0Adapter({
|
|
519
|
+
* domain: process.env.AUTH0_DOMAIN!,
|
|
520
|
+
* audience: process.env.AUTH0_AUDIENCE!,
|
|
521
|
+
* clientId: process.env.AUTH0_CLIENT_ID, // Optional
|
|
522
|
+
* debug: process.env.NODE_ENV === 'development',
|
|
523
|
+
* });
|
|
524
|
+
*
|
|
525
|
+
* // Simplified API - just pass the adapter
|
|
526
|
+
* const authPlugin = createAuthAdapterPlugin(adapter);
|
|
527
|
+
*
|
|
528
|
+
* app.use(authPlugin);
|
|
529
|
+
* ```
|
|
530
|
+
*/
|
|
531
|
+
export function createAuth0Adapter(config) {
|
|
532
|
+
const adapter = new Auth0Adapter();
|
|
533
|
+
// Attach config for easy access when creating plugin
|
|
534
|
+
return Object.assign(adapter, { config });
|
|
535
|
+
}
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// Re-exports
|
|
538
|
+
// ============================================================================
|
|
539
|
+
export { AuthAdapterError } from '../adapter.js';
|