@adonisjs/auth 10.0.0 → 10.1.0
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/build/auth_manager-Cp_ofh4p.js +384 -0
- package/build/debug-CrTUB4zl.js +12 -0
- package/build/{errors-sGy-K8pd.js → errors-eDV8ejO0.js} +114 -2
- package/build/index.js +46 -4
- package/build/modules/access_tokens_guard/main.js +757 -2
- package/build/modules/basic_auth_guard/main.js +224 -2
- package/build/modules/session_guard/main.js +716 -2
- package/build/providers/auth_provider.js +24 -3
- package/build/services/auth.js +5 -0
- package/build/src/middleware/initialize_auth_middleware.js +29 -0
- package/build/src/mixins/lucid.js +94 -1
- package/build/src/plugins/japa/api_client.js +26 -1
- package/build/src/plugins/japa/browser_client.js +24 -1
- package/build/{symbols-BQLDWwuQ.js → symbols-C5QEqFvJ.js} +10 -1
- package/package.json +20 -20
- package/build/auth_manager-hJTiBA2V.js +0 -129
- package/build/debug-Ckko95-M.js +0 -3
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
import { n as E_UNAUTHORIZED_ACCESS } from "../../errors-
|
|
2
|
-
import "../../symbols-
|
|
1
|
+
import { n as E_UNAUTHORIZED_ACCESS } from "../../errors-eDV8ejO0.js";
|
|
2
|
+
import "../../symbols-C5QEqFvJ.js";
|
|
3
3
|
import { RuntimeException } from "@adonisjs/core/exceptions";
|
|
4
4
|
import { inspect } from "node:util";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import string from "@adonisjs/core/helpers/string";
|
|
7
7
|
import { Secret, base64, safeEqual } from "@adonisjs/core/helpers";
|
|
8
|
+
//#region modules/access_tokens_guard/crc32.ts
|
|
9
|
+
/**
|
|
10
|
+
* We use CRC32 just to add a recognizable checksum to tokens. This helps
|
|
11
|
+
* secret scanning tools like https://docs.github.com/en/github/administering-a-repository/about-secret-scanning easily detect tokens generated by a given program.
|
|
12
|
+
*
|
|
13
|
+
* You can learn more about appending checksum to a hash here in this Github
|
|
14
|
+
* article. https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/
|
|
15
|
+
*
|
|
16
|
+
* Code taken from:
|
|
17
|
+
* https://github.com/tsxper/crc32/blob/main/src/CRC32.ts
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* CRC32 implementation for adding recognizable checksums to tokens.
|
|
21
|
+
* This helps secret scanning tools easily detect tokens generated by AdonisJS.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const crc = new CRC32()
|
|
25
|
+
* const checksum = crc.calculate('my-secret-token')
|
|
26
|
+
* console.log('Checksum:', checksum)
|
|
27
|
+
*/
|
|
8
28
|
var CRC32 = class {
|
|
29
|
+
/**
|
|
30
|
+
* Lookup table calculated for 0xEDB88320 divisor
|
|
31
|
+
*/
|
|
9
32
|
#lookupTable = [
|
|
10
33
|
0,
|
|
11
34
|
1996959894,
|
|
@@ -284,21 +307,97 @@ var CRC32 = class {
|
|
|
284
307
|
if (num >= 0) return num;
|
|
285
308
|
return 4294967295 - num * -1 + 1;
|
|
286
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Calculate CRC32 checksum for a string input
|
|
312
|
+
*
|
|
313
|
+
* @param input - The string to calculate checksum for
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* const crc = new CRC32()
|
|
317
|
+
* const checksum = crc.calculate('hello-world')
|
|
318
|
+
* console.log('CRC32:', checksum)
|
|
319
|
+
*/
|
|
287
320
|
calculate(input) {
|
|
288
321
|
return this.forString(input);
|
|
289
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Calculate CRC32 checksum for a string
|
|
325
|
+
*
|
|
326
|
+
* @param input - The string to process
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* const crc = new CRC32()
|
|
330
|
+
* const result = crc.forString('test-string')
|
|
331
|
+
*/
|
|
290
332
|
forString(input) {
|
|
291
333
|
const bytes = this.#strToBytes(input);
|
|
292
334
|
return this.forBytes(bytes);
|
|
293
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Calculate CRC32 checksum for byte array
|
|
338
|
+
*
|
|
339
|
+
* @param bytes - The byte array to process
|
|
340
|
+
* @param accumulator - Optional accumulator for chained calculations
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* const crc = new CRC32()
|
|
344
|
+
* const bytes = new TextEncoder().encode('hello')
|
|
345
|
+
* const result = crc.forBytes(bytes)
|
|
346
|
+
*/
|
|
294
347
|
forBytes(bytes, accumulator) {
|
|
295
348
|
const crc = this.#calculateBytes(bytes, accumulator);
|
|
296
349
|
return this.#crcToUint(crc);
|
|
297
350
|
}
|
|
298
351
|
};
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region modules/access_tokens_guard/access_token.ts
|
|
354
|
+
/**
|
|
355
|
+
* Access token represents a token created for a user to authenticate
|
|
356
|
+
* using the auth module.
|
|
357
|
+
*
|
|
358
|
+
* It encapsulates the logic of creating an opaque token, generating
|
|
359
|
+
* its hash and verifying its hash.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* const token = new AccessToken({
|
|
363
|
+
* identifier: 1,
|
|
364
|
+
* tokenableId: 123,
|
|
365
|
+
* type: 'api',
|
|
366
|
+
* hash: 'sha256hash',
|
|
367
|
+
* createdAt: new Date(),
|
|
368
|
+
* updatedAt: new Date(),
|
|
369
|
+
* lastUsedAt: null,
|
|
370
|
+
* expiresAt: null,
|
|
371
|
+
* name: 'API Token',
|
|
372
|
+
* abilities: ['read', 'write']
|
|
373
|
+
* })
|
|
374
|
+
*/
|
|
299
375
|
var AccessToken = class {
|
|
376
|
+
/**
|
|
377
|
+
* Decodes a publicly shared token and return the series
|
|
378
|
+
* and the token value from it.
|
|
379
|
+
*
|
|
380
|
+
* Returns null when unable to decode the token because of
|
|
381
|
+
* invalid format or encoding.
|
|
382
|
+
*
|
|
383
|
+
* @param prefix - The token prefix to validate against
|
|
384
|
+
* @param value - The token value to decode
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* const decoded = AccessToken.decode('oat_', 'oat_abc123.def456')
|
|
388
|
+
* if (decoded) {
|
|
389
|
+
* console.log('Token ID:', decoded.identifier)
|
|
390
|
+
* console.log('Secret:', decoded.secret.release())
|
|
391
|
+
* }
|
|
392
|
+
*/
|
|
300
393
|
static decode(prefix, value) {
|
|
394
|
+
/**
|
|
395
|
+
* Ensure value is a string and starts with the prefix.
|
|
396
|
+
*/
|
|
301
397
|
if (typeof value !== "string" || !value.startsWith(`${prefix}`)) return null;
|
|
398
|
+
/**
|
|
399
|
+
* Remove prefix from the rest of the token.
|
|
400
|
+
*/
|
|
302
401
|
const token = value.replace(new RegExp(`^${prefix}`), "");
|
|
303
402
|
if (!token) return null;
|
|
304
403
|
const [identifier, ...tokenValue] = token.split(".");
|
|
@@ -311,6 +410,18 @@ var AccessToken = class {
|
|
|
311
410
|
secret: new Secret(decodedSecret)
|
|
312
411
|
};
|
|
313
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Creates a transient token that can be shared with the persistence
|
|
415
|
+
* layer.
|
|
416
|
+
*
|
|
417
|
+
* @param userId - The ID of the user for whom the token is created
|
|
418
|
+
* @param size - The size of the random secret to generate
|
|
419
|
+
* @param expiresIn - Optional expiration time (seconds or duration string)
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* const transientToken = AccessToken.createTransientToken(123, 32, '7d')
|
|
423
|
+
* // Store transientToken in database
|
|
424
|
+
*/
|
|
314
425
|
static createTransientToken(userId, size, expiresIn) {
|
|
315
426
|
let expiresAt;
|
|
316
427
|
if (expiresIn) {
|
|
@@ -323,6 +434,18 @@ var AccessToken = class {
|
|
|
323
434
|
...this.seed(size)
|
|
324
435
|
};
|
|
325
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Creates a secret opaque token and its hash. The secret is
|
|
439
|
+
* suffixed with a crc32 checksum for secret scanning tools
|
|
440
|
+
* to easily identify the token.
|
|
441
|
+
*
|
|
442
|
+
* @param size - The size of the random string to generate
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* const { secret, hash } = AccessToken.seed(32)
|
|
446
|
+
* console.log('Secret:', secret.release())
|
|
447
|
+
* console.log('Hash:', hash)
|
|
448
|
+
*/
|
|
326
449
|
static seed(size) {
|
|
327
450
|
const seed = string.random(size);
|
|
328
451
|
const secret = new Secret(`${seed}${new CRC32().calculate(seed)}`);
|
|
@@ -331,17 +454,76 @@ var AccessToken = class {
|
|
|
331
454
|
hash: createHash("sha256").update(secret.release()).digest("hex")
|
|
332
455
|
};
|
|
333
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Identifer is a unique sequence to identify the
|
|
459
|
+
* token within database. It should be the
|
|
460
|
+
* primary/unique key
|
|
461
|
+
*/
|
|
334
462
|
identifier;
|
|
463
|
+
/**
|
|
464
|
+
* Reference to the user id for whom the token
|
|
465
|
+
* is generated.
|
|
466
|
+
*/
|
|
335
467
|
tokenableId;
|
|
468
|
+
/**
|
|
469
|
+
* The value is a public representation of a token. It is created
|
|
470
|
+
* by combining the "identifier"."secret"
|
|
471
|
+
*/
|
|
336
472
|
value;
|
|
473
|
+
/**
|
|
474
|
+
* Recognizable name for the token
|
|
475
|
+
*/
|
|
337
476
|
name;
|
|
477
|
+
/**
|
|
478
|
+
* A unique type to identify a bucket of tokens inside the
|
|
479
|
+
* storage layer.
|
|
480
|
+
*/
|
|
338
481
|
type;
|
|
482
|
+
/**
|
|
483
|
+
* Hash is computed from the seed to later verify the validity
|
|
484
|
+
* of seed
|
|
485
|
+
*/
|
|
339
486
|
hash;
|
|
487
|
+
/**
|
|
488
|
+
* Date/time when the token instance was created
|
|
489
|
+
*/
|
|
340
490
|
createdAt;
|
|
491
|
+
/**
|
|
492
|
+
* Date/time when the token was updated
|
|
493
|
+
*/
|
|
341
494
|
updatedAt;
|
|
495
|
+
/**
|
|
496
|
+
* Timestamp at which the token was used for authentication
|
|
497
|
+
*/
|
|
342
498
|
lastUsedAt;
|
|
499
|
+
/**
|
|
500
|
+
* Timestamp at which the token will expire
|
|
501
|
+
*/
|
|
343
502
|
expiresAt;
|
|
503
|
+
/**
|
|
504
|
+
* An array of abilities the token can perform. The abilities
|
|
505
|
+
* is an array of abritary string values
|
|
506
|
+
*/
|
|
344
507
|
abilities;
|
|
508
|
+
/**
|
|
509
|
+
* Creates a new AccessToken instance
|
|
510
|
+
*
|
|
511
|
+
* @param attributes - Token attributes including identifier, user ID, type, hash, etc.
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* const token = new AccessToken({
|
|
515
|
+
* identifier: 1,
|
|
516
|
+
* tokenableId: 123,
|
|
517
|
+
* type: 'api',
|
|
518
|
+
* hash: 'sha256hash',
|
|
519
|
+
* createdAt: new Date(),
|
|
520
|
+
* updatedAt: new Date(),
|
|
521
|
+
* lastUsedAt: null,
|
|
522
|
+
* expiresAt: new Date(Date.now() + 86400000),
|
|
523
|
+
* name: 'Mobile App Token',
|
|
524
|
+
* abilities: ['read:posts', 'write:posts']
|
|
525
|
+
* })
|
|
526
|
+
*/
|
|
345
527
|
constructor(attributes) {
|
|
346
528
|
this.identifier = attributes.identifier;
|
|
347
529
|
this.tokenableId = attributes.tokenableId;
|
|
@@ -353,28 +535,95 @@ var AccessToken = class {
|
|
|
353
535
|
this.expiresAt = attributes.expiresAt;
|
|
354
536
|
this.lastUsedAt = attributes.lastUsedAt;
|
|
355
537
|
this.abilities = attributes.abilities || ["*"];
|
|
538
|
+
/**
|
|
539
|
+
* Compute value when secret is provided
|
|
540
|
+
*/
|
|
356
541
|
if (attributes.secret) {
|
|
357
542
|
if (!attributes.prefix) throw new RuntimeException("Cannot compute token value without the prefix");
|
|
358
543
|
this.value = new Secret(`${attributes.prefix}${base64.urlEncode(String(this.identifier))}.${base64.urlEncode(attributes.secret.release())}`);
|
|
359
544
|
}
|
|
360
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Check if the token allows the given ability.
|
|
548
|
+
*
|
|
549
|
+
* @param ability - The ability to check
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* if (token.allows('read:posts')) {
|
|
553
|
+
* console.log('User can read posts')
|
|
554
|
+
* }
|
|
555
|
+
*/
|
|
361
556
|
allows(ability) {
|
|
362
557
|
return this.abilities.includes(ability) || this.abilities.includes("*");
|
|
363
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Check if the token denies the ability.
|
|
561
|
+
*
|
|
562
|
+
* @param ability - The ability to check
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* if (token.denies('delete:posts')) {
|
|
566
|
+
* console.log('User cannot delete posts')
|
|
567
|
+
* }
|
|
568
|
+
*/
|
|
364
569
|
denies(ability) {
|
|
365
570
|
return !this.abilities.includes(ability) && !this.abilities.includes("*");
|
|
366
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Authorize ability access using the current access token
|
|
574
|
+
*
|
|
575
|
+
* @param ability - The ability to authorize
|
|
576
|
+
*
|
|
577
|
+
* @throws {E_UNAUTHORIZED_ACCESS} When the token denies the ability
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* token.authorize('write:posts') // Throws if not allowed
|
|
581
|
+
* console.log('Authorization successful')
|
|
582
|
+
*/
|
|
367
583
|
authorize(ability) {
|
|
368
584
|
if (this.denies(ability)) throw new E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: "access_tokens" });
|
|
369
585
|
}
|
|
586
|
+
/**
|
|
587
|
+
* Check if the token has been expired. Verifies
|
|
588
|
+
* the "expiresAt" timestamp with the current
|
|
589
|
+
* date.
|
|
590
|
+
*
|
|
591
|
+
* Tokens with no expiry never expire
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* if (token.isExpired()) {
|
|
595
|
+
* console.log('Token has expired')
|
|
596
|
+
* } else {
|
|
597
|
+
* console.log('Token is still valid')
|
|
598
|
+
* }
|
|
599
|
+
*/
|
|
370
600
|
isExpired() {
|
|
371
601
|
if (!this.expiresAt) return false;
|
|
372
602
|
return this.expiresAt < /* @__PURE__ */ new Date();
|
|
373
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Verifies the value of a token against the pre-defined hash
|
|
606
|
+
*
|
|
607
|
+
* @param secret - The secret to verify against the stored hash
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* const isValid = token.verify(new Secret('user-provided-secret'))
|
|
611
|
+
* if (isValid) {
|
|
612
|
+
* console.log('Token is valid')
|
|
613
|
+
* }
|
|
614
|
+
*/
|
|
374
615
|
verify(secret) {
|
|
375
616
|
const newHash = createHash("sha256").update(secret.release()).digest("hex");
|
|
376
617
|
return safeEqual(this.hash, newHash);
|
|
377
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Converts the token to a JSON representation suitable for API responses
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* const tokenData = token.toJSON()
|
|
624
|
+
* console.log(tokenData.type) // 'bearer'
|
|
625
|
+
* console.log(tokenData.token) // 'oat_abc123.def456'
|
|
626
|
+
*/
|
|
378
627
|
toJSON() {
|
|
379
628
|
return {
|
|
380
629
|
type: "bearer",
|
|
@@ -386,21 +635,95 @@ var AccessToken = class {
|
|
|
386
635
|
};
|
|
387
636
|
}
|
|
388
637
|
};
|
|
638
|
+
//#endregion
|
|
639
|
+
//#region modules/access_tokens_guard/guard.ts
|
|
640
|
+
/**
|
|
641
|
+
* Implementation of access tokens guard for the Auth layer. The heavy lifting
|
|
642
|
+
* of verifying tokens is done by the user provider. However, the guard is
|
|
643
|
+
* used to seamlessly integrate with the auth layer of the package.
|
|
644
|
+
*
|
|
645
|
+
* @template UserProvider - The user provider contract
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* const guard = new AccessTokensGuard(
|
|
649
|
+
* 'api',
|
|
650
|
+
* ctx,
|
|
651
|
+
* emitter,
|
|
652
|
+
* userProvider
|
|
653
|
+
* )
|
|
654
|
+
*
|
|
655
|
+
* const user = await guard.authenticate()
|
|
656
|
+
* console.log('Authenticated user:', user.email)
|
|
657
|
+
*/
|
|
389
658
|
var AccessTokensGuard = class {
|
|
659
|
+
/**
|
|
660
|
+
* A unique name for the guard.
|
|
661
|
+
*/
|
|
390
662
|
#name;
|
|
663
|
+
/**
|
|
664
|
+
* Reference to the current HTTP context
|
|
665
|
+
*/
|
|
391
666
|
#ctx;
|
|
667
|
+
/**
|
|
668
|
+
* Provider to lookup user details
|
|
669
|
+
*/
|
|
392
670
|
#userProvider;
|
|
671
|
+
/**
|
|
672
|
+
* Emitter to emit events
|
|
673
|
+
*/
|
|
393
674
|
#emitter;
|
|
675
|
+
/**
|
|
676
|
+
* Driver name of the guard
|
|
677
|
+
*/
|
|
394
678
|
driverName = "access_tokens";
|
|
679
|
+
/**
|
|
680
|
+
* Whether or not the authentication has been attempted
|
|
681
|
+
* during the current request.
|
|
682
|
+
*/
|
|
395
683
|
authenticationAttempted = false;
|
|
684
|
+
/**
|
|
685
|
+
* A boolean to know if the current request has
|
|
686
|
+
* been authenticated
|
|
687
|
+
*/
|
|
396
688
|
isAuthenticated = false;
|
|
689
|
+
/**
|
|
690
|
+
* Reference to an instance of the authenticated user.
|
|
691
|
+
* The value only exists after calling one of the
|
|
692
|
+
* following methods.
|
|
693
|
+
*
|
|
694
|
+
* - authenticate
|
|
695
|
+
* - check
|
|
696
|
+
*
|
|
697
|
+
* You can use the "getUserOrFail" method to throw an exception if
|
|
698
|
+
* the request is not authenticated.
|
|
699
|
+
*/
|
|
397
700
|
user;
|
|
701
|
+
/**
|
|
702
|
+
* Creates a new AccessTokensGuard instance
|
|
703
|
+
*
|
|
704
|
+
* @param name - Unique name for the guard instance
|
|
705
|
+
* @param ctx - HTTP context for the current request
|
|
706
|
+
* @param emitter - Event emitter for guard events
|
|
707
|
+
* @param userProvider - User provider for token verification
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* const guard = new AccessTokensGuard(
|
|
711
|
+
* 'api',
|
|
712
|
+
* ctx,
|
|
713
|
+
* emitter,
|
|
714
|
+
* new TokenUserProvider()
|
|
715
|
+
* )
|
|
716
|
+
*/
|
|
398
717
|
constructor(name, ctx, emitter, userProvider) {
|
|
399
718
|
this.#name = name;
|
|
400
719
|
this.#ctx = ctx;
|
|
401
720
|
this.#emitter = emitter;
|
|
402
721
|
this.#userProvider = userProvider;
|
|
403
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* Emits authentication failure and returns an exception
|
|
725
|
+
* to end the authentication cycle.
|
|
726
|
+
*/
|
|
404
727
|
#authenticationFailed() {
|
|
405
728
|
const error = new E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: this.driverName });
|
|
406
729
|
this.#emitter.emit("access_tokens_auth:authentication_failed", {
|
|
@@ -410,30 +733,82 @@ var AccessTokensGuard = class {
|
|
|
410
733
|
});
|
|
411
734
|
return error;
|
|
412
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Returns the bearer token from the request headers or fails
|
|
738
|
+
*/
|
|
413
739
|
#getBearerToken() {
|
|
414
740
|
const [type, token] = this.#ctx.request.header("authorization", "").split(" ");
|
|
415
741
|
if (!type || type.toLowerCase() !== "bearer" || !token) throw this.#authenticationFailed();
|
|
416
742
|
return token;
|
|
417
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Returns an instance of the authenticated user. Or throws
|
|
746
|
+
* an exception if the request is not authenticated.
|
|
747
|
+
*
|
|
748
|
+
* @throws {E_UNAUTHORIZED_ACCESS} When user is not authenticated
|
|
749
|
+
*
|
|
750
|
+
* @example
|
|
751
|
+
* const user = guard.getUserOrFail()
|
|
752
|
+
* console.log('User ID:', user.id)
|
|
753
|
+
* console.log('Current token:', user.currentAccessToken.name)
|
|
754
|
+
*/
|
|
418
755
|
getUserOrFail() {
|
|
419
756
|
if (!this.user) throw new E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: this.driverName });
|
|
420
757
|
return this.user;
|
|
421
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Authenticate the current HTTP request by verifying the bearer
|
|
761
|
+
* token or fails with an exception
|
|
762
|
+
*
|
|
763
|
+
* @throws {E_UNAUTHORIZED_ACCESS} When authentication fails
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* try {
|
|
767
|
+
* const user = await guard.authenticate()
|
|
768
|
+
* console.log('Authenticated as:', user.email)
|
|
769
|
+
* console.log('Token abilities:', user.currentAccessToken.abilities)
|
|
770
|
+
* } catch (error) {
|
|
771
|
+
* console.log('Authentication failed')
|
|
772
|
+
* }
|
|
773
|
+
*/
|
|
422
774
|
async authenticate() {
|
|
775
|
+
/**
|
|
776
|
+
* Return early when authentication has already
|
|
777
|
+
* been attempted
|
|
778
|
+
*/
|
|
423
779
|
if (this.authenticationAttempted) return this.getUserOrFail();
|
|
780
|
+
/**
|
|
781
|
+
* Notify we begin to attempt the authentication
|
|
782
|
+
*/
|
|
424
783
|
this.authenticationAttempted = true;
|
|
425
784
|
this.#emitter.emit("access_tokens_auth:authentication_attempted", {
|
|
426
785
|
ctx: this.#ctx,
|
|
427
786
|
guardName: this.#name
|
|
428
787
|
});
|
|
788
|
+
/**
|
|
789
|
+
* Decode token or fail when unable to do so
|
|
790
|
+
*/
|
|
429
791
|
const bearerToken = new Secret(this.#getBearerToken());
|
|
792
|
+
/**
|
|
793
|
+
* Verify for token via the user provider
|
|
794
|
+
*/
|
|
430
795
|
const token = await this.#userProvider.verifyToken(bearerToken);
|
|
431
796
|
if (!token) throw this.#authenticationFailed();
|
|
797
|
+
/**
|
|
798
|
+
* Check if a user for the token exists. Otherwise abort
|
|
799
|
+
* authentication
|
|
800
|
+
*/
|
|
432
801
|
const providerUser = await this.#userProvider.findById(token.tokenableId);
|
|
433
802
|
if (!providerUser) throw this.#authenticationFailed();
|
|
803
|
+
/**
|
|
804
|
+
* Update local state
|
|
805
|
+
*/
|
|
434
806
|
this.isAuthenticated = true;
|
|
435
807
|
this.user = providerUser.getOriginal();
|
|
436
808
|
this.user.currentAccessToken = token;
|
|
809
|
+
/**
|
|
810
|
+
* Notify
|
|
811
|
+
*/
|
|
437
812
|
this.#emitter.emit("access_tokens_auth:authentication_succeeded", {
|
|
438
813
|
ctx: this.#ctx,
|
|
439
814
|
token,
|
|
@@ -442,16 +817,61 @@ var AccessTokensGuard = class {
|
|
|
442
817
|
});
|
|
443
818
|
return this.user;
|
|
444
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Create a token for a user (sign in)
|
|
822
|
+
*
|
|
823
|
+
* @param user - The user to create a token for
|
|
824
|
+
* @param abilities - Optional array of abilities the token should have
|
|
825
|
+
* @param options - Optional token configuration
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* const token = await guard.createToken(user, ['read', 'write'], {
|
|
829
|
+
* name: 'Mobile App',
|
|
830
|
+
* expiresIn: '7d'
|
|
831
|
+
* })
|
|
832
|
+
* console.log('Token:', token.value.release())
|
|
833
|
+
*/
|
|
445
834
|
async createToken(user, abilities, options) {
|
|
446
835
|
return await this.#userProvider.createToken(user, abilities, options);
|
|
447
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Invalidates the currently authenticated token (sign out)
|
|
839
|
+
*
|
|
840
|
+
* @example
|
|
841
|
+
* await guard.invalidateToken()
|
|
842
|
+
* console.log('Token invalidated successfully')
|
|
843
|
+
*/
|
|
448
844
|
async invalidateToken() {
|
|
449
845
|
const bearerToken = new Secret(this.#getBearerToken());
|
|
450
846
|
return await this.#userProvider.invalidateToken(bearerToken);
|
|
451
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Returns the Authorization header clients can use to authenticate
|
|
850
|
+
* the request.
|
|
851
|
+
*
|
|
852
|
+
* @param user - The user to authenticate as
|
|
853
|
+
* @param abilities - Optional array of abilities
|
|
854
|
+
* @param options - Optional token configuration
|
|
855
|
+
*
|
|
856
|
+
* @example
|
|
857
|
+
* const clientAuth = await guard.authenticateAsClient(user, ['read'])
|
|
858
|
+
* // Use clientAuth.headers.authorization in API tests
|
|
859
|
+
*/
|
|
452
860
|
async authenticateAsClient(user, abilities, options) {
|
|
453
861
|
return { headers: { authorization: `Bearer ${(await this.#userProvider.createToken(user, abilities, options)).value.release()}` } };
|
|
454
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* Silently check if the user is authenticated or not. The
|
|
865
|
+
* method is same as the "authenticate" method but does not
|
|
866
|
+
* throw any exceptions.
|
|
867
|
+
*
|
|
868
|
+
* @example
|
|
869
|
+
* const isAuthenticated = await guard.check()
|
|
870
|
+
* if (isAuthenticated) {
|
|
871
|
+
* const user = guard.user
|
|
872
|
+
* console.log('User is authenticated:', user.email)
|
|
873
|
+
* }
|
|
874
|
+
*/
|
|
455
875
|
async check() {
|
|
456
876
|
try {
|
|
457
877
|
await this.authenticate();
|
|
@@ -462,17 +882,79 @@ var AccessTokensGuard = class {
|
|
|
462
882
|
}
|
|
463
883
|
}
|
|
464
884
|
};
|
|
885
|
+
//#endregion
|
|
886
|
+
//#region modules/access_tokens_guard/token_providers/db.ts
|
|
887
|
+
/**
|
|
888
|
+
* DbAccessTokensProvider uses lucid database service to fetch and
|
|
889
|
+
* persist tokens for a given user.
|
|
890
|
+
*
|
|
891
|
+
* The user must be an instance of the associated user model.
|
|
892
|
+
*
|
|
893
|
+
* @template TokenableModel - The Lucid model that can have tokens
|
|
894
|
+
*
|
|
895
|
+
* @example
|
|
896
|
+
* const provider = new DbAccessTokensProvider({
|
|
897
|
+
* tokenableModel: () => import('#models/user'),
|
|
898
|
+
* table: 'api_tokens',
|
|
899
|
+
* type: 'api_token',
|
|
900
|
+
* prefix: 'api_'
|
|
901
|
+
* })
|
|
902
|
+
*/
|
|
465
903
|
var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
904
|
+
/**
|
|
905
|
+
* Create tokens provider instance for a given Lucid model
|
|
906
|
+
*
|
|
907
|
+
* @param model - The tokenable model factory function
|
|
908
|
+
* @param options - Optional configuration options
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* const provider = DbAccessTokensProvider.forModel(
|
|
912
|
+
* () => import('#models/user'),
|
|
913
|
+
* { prefix: 'api_', type: 'api_token' }
|
|
914
|
+
* )
|
|
915
|
+
*/
|
|
466
916
|
static forModel(model, options) {
|
|
467
917
|
return new DbAccessTokensProvider({
|
|
468
918
|
tokenableModel: model,
|
|
469
919
|
...options || {}
|
|
470
920
|
});
|
|
471
921
|
}
|
|
922
|
+
/**
|
|
923
|
+
* A unique type for the value. The type is used to identify a
|
|
924
|
+
* bucket of tokens within the storage layer.
|
|
925
|
+
*
|
|
926
|
+
* Defaults to auth_token
|
|
927
|
+
*/
|
|
472
928
|
type;
|
|
929
|
+
/**
|
|
930
|
+
* A unique prefix to append to the publicly shared token value.
|
|
931
|
+
*
|
|
932
|
+
* Defaults to oat
|
|
933
|
+
*/
|
|
473
934
|
prefix;
|
|
935
|
+
/**
|
|
936
|
+
* Database table to use for querying access tokens
|
|
937
|
+
*/
|
|
474
938
|
table;
|
|
939
|
+
/**
|
|
940
|
+
* The length for the token secret. A secret is a cryptographically
|
|
941
|
+
* secure random string.
|
|
942
|
+
*/
|
|
475
943
|
tokenSecretLength;
|
|
944
|
+
/**
|
|
945
|
+
* Creates a new DbAccessTokensProvider instance
|
|
946
|
+
*
|
|
947
|
+
* @param options - Configuration options for the provider
|
|
948
|
+
*
|
|
949
|
+
* @example
|
|
950
|
+
* const provider = new DbAccessTokensProvider({
|
|
951
|
+
* tokenableModel: () => import('#models/user'),
|
|
952
|
+
* table: 'auth_access_tokens',
|
|
953
|
+
* tokenSecretLength: 40,
|
|
954
|
+
* type: 'auth_token',
|
|
955
|
+
* prefix: 'oat_'
|
|
956
|
+
* })
|
|
957
|
+
*/
|
|
476
958
|
constructor(options) {
|
|
477
959
|
this.options = options;
|
|
478
960
|
this.table = options.table || "auth_access_tokens";
|
|
@@ -480,14 +962,35 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
480
962
|
this.type = options.type || "auth_token";
|
|
481
963
|
this.prefix = options.prefix || "oat_";
|
|
482
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Check if value is an object
|
|
967
|
+
*/
|
|
483
968
|
#isObject(value) {
|
|
484
969
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
485
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Ensure the provided user is an instance of the user model and
|
|
973
|
+
* has a primary key
|
|
974
|
+
*/
|
|
486
975
|
#ensureIsPersisted(user) {
|
|
487
976
|
const model = this.options.tokenableModel;
|
|
488
977
|
if (user instanceof model === false) throw new RuntimeException(`Invalid user object. It must be an instance of the "${model.name}" model`);
|
|
489
978
|
if (!user.$primaryKeyValue) throw new RuntimeException(`Cannot use "${model.name}" model for managing access tokens. The value of column "${model.primaryKey}" is undefined or null`);
|
|
490
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* Maps a database row to an AccessToken instance
|
|
982
|
+
*
|
|
983
|
+
* @param dbRow - The database row containing token data
|
|
984
|
+
*
|
|
985
|
+
* @example
|
|
986
|
+
* const token = provider.dbRowToAccessToken({
|
|
987
|
+
* id: 1,
|
|
988
|
+
* tokenable_id: 123,
|
|
989
|
+
* type: 'auth_token',
|
|
990
|
+
* hash: 'sha256hash',
|
|
991
|
+
* // ... other columns
|
|
992
|
+
* })
|
|
993
|
+
*/
|
|
491
994
|
dbRowToAccessToken(dbRow) {
|
|
492
995
|
return new AccessToken({
|
|
493
996
|
identifier: dbRow.id,
|
|
@@ -502,14 +1005,44 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
502
1005
|
expiresAt: typeof dbRow.expires_at === "number" ? new Date(dbRow.expires_at) : dbRow.expires_at
|
|
503
1006
|
});
|
|
504
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Returns a query client instance from the parent model
|
|
1010
|
+
*
|
|
1011
|
+
* @example
|
|
1012
|
+
* const db = await provider.getDb()
|
|
1013
|
+
* const tokens = await db.from('auth_access_tokens').select('*')
|
|
1014
|
+
*/
|
|
505
1015
|
async getDb() {
|
|
506
1016
|
const model = this.options.tokenableModel;
|
|
507
1017
|
return model.$adapter.query(model).client;
|
|
508
1018
|
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Create a token for a user
|
|
1021
|
+
*
|
|
1022
|
+
* @param user - The user instance to create a token for
|
|
1023
|
+
* @param abilities - Array of abilities the token should have
|
|
1024
|
+
* @param options - Optional token configuration
|
|
1025
|
+
*
|
|
1026
|
+
* @example
|
|
1027
|
+
* const token = await provider.create(user, ['read', 'write'], {
|
|
1028
|
+
* name: 'Mobile App Token',
|
|
1029
|
+
* expiresIn: '7d'
|
|
1030
|
+
* })
|
|
1031
|
+
* console.log('Token:', token.value.release())
|
|
1032
|
+
*/
|
|
509
1033
|
async create(user, abilities = ["*"], options) {
|
|
510
1034
|
this.#ensureIsPersisted(user);
|
|
511
1035
|
const queryClient = await this.getDb();
|
|
1036
|
+
/**
|
|
1037
|
+
* Creating a transient token. Transient token abstracts
|
|
1038
|
+
* the logic of creating a random secure secret and its
|
|
1039
|
+
* hash
|
|
1040
|
+
*/
|
|
512
1041
|
const transientToken = AccessToken.createTransientToken(user.$primaryKeyValue, this.tokenSecretLength, options?.expiresIn || this.options.expiresIn);
|
|
1042
|
+
/**
|
|
1043
|
+
* Row to insert inside the database. We expect exactly these
|
|
1044
|
+
* columns to exist.
|
|
1045
|
+
*/
|
|
513
1046
|
const dbRow = {
|
|
514
1047
|
tokenable_id: transientToken.userId,
|
|
515
1048
|
type: this.type,
|
|
@@ -521,9 +1054,19 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
521
1054
|
last_used_at: null,
|
|
522
1055
|
expires_at: transientToken.expiresAt || null
|
|
523
1056
|
};
|
|
1057
|
+
/**
|
|
1058
|
+
* Insert data to the database.
|
|
1059
|
+
*/
|
|
524
1060
|
const result = await queryClient.table(this.table).insert(dbRow).returning("id");
|
|
525
1061
|
const id = this.#isObject(result[0]) ? result[0].id : result[0];
|
|
1062
|
+
/**
|
|
1063
|
+
* Throw error when unable to find id in the return value of
|
|
1064
|
+
* the insert query
|
|
1065
|
+
*/
|
|
526
1066
|
if (!id) throw new RuntimeException(`Cannot save access token. The result "${inspect(result)}" of insert query is unexpected`);
|
|
1067
|
+
/**
|
|
1068
|
+
* Convert db row to an access token
|
|
1069
|
+
*/
|
|
527
1070
|
return new AccessToken({
|
|
528
1071
|
identifier: id,
|
|
529
1072
|
tokenableId: dbRow.tokenable_id,
|
|
@@ -539,6 +1082,18 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
539
1082
|
expiresAt: dbRow.expires_at
|
|
540
1083
|
});
|
|
541
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Find a token for a user by the token id
|
|
1087
|
+
*
|
|
1088
|
+
* @param user - The user instance that owns the token
|
|
1089
|
+
* @param identifier - The token identifier to search for
|
|
1090
|
+
*
|
|
1091
|
+
* @example
|
|
1092
|
+
* const token = await provider.find(user, 123)
|
|
1093
|
+
* if (token) {
|
|
1094
|
+
* console.log('Found token:', token.name)
|
|
1095
|
+
* }
|
|
1096
|
+
*/
|
|
542
1097
|
async find(user, identifier) {
|
|
543
1098
|
this.#ensureIsPersisted(user);
|
|
544
1099
|
const dbRow = await (await this.getDb()).query().from(this.table).where({
|
|
@@ -549,6 +1104,16 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
549
1104
|
if (!dbRow) return null;
|
|
550
1105
|
return this.dbRowToAccessToken(dbRow);
|
|
551
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Delete a token by its id
|
|
1109
|
+
*
|
|
1110
|
+
* @param user - The user instance that owns the token
|
|
1111
|
+
* @param identifier - The token identifier to delete
|
|
1112
|
+
*
|
|
1113
|
+
* @example
|
|
1114
|
+
* const deletedCount = await provider.delete(user, 123)
|
|
1115
|
+
* console.log('Deleted tokens:', deletedCount)
|
|
1116
|
+
*/
|
|
552
1117
|
async delete(user, identifier) {
|
|
553
1118
|
this.#ensureIsPersisted(user);
|
|
554
1119
|
return await (await this.getDb()).query().from(this.table).where({
|
|
@@ -557,6 +1122,15 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
557
1122
|
type: this.type
|
|
558
1123
|
}).del().exec();
|
|
559
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Delete all tokens for a given user
|
|
1127
|
+
*
|
|
1128
|
+
* @param user - The user instance to delete tokens for
|
|
1129
|
+
*
|
|
1130
|
+
* @example
|
|
1131
|
+
* const deletedCount = await provider.deleteAll(user)
|
|
1132
|
+
* console.log('Deleted tokens:', deletedCount)
|
|
1133
|
+
*/
|
|
560
1134
|
async deleteAll(user) {
|
|
561
1135
|
this.#ensureIsPersisted(user);
|
|
562
1136
|
return await (await this.getDb()).query().from(this.table).where({
|
|
@@ -564,6 +1138,16 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
564
1138
|
type: this.type
|
|
565
1139
|
}).del().exec();
|
|
566
1140
|
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Returns all the tokens for a given user
|
|
1143
|
+
*
|
|
1144
|
+
* @param user - The user instance to get tokens for
|
|
1145
|
+
*
|
|
1146
|
+
* @example
|
|
1147
|
+
* const tokens = await provider.all(user)
|
|
1148
|
+
* console.log('User has', tokens.length, 'tokens')
|
|
1149
|
+
* tokens.forEach(token => console.log(token.name))
|
|
1150
|
+
*/
|
|
567
1151
|
async all(user) {
|
|
568
1152
|
this.#ensureIsPersisted(user);
|
|
569
1153
|
return (await (await this.getDb()).query().from(this.table).where({
|
|
@@ -585,6 +1169,21 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
585
1169
|
return this.dbRowToAccessToken(dbRow);
|
|
586
1170
|
});
|
|
587
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Verifies a publicly shared access token and returns an
|
|
1174
|
+
* access token for it.
|
|
1175
|
+
*
|
|
1176
|
+
* Returns null when unable to verify the token or find it
|
|
1177
|
+
* inside the storage
|
|
1178
|
+
*
|
|
1179
|
+
* @param tokenValue - The token value to verify
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* const token = await provider.verify(new Secret('oat_abc123.def456'))
|
|
1183
|
+
* if (token && !token.isExpired()) {
|
|
1184
|
+
* console.log('Valid token for user:', token.tokenableId)
|
|
1185
|
+
* }
|
|
1186
|
+
*/
|
|
588
1187
|
async verify(tokenValue) {
|
|
589
1188
|
const decodedToken = AccessToken.decode(this.prefix, tokenValue.release());
|
|
590
1189
|
if (!decodedToken) return null;
|
|
@@ -594,15 +1193,35 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
594
1193
|
type: this.type
|
|
595
1194
|
}).limit(1).first();
|
|
596
1195
|
if (!dbRow) return null;
|
|
1196
|
+
/**
|
|
1197
|
+
* Update last time the token is used
|
|
1198
|
+
*/
|
|
597
1199
|
dbRow.last_used_at = /* @__PURE__ */ new Date();
|
|
598
1200
|
await db.from(this.table).where({
|
|
599
1201
|
id: dbRow.id,
|
|
600
1202
|
type: dbRow.type
|
|
601
1203
|
}).update({ last_used_at: dbRow.last_used_at });
|
|
1204
|
+
/**
|
|
1205
|
+
* Convert to access token instance
|
|
1206
|
+
*/
|
|
602
1207
|
const accessToken = this.dbRowToAccessToken(dbRow);
|
|
1208
|
+
/**
|
|
1209
|
+
* Ensure the token secret matches the token hash
|
|
1210
|
+
*/
|
|
603
1211
|
if (!accessToken.verify(decodedToken.secret) || accessToken.isExpired()) return null;
|
|
604
1212
|
return accessToken;
|
|
605
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Invalidates a token identified by its publicly shared token
|
|
1216
|
+
*
|
|
1217
|
+
* @param tokenValue - The token value to invalidate
|
|
1218
|
+
*
|
|
1219
|
+
* @example
|
|
1220
|
+
* const wasInvalidated = await provider.invalidate(new Secret('oat_abc123.def456'))
|
|
1221
|
+
* if (wasInvalidated) {
|
|
1222
|
+
* console.log('Token successfully invalidated')
|
|
1223
|
+
* }
|
|
1224
|
+
*/
|
|
606
1225
|
async invalidate(tokenValue) {
|
|
607
1226
|
const decodedToken = AccessToken.decode(this.prefix, tokenValue.release());
|
|
608
1227
|
if (!decodedToken) return false;
|
|
@@ -613,26 +1232,83 @@ var DbAccessTokensProvider = class DbAccessTokensProvider {
|
|
|
613
1232
|
return Boolean(deleteCount);
|
|
614
1233
|
}
|
|
615
1234
|
};
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region modules/access_tokens_guard/user_providers/lucid.ts
|
|
1237
|
+
/**
|
|
1238
|
+
* Uses a lucid model to verify access tokens and find a user during
|
|
1239
|
+
* authentication
|
|
1240
|
+
*
|
|
1241
|
+
* @template TokenableProperty - The property name that holds the tokens provider
|
|
1242
|
+
* @template UserModel - The Lucid model representing the user
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* const userProvider = new AccessTokensLucidUserProvider({
|
|
1246
|
+
* model: () => import('#models/user'),
|
|
1247
|
+
* tokens: 'accessTokens'
|
|
1248
|
+
* })
|
|
1249
|
+
*/
|
|
616
1250
|
var AccessTokensLucidUserProvider = class {
|
|
1251
|
+
/**
|
|
1252
|
+
* Reference to the lazily imported model
|
|
1253
|
+
*/
|
|
617
1254
|
model;
|
|
1255
|
+
/**
|
|
1256
|
+
* Creates a new AccessTokensLucidUserProvider instance
|
|
1257
|
+
*
|
|
1258
|
+
* @param options - Configuration options for the user provider
|
|
1259
|
+
*
|
|
1260
|
+
* @example
|
|
1261
|
+
* const provider = new AccessTokensLucidUserProvider({
|
|
1262
|
+
* model: () => import('#models/user'),
|
|
1263
|
+
* tokens: 'accessTokens'
|
|
1264
|
+
* })
|
|
1265
|
+
*/
|
|
618
1266
|
constructor(options) {
|
|
619
1267
|
this.options = options;
|
|
620
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Imports the model from the provider, returns and caches it
|
|
1271
|
+
* for further operations.
|
|
1272
|
+
*
|
|
1273
|
+
* @example
|
|
1274
|
+
* const UserModel = await provider.getModel()
|
|
1275
|
+
* const user = await UserModel.find(1)
|
|
1276
|
+
*/
|
|
621
1277
|
async getModel() {
|
|
622
1278
|
if (this.model && !("hot" in import.meta)) return this.model;
|
|
623
1279
|
this.model = (await this.options.model()).default;
|
|
624
1280
|
return this.model;
|
|
625
1281
|
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Returns the tokens provider associated with the user model
|
|
1284
|
+
*
|
|
1285
|
+
* @example
|
|
1286
|
+
* const tokensProvider = await provider.getTokensProvider()
|
|
1287
|
+
* const token = await tokensProvider.create(user, ['read'])
|
|
1288
|
+
*/
|
|
626
1289
|
async getTokensProvider() {
|
|
627
1290
|
const model = await this.getModel();
|
|
628
1291
|
if (!model[this.options.tokens]) throw new RuntimeException(`Cannot use "${model.name}" model for verifying access tokens. Make sure to assign a token provider to the model.`);
|
|
629
1292
|
return model[this.options.tokens];
|
|
630
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Creates an adapter user for the guard
|
|
1296
|
+
*
|
|
1297
|
+
* @param user - The user model instance
|
|
1298
|
+
*
|
|
1299
|
+
* @example
|
|
1300
|
+
* const guardUser = await provider.createUserForGuard(user)
|
|
1301
|
+
* console.log('User ID:', guardUser.getId())
|
|
1302
|
+
* console.log('Original user:', guardUser.getOriginal())
|
|
1303
|
+
*/
|
|
631
1304
|
async createUserForGuard(user) {
|
|
632
1305
|
const model = await this.getModel();
|
|
633
1306
|
if (user instanceof model === false) throw new RuntimeException(`Invalid user object. It must be an instance of the "${model.name}" model`);
|
|
634
1307
|
return {
|
|
635
1308
|
getId() {
|
|
1309
|
+
/**
|
|
1310
|
+
* Ensure user has a primary key
|
|
1311
|
+
*/
|
|
636
1312
|
if (!user.$primaryKeyValue) throw new RuntimeException(`Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null`);
|
|
637
1313
|
return user.$primaryKeyValue;
|
|
638
1314
|
},
|
|
@@ -641,21 +1317,87 @@ var AccessTokensLucidUserProvider = class {
|
|
|
641
1317
|
}
|
|
642
1318
|
};
|
|
643
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Create a token for a given user
|
|
1322
|
+
*
|
|
1323
|
+
* @param user - The user to create a token for
|
|
1324
|
+
* @param abilities - Optional array of abilities the token should have
|
|
1325
|
+
* @param options - Optional token configuration
|
|
1326
|
+
*
|
|
1327
|
+
* @example
|
|
1328
|
+
* const token = await provider.createToken(user, ['read', 'write'], {
|
|
1329
|
+
* name: 'API Token',
|
|
1330
|
+
* expiresIn: '30d'
|
|
1331
|
+
* })
|
|
1332
|
+
* console.log('Created token:', token.value.release())
|
|
1333
|
+
*/
|
|
644
1334
|
async createToken(user, abilities, options) {
|
|
645
1335
|
return (await this.getTokensProvider()).create(user, abilities, options);
|
|
646
1336
|
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Invalidates a token identified by its publicly shared token
|
|
1339
|
+
*
|
|
1340
|
+
* @param tokenValue - The token value to invalidate
|
|
1341
|
+
*
|
|
1342
|
+
* @example
|
|
1343
|
+
* const wasInvalidated = await provider.invalidateToken(
|
|
1344
|
+
* new Secret('oat_abc123.def456')
|
|
1345
|
+
* )
|
|
1346
|
+
* console.log('Token invalidated:', wasInvalidated)
|
|
1347
|
+
*/
|
|
647
1348
|
async invalidateToken(tokenValue) {
|
|
648
1349
|
return (await this.getTokensProvider()).invalidate(tokenValue);
|
|
649
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Finds a user by the user id
|
|
1353
|
+
*
|
|
1354
|
+
* @param identifier - The user identifier to search for
|
|
1355
|
+
*
|
|
1356
|
+
* @example
|
|
1357
|
+
* const guardUser = await provider.findById(123)
|
|
1358
|
+
* if (guardUser) {
|
|
1359
|
+
* const originalUser = guardUser.getOriginal()
|
|
1360
|
+
* console.log('Found user:', originalUser.email)
|
|
1361
|
+
* }
|
|
1362
|
+
*/
|
|
650
1363
|
async findById(identifier) {
|
|
651
1364
|
const user = await (await this.getModel()).find(identifier);
|
|
652
1365
|
if (!user) return null;
|
|
653
1366
|
return this.createUserForGuard(user);
|
|
654
1367
|
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Verifies a publicly shared access token and returns an
|
|
1370
|
+
* access token for it.
|
|
1371
|
+
*
|
|
1372
|
+
* @param tokenValue - The token value to verify
|
|
1373
|
+
*
|
|
1374
|
+
* @example
|
|
1375
|
+
* const token = await provider.verifyToken(
|
|
1376
|
+
* new Secret('oat_abc123.def456')
|
|
1377
|
+
* )
|
|
1378
|
+
* if (token && !token.isExpired()) {
|
|
1379
|
+
* console.log('Valid token with abilities:', token.abilities)
|
|
1380
|
+
* }
|
|
1381
|
+
*/
|
|
655
1382
|
async verifyToken(tokenValue) {
|
|
656
1383
|
return (await this.getTokensProvider()).verify(tokenValue);
|
|
657
1384
|
}
|
|
658
1385
|
};
|
|
1386
|
+
//#endregion
|
|
1387
|
+
//#region modules/access_tokens_guard/define_config.ts
|
|
1388
|
+
/**
|
|
1389
|
+
* Configures access tokens guard for authentication
|
|
1390
|
+
*
|
|
1391
|
+
* @param config - Configuration object containing the user provider
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* const guard = tokensGuard({
|
|
1395
|
+
* provider: tokensUserProvider({
|
|
1396
|
+
* model: () => import('#models/user'),
|
|
1397
|
+
* tokens: 'accessTokens'
|
|
1398
|
+
* })
|
|
1399
|
+
* })
|
|
1400
|
+
*/
|
|
659
1401
|
function tokensGuard(config) {
|
|
660
1402
|
return { async resolver(name, app) {
|
|
661
1403
|
const emitter = await app.container.make("emitter");
|
|
@@ -663,7 +1405,20 @@ function tokensGuard(config) {
|
|
|
663
1405
|
return (ctx) => new AccessTokensGuard(name, ctx, emitter, provider);
|
|
664
1406
|
} };
|
|
665
1407
|
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Configures user provider that uses Lucid models to verify
|
|
1410
|
+
* access tokens and find users during authentication.
|
|
1411
|
+
*
|
|
1412
|
+
* @param config - Configuration options for the Lucid user provider
|
|
1413
|
+
*
|
|
1414
|
+
* @example
|
|
1415
|
+
* const userProvider = tokensUserProvider({
|
|
1416
|
+
* model: () => import('#models/user'),
|
|
1417
|
+
* tokens: 'accessTokens'
|
|
1418
|
+
* })
|
|
1419
|
+
*/
|
|
666
1420
|
function tokensUserProvider(config) {
|
|
667
1421
|
return new AccessTokensLucidUserProvider(config);
|
|
668
1422
|
}
|
|
1423
|
+
//#endregion
|
|
669
1424
|
export { AccessToken, AccessTokensGuard, AccessTokensLucidUserProvider, DbAccessTokensProvider, tokensGuard, tokensUserProvider };
|