@arkstack/auth 0.5.2 → 0.5.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/README.md +2 -0
- package/dist/app.d.ts +13 -3
- package/dist/index.d.ts +480 -375
- package/dist/index.js +170 -53
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +25 -0
- package/package.json +9 -10
- package/stubs/migrations/20260505110000_create_user_two_factors_table.ts.stub +24 -0
- package/stubs/models/UserTwoFactor.ts.stub +20 -0
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Encryption, Exception, Hash, env, getModel } from "@arkstack/common";
|
|
1
|
+
import { Encryption, Exception, Hash, appKey, env, getModel } from "@arkstack/common";
|
|
2
2
|
import { SignJWT, jwtVerify } from "jose";
|
|
3
|
-
import { Request, Response } from "@arkstack/http";
|
|
3
|
+
import { Request, Response, Session } from "@arkstack/http";
|
|
4
4
|
import { UAParser } from "ua-parser-js";
|
|
5
|
+
import { Secret } from "otpauth";
|
|
5
6
|
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { Model } from "@arkstack/database";
|
|
7
8
|
//#region src/Contracts/AuthContract.ts
|
|
@@ -37,35 +38,33 @@ var AuthenticationException = class extends Exception {
|
|
|
37
38
|
}
|
|
38
39
|
};
|
|
39
40
|
//#endregion
|
|
40
|
-
//#region src/
|
|
41
|
+
//#region src/AuthSession.ts
|
|
41
42
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* handle session-specific operations.
|
|
46
|
-
*
|
|
47
|
-
* @author Legacy (3m1n3nc3)
|
|
48
|
-
* @since 1.0.0
|
|
49
|
-
* @version 1.0.0
|
|
50
|
-
* @see Auth
|
|
43
|
+
* Represents an authenticated user session.
|
|
44
|
+
*
|
|
45
|
+
* @author 3m1n3nc3
|
|
51
46
|
*/
|
|
52
|
-
var
|
|
47
|
+
var AuthSession = class extends Session {
|
|
53
48
|
auth;
|
|
54
|
-
constructor(auth) {
|
|
49
|
+
constructor(auth, current = globalThis.session?.()) {
|
|
50
|
+
super(current instanceof Session ? current : void 0);
|
|
55
51
|
this.auth = auth;
|
|
56
52
|
}
|
|
57
53
|
/**
|
|
58
|
-
* Destroy the current session
|
|
59
|
-
*
|
|
54
|
+
* Destroy the current session
|
|
55
|
+
*
|
|
56
|
+
* @returns
|
|
60
57
|
*/
|
|
61
58
|
async destroy() {
|
|
62
59
|
const pat = await this.token();
|
|
63
60
|
if (pat) await this.auth.logout(pat);
|
|
61
|
+
await super.destroy();
|
|
62
|
+
return this;
|
|
64
63
|
}
|
|
65
64
|
/**
|
|
66
|
-
* Get the current session
|
|
65
|
+
* Get the current auth session token
|
|
67
66
|
*
|
|
68
|
-
* @returns
|
|
67
|
+
* @returns
|
|
69
68
|
*/
|
|
70
69
|
async token() {
|
|
71
70
|
if (!this.auth.getRequest()) return null;
|
|
@@ -96,7 +95,7 @@ var SessionDevice = class {
|
|
|
96
95
|
static fromRequest(req) {
|
|
97
96
|
const userAgent = this.readUserAgent(req);
|
|
98
97
|
const ua = new UAParser(userAgent ?? void 0).getResult();
|
|
99
|
-
const ca = this.
|
|
98
|
+
const ca = this.readDeviceAgent(req);
|
|
100
99
|
return {
|
|
101
100
|
browser: this.readString(ua.browser.name) ?? this.detectBrowser(userAgent),
|
|
102
101
|
os: ca?.os ?? this.readString(ua.os.name) ?? this.detectOs(userAgent),
|
|
@@ -182,8 +181,8 @@ var SessionDevice = class {
|
|
|
182
181
|
* @param headerName
|
|
183
182
|
* @returns
|
|
184
183
|
*/
|
|
185
|
-
static
|
|
186
|
-
const value = req?.header("x-
|
|
184
|
+
static readDeviceAgent(req) {
|
|
185
|
+
const value = req?.header("x-device-agent");
|
|
187
186
|
if (typeof value !== "string" || value.length < 1) return null;
|
|
188
187
|
try {
|
|
189
188
|
const parsed = JSON.parse(value);
|
|
@@ -357,8 +356,7 @@ var Auth = class Auth extends AuthContract {
|
|
|
357
356
|
status: 422,
|
|
358
357
|
errors: { password: ["Invalid password"] }
|
|
359
358
|
});
|
|
360
|
-
|
|
361
|
-
this.#user = user;
|
|
359
|
+
this.setAuthenticated(user);
|
|
362
360
|
return user;
|
|
363
361
|
}
|
|
364
362
|
/**
|
|
@@ -407,8 +405,7 @@ var Auth = class Auth extends AuthContract {
|
|
|
407
405
|
req: Auth.req,
|
|
408
406
|
status: 401
|
|
409
407
|
});
|
|
410
|
-
|
|
411
|
-
this.#user = user;
|
|
408
|
+
this.setAuthenticated(user, token);
|
|
412
409
|
return user;
|
|
413
410
|
}
|
|
414
411
|
/**
|
|
@@ -423,6 +420,7 @@ var Auth = class Auth extends AuthContract {
|
|
|
423
420
|
else await token.delete();
|
|
424
421
|
else await (await getModel("PersonalAccessToken")).query().where({ userId: this.#user.id }).delete();
|
|
425
422
|
this.#user = null;
|
|
423
|
+
if (Auth.req?.auth === this) Auth.req.clearAuthentication();
|
|
426
424
|
}
|
|
427
425
|
/**
|
|
428
426
|
* Check if the user is authenticated
|
|
@@ -437,8 +435,8 @@ var Auth = class Auth extends AuthContract {
|
|
|
437
435
|
*
|
|
438
436
|
* @returns
|
|
439
437
|
*/
|
|
440
|
-
|
|
441
|
-
return new
|
|
438
|
+
session() {
|
|
439
|
+
return new AuthSession(this);
|
|
442
440
|
}
|
|
443
441
|
/**
|
|
444
442
|
* Create a personal access token for a user
|
|
@@ -451,11 +449,11 @@ var Auth = class Auth extends AuthContract {
|
|
|
451
449
|
sub: user.id.toString(),
|
|
452
450
|
email: user.email
|
|
453
451
|
};
|
|
454
|
-
Auth.req?.setUser(user);
|
|
455
452
|
const token = await this.createJWT(payload);
|
|
456
453
|
const deviceInfo = SessionDevice.fromRequest(Auth.req);
|
|
457
454
|
const pat = await this.upsertDeviceToken(user, token, deviceInfo);
|
|
458
455
|
pat.setLoadedRelation("user", user);
|
|
456
|
+
this.setAuthenticated(user, token);
|
|
459
457
|
return pat;
|
|
460
458
|
}
|
|
461
459
|
/**
|
|
@@ -483,15 +481,15 @@ var Auth = class Auth extends AuthContract {
|
|
|
483
481
|
return (right.lastUsedAt ?? right.createdAt).getTime() - leftTime;
|
|
484
482
|
});
|
|
485
483
|
if (matchingSessions.length < 1) return await TokenModel.query().create(payload);
|
|
486
|
-
const [
|
|
484
|
+
const [session, ...duplicateSessions] = matchingSessions;
|
|
487
485
|
if (duplicateSessions.length > 0) await Promise.all(duplicateSessions.map(async (session) => await session.delete()));
|
|
488
|
-
await TokenModel.query().where({ id:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return
|
|
486
|
+
await TokenModel.query().where({ id: session.id }).update(payload);
|
|
487
|
+
session.token = payload.token;
|
|
488
|
+
session.name = payload.name;
|
|
489
|
+
session.userId = payload.userId;
|
|
490
|
+
session.deviceInfo = payload.deviceInfo;
|
|
491
|
+
session.lastUsedAt = payload.lastUsedAt;
|
|
492
|
+
return session;
|
|
495
493
|
}
|
|
496
494
|
/**
|
|
497
495
|
* Authorize a token and return the associated user
|
|
@@ -515,11 +513,10 @@ var Auth = class Auth extends AuthContract {
|
|
|
515
513
|
req: Auth.req,
|
|
516
514
|
status: 401
|
|
517
515
|
});
|
|
518
|
-
Auth.req?.setUser(user);
|
|
519
516
|
this.touchSession(pat).catch((error) => {
|
|
520
517
|
if (env("NODE_ENV") === "development") console.error("Failed to update session activity", error);
|
|
521
518
|
});
|
|
522
|
-
this
|
|
519
|
+
this.setAuthenticated(user, token);
|
|
523
520
|
return user;
|
|
524
521
|
}
|
|
525
522
|
/**
|
|
@@ -546,7 +543,11 @@ var Auth = class Auth extends AuthContract {
|
|
|
546
543
|
}
|
|
547
544
|
}
|
|
548
545
|
getSecret() {
|
|
549
|
-
return this.configuredSecret ??
|
|
546
|
+
return this.configuredSecret ?? appKey("JWT_SECRET") ?? "default_secret";
|
|
547
|
+
}
|
|
548
|
+
setAuthenticated(user, token) {
|
|
549
|
+
this.#user = user;
|
|
550
|
+
Auth.req?.setAuthentication(this, user, token);
|
|
550
551
|
}
|
|
551
552
|
/**
|
|
552
553
|
* Update the last used timestamp and device information of a personal
|
|
@@ -576,11 +577,20 @@ var Auth = class Auth extends AuthContract {
|
|
|
576
577
|
}
|
|
577
578
|
};
|
|
578
579
|
//#endregion
|
|
580
|
+
//#region src/utils.ts
|
|
581
|
+
/**
|
|
582
|
+
* Create a new instance of the Auth class with an optional secret for JWT
|
|
583
|
+
* signing and verification.
|
|
584
|
+
*
|
|
585
|
+
* @param secret — The secret key used for signing and verifying JWTs.
|
|
586
|
+
*
|
|
587
|
+
* @returns — A new instance of the Auth class.
|
|
588
|
+
*/
|
|
589
|
+
const auth = (secret) => Auth.make(secret);
|
|
590
|
+
//#endregion
|
|
579
591
|
//#region src/TwoFactor.ts
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const smsCodeTtlMinutes = () => Number(env("TWO_FACTOR_SMS_TTL_MINUTES", 10)) || 10;
|
|
583
|
-
var TwoFactor = class {
|
|
592
|
+
var TwoFactor = class TwoFactor {
|
|
593
|
+
static smsCodeTtlMinutes = Number(env("TWO_FACTOR_SMS_TTL_MINUTES", 10)) || 10;
|
|
584
594
|
static async getModel() {
|
|
585
595
|
return await getModel("UserTwoFactor");
|
|
586
596
|
}
|
|
@@ -600,18 +610,40 @@ var TwoFactor = class {
|
|
|
600
610
|
if (normalized.length <= 4) return normalized;
|
|
601
611
|
return `${"*".repeat(Math.max(normalized.length - 4, 2))}${normalized.slice(-4)}`;
|
|
602
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Build the account label used inside the OTP URI.
|
|
615
|
+
*
|
|
616
|
+
* @param user
|
|
617
|
+
* @returns
|
|
618
|
+
*/
|
|
603
619
|
static getLabel(user) {
|
|
604
|
-
return user.email || `${
|
|
620
|
+
return user.email || `${env("APP_NAME", "Arkstack")}:${user.id}`;
|
|
605
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Create the per-user TOTP instance for setup and verification.
|
|
624
|
+
*
|
|
625
|
+
* @param user
|
|
626
|
+
* @param secret
|
|
627
|
+
* @returns
|
|
628
|
+
*/
|
|
606
629
|
static getTotp(user, secret) {
|
|
607
|
-
return Hash.totp(secret, this.getLabel(user),
|
|
630
|
+
return Hash.totp(secret, this.getLabel(user), env("APP_NAME", "Arkstack"));
|
|
608
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Generate a new shared secret for authenticator-based 2FA.
|
|
634
|
+
*
|
|
635
|
+
* @returns The generated secret in base32 format.
|
|
636
|
+
*/
|
|
609
637
|
static generateSecret(size = 20) {
|
|
610
|
-
|
|
611
|
-
let secret = "";
|
|
612
|
-
for (const byte of bytes) secret += secretAlphabet[byte % 32];
|
|
613
|
-
return secret;
|
|
638
|
+
return new Secret({ size }).base32;
|
|
614
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Build the setup payload returned to the client.
|
|
642
|
+
*
|
|
643
|
+
* @param user The user for whom the setup is being created.
|
|
644
|
+
* @param secret Optional existing secret to use for the setup.
|
|
645
|
+
* @returns An object containing the secret and the OTPAuth URL.
|
|
646
|
+
*/
|
|
615
647
|
static createSetup(user, secret) {
|
|
616
648
|
const resolvedSecret = secret ?? this.generateSecret();
|
|
617
649
|
return {
|
|
@@ -619,6 +651,14 @@ var TwoFactor = class {
|
|
|
619
651
|
otpauthUrl: this.getTotp(user, resolvedSecret).toString()
|
|
620
652
|
};
|
|
621
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* Verify a 6-digit authenticator code for a user.
|
|
656
|
+
*
|
|
657
|
+
* @param user The user for whom the code is being verified.
|
|
658
|
+
* @param secret The secret used to generate the code.
|
|
659
|
+
* @param code The 6-digit code to verify.
|
|
660
|
+
* @returns True if the code is valid, false otherwise.
|
|
661
|
+
*/
|
|
622
662
|
static verifyCode(user, secret, code) {
|
|
623
663
|
return this.getTotp(user, secret).validate({
|
|
624
664
|
token: code,
|
|
@@ -632,39 +672,98 @@ var TwoFactor = class {
|
|
|
632
672
|
static async setMethod(userId, method) {
|
|
633
673
|
await this.upsert(userId, { method });
|
|
634
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Read the setup secret stored for a user.
|
|
677
|
+
*
|
|
678
|
+
* @param userId The ID of the user.
|
|
679
|
+
* @returns The stored secret, or null if not found.
|
|
680
|
+
*/
|
|
635
681
|
static async getSecret(userId) {
|
|
636
682
|
const record = await this.getRecord(userId);
|
|
637
683
|
return record?.secretCiphertext ? Encryption.decrypt(record.secretCiphertext) : null;
|
|
638
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Store the setup secret for a user.
|
|
687
|
+
*
|
|
688
|
+
* @param userId The ID of the user.
|
|
689
|
+
* @param secret The secret to store.
|
|
690
|
+
*/
|
|
639
691
|
static async setSecret(userId, secret) {
|
|
640
692
|
await this.upsert(userId, { secretCiphertext: Encryption.encrypt(secret) });
|
|
641
693
|
}
|
|
642
694
|
static async clearSecret(userId) {
|
|
643
695
|
await this.upsert(userId, { secretCiphertext: null });
|
|
644
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* Read the timestamp indicating whether 2FA is enabled.
|
|
699
|
+
*
|
|
700
|
+
* @param userId The ID of the user.
|
|
701
|
+
* @returns The timestamp when 2FA was enabled, or null if not enabled.
|
|
702
|
+
*/
|
|
645
703
|
static async getEnabledAt(userId) {
|
|
646
704
|
return (await this.getRecord(userId))?.enabledAt?.toISOString() ?? null;
|
|
647
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Persist the timestamp marking 2FA as enabled.
|
|
708
|
+
*
|
|
709
|
+
* @param userId The ID of the user.
|
|
710
|
+
* @param enabledAt The timestamp to store.
|
|
711
|
+
*/
|
|
648
712
|
static async setEnabledAt(userId, enabledAt = /* @__PURE__ */ new Date()) {
|
|
649
713
|
await this.upsert(userId, { enabledAt: typeof enabledAt === "string" ? new Date(enabledAt) : enabledAt });
|
|
650
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* Remove all persisted 2FA state for a user.
|
|
717
|
+
*
|
|
718
|
+
* @param userId The ID of the user.
|
|
719
|
+
*/
|
|
651
720
|
static async clear(userId) {
|
|
652
721
|
await (await this.getModel()).query().where({ userId }).delete();
|
|
653
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* Generate one-time recovery codes shown when 2FA is enabled.
|
|
725
|
+
*
|
|
726
|
+
* @returns An array of recovery codes.
|
|
727
|
+
*/
|
|
654
728
|
static generateBackupCodes(count = 8) {
|
|
655
729
|
return Array.from({ length: count }, () => {
|
|
656
730
|
return `${randomBytes(3).toString("hex").slice(0, 4).toUpperCase()}-${randomBytes(3).toString("hex").slice(0, 4).toUpperCase()}`;
|
|
657
731
|
});
|
|
658
732
|
}
|
|
733
|
+
/**
|
|
734
|
+
* Hash recovery codes before persisting them.
|
|
735
|
+
*
|
|
736
|
+
* @param codes An array of recovery codes to hash.
|
|
737
|
+
* @returns An array of hashed recovery codes.
|
|
738
|
+
*/
|
|
659
739
|
static async hashBackupCodes(codes) {
|
|
660
740
|
return await Promise.all(codes.map(async (code) => await Hash.make(code)));
|
|
661
741
|
}
|
|
742
|
+
/**
|
|
743
|
+
* Read stored recovery-code hashes for a user.
|
|
744
|
+
*
|
|
745
|
+
* @param userId The ID of the user.
|
|
746
|
+
* @returns An array of recovery-code hashes.
|
|
747
|
+
*/
|
|
662
748
|
static async readRecoveryCodeHashes(userId) {
|
|
663
749
|
return (await this.getRecord(userId))?.recoveryCodeHashes ?? [];
|
|
664
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* Persist recovery-code hashes on the user's dedicated 2FA record.
|
|
753
|
+
*
|
|
754
|
+
* @param userId
|
|
755
|
+
* @param hashes
|
|
756
|
+
*/
|
|
665
757
|
static async writeRecoveryCodeHashes(userId, hashes) {
|
|
666
758
|
await this.upsert(userId, { recoveryCodeHashes: hashes });
|
|
667
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Consume a valid recovery code and invalidate it immediately.
|
|
762
|
+
*
|
|
763
|
+
* @param userId The ID of the user.
|
|
764
|
+
* @param recoveryCode The recovery code to consume.
|
|
765
|
+
* @returns True if the recovery code was valid and consumed, false otherwise.
|
|
766
|
+
*/
|
|
668
767
|
static async consumeRecoveryCode(userId, recoveryCode) {
|
|
669
768
|
const hashes = await this.readRecoveryCodeHashes(userId);
|
|
670
769
|
for (const [index, hash] of hashes.entries()) if (await Hash.verify(recoveryCode, hash)) {
|
|
@@ -673,6 +772,12 @@ var TwoFactor = class {
|
|
|
673
772
|
}
|
|
674
773
|
return false;
|
|
675
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Return the public 2FA status payload for a user.
|
|
777
|
+
*
|
|
778
|
+
* @param userId The ID of the user.
|
|
779
|
+
* @returns An object containing the 2FA status and recovery codes remaining.
|
|
780
|
+
*/
|
|
676
781
|
static async readStatus(userId) {
|
|
677
782
|
const record = await this.getRecord(userId);
|
|
678
783
|
const enabledAt = record?.enabledAt?.toISOString() ?? null;
|
|
@@ -687,11 +792,17 @@ var TwoFactor = class {
|
|
|
687
792
|
static createSmsCode() {
|
|
688
793
|
return Math.floor(1e5 + Math.random() * 9e5).toString();
|
|
689
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* Issue a new SMS code for the given user and send it via SMS for the specified purpose.
|
|
797
|
+
*
|
|
798
|
+
* @param user
|
|
799
|
+
* @param purpose
|
|
800
|
+
*/
|
|
690
801
|
static async issueSmsCode(user, purpose) {
|
|
691
802
|
if (!user.phone) throw new Error("A phone number is required to issue a two-factor SMS code.");
|
|
692
803
|
const code = this.createSmsCode();
|
|
693
804
|
const smsCodeHash = await Hash.make(code);
|
|
694
|
-
const expiresAt = new Date(Date.now() + smsCodeTtlMinutes
|
|
805
|
+
const expiresAt = new Date(Date.now() + TwoFactor.smsCodeTtlMinutes * 60 * 1e3);
|
|
695
806
|
await this.upsert(user.id, {
|
|
696
807
|
smsCodeHash,
|
|
697
808
|
smsCodeExpiresAt: expiresAt,
|
|
@@ -710,6 +821,14 @@ var TwoFactor = class {
|
|
|
710
821
|
smsCodePurpose: null
|
|
711
822
|
});
|
|
712
823
|
}
|
|
824
|
+
/**
|
|
825
|
+
* Verify a submitted SMS code for a user and purpose, consuming the code if valid.
|
|
826
|
+
*
|
|
827
|
+
* @param userId
|
|
828
|
+
* @param code
|
|
829
|
+
* @param purpose
|
|
830
|
+
* @returns
|
|
831
|
+
*/
|
|
713
832
|
static async verifySmsCode(userId, code, purpose) {
|
|
714
833
|
const record = await this.getRecord(userId);
|
|
715
834
|
if (!record?.smsCodeHash || !record.smsCodeExpiresAt || record.smsCodePurpose !== purpose) return false;
|
|
@@ -737,6 +856,4 @@ var UserTwoFactor = class extends Model {
|
|
|
737
856
|
casts = { recoveryCodeHashes: "json" };
|
|
738
857
|
};
|
|
739
858
|
//#endregion
|
|
740
|
-
export { Auth, AuthContract,
|
|
741
|
-
|
|
742
|
-
//# sourceMappingURL=index.js.map
|
|
859
|
+
export { Auth, AuthContract, AuthSession, AuthenticationException, PersonalAccessToken, SessionDevice, TwoFactor, User, UserTwoFactor, auth };
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Publisher } from "@arkstack/common";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
//#region src/setup.ts
|
|
5
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
/**
|
|
7
|
+
* Register the artifacts `@arkstack/auth` publishes into the application.
|
|
8
|
+
*
|
|
9
|
+
* Run `ark publish --tag two-factor` (or `--package @arkstack/auth`) to copy the
|
|
10
|
+
* `UserTwoFactor` model and the migration that creates the `user_two_factors`
|
|
11
|
+
* table into the app.
|
|
12
|
+
*/
|
|
13
|
+
Publisher.publishes({
|
|
14
|
+
package: "@arkstack/auth",
|
|
15
|
+
tag: "two-factor",
|
|
16
|
+
entries: [{
|
|
17
|
+
from: join(root, "stubs/models/UserTwoFactor.ts.stub"),
|
|
18
|
+
to: "src/app/models/UserTwoFactor.ts"
|
|
19
|
+
}, {
|
|
20
|
+
from: join(root, "stubs/migrations/20260505110000_create_user_two_factors_table.ts.stub"),
|
|
21
|
+
to: "src/database/migrations/20260505110000_create_user_two_factors_table.ts"
|
|
22
|
+
}]
|
|
23
|
+
});
|
|
24
|
+
//#endregion
|
|
25
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkstack/auth",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Authentication module for Arkstack, providing core authentication and identity features.",
|
|
6
6
|
"homepage": "https://arkstack.toneflix.net/guide/auth",
|
|
@@ -25,28 +25,27 @@
|
|
|
25
25
|
"arkstack"
|
|
26
26
|
],
|
|
27
27
|
"files": [
|
|
28
|
-
"dist"
|
|
28
|
+
"dist",
|
|
29
|
+
"stubs"
|
|
29
30
|
],
|
|
30
31
|
"publishConfig": {
|
|
31
32
|
"access": "public"
|
|
32
33
|
},
|
|
33
34
|
"exports": {
|
|
34
35
|
".": "./dist/index.js",
|
|
36
|
+
"./setup": "./dist/setup.js",
|
|
35
37
|
"./package.json": "./package.json"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"jose": "^6.2.3",
|
|
41
|
+
"otpauth": "^9.5.1",
|
|
39
42
|
"ua-parser-js": "^2.0.9",
|
|
40
|
-
"@arkstack/
|
|
41
|
-
"@arkstack/
|
|
43
|
+
"@arkstack/http": "^0.5.3",
|
|
44
|
+
"@arkstack/common": "^0.5.3"
|
|
42
45
|
},
|
|
43
46
|
"peerDependencies": {
|
|
44
|
-
"@h3ravel/support": "^
|
|
45
|
-
"@arkstack/database": "^0.5.
|
|
46
|
-
"@arkstack/notifications": "^0.5.2"
|
|
47
|
-
},
|
|
48
|
-
"inlinedDependencies": {
|
|
49
|
-
"otpauth": "9.5.1"
|
|
47
|
+
"@h3ravel/support": "^2.2.0",
|
|
48
|
+
"@arkstack/database": "^0.5.3"
|
|
50
49
|
},
|
|
51
50
|
"scripts": {
|
|
52
51
|
"build": "tsdown --config-loader unrun",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Migration, SchemaBuilder } from 'arkormx'
|
|
2
|
+
|
|
3
|
+
export default class CreateUserTwoFactorsTableMigration extends Migration {
|
|
4
|
+
public async up (schema: SchemaBuilder): Promise<void> {
|
|
5
|
+
schema.createTable('user_two_factors', (table) => {
|
|
6
|
+
table.id('id', 'uuid').primary()
|
|
7
|
+
table.uuid('userId').map('user_id')
|
|
8
|
+
.foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('twoFactor')
|
|
9
|
+
table.enum('method', ['authenticator', 'sms']).nullable().enumName('TwoFactorMethod')
|
|
10
|
+
table.string('secretCiphertext').nullable().map('secret_ciphertext')
|
|
11
|
+
table.string('smsCodeHash').nullable().map('sms_code_hash')
|
|
12
|
+
table.date('smsCodeExpiresAt').nullable().map('sms_code_expires_at')
|
|
13
|
+
table.string('smsCodePurpose').nullable().map('sms_code_purpose')
|
|
14
|
+
table.date('enabledAt').nullable().map('enabled_at')
|
|
15
|
+
table.json('recoveryCodeHashes').nullable().map('recovery_code_hashes')
|
|
16
|
+
table.timestamps()
|
|
17
|
+
table.index(['userId'])
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async down (schema: SchemaBuilder): Promise<void> {
|
|
22
|
+
schema.dropTable('user_two_factors')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { UserTwoFactor as BaseUserTwoFactor } from '@arkstack/auth'
|
|
2
|
+
import { User } from '@app/models/User'
|
|
3
|
+
|
|
4
|
+
export class UserTwoFactor extends BaseUserTwoFactor {
|
|
5
|
+
protected static columns = {
|
|
6
|
+
userId: 'user_id',
|
|
7
|
+
secretCiphertext: 'secret_ciphertext',
|
|
8
|
+
smsCodeHash: 'sms_code_hash',
|
|
9
|
+
smsCodeExpiresAt: 'sms_code_expires_at',
|
|
10
|
+
smsCodePurpose: 'sms_code_purpose',
|
|
11
|
+
enabledAt: 'enabled_at',
|
|
12
|
+
recoveryCodeHashes: 'recovery_code_hashes',
|
|
13
|
+
createdAt: 'created_at',
|
|
14
|
+
updatedAt: 'updated_at',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
user() {
|
|
18
|
+
return this.belongsTo(User, 'userId', 'id')
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#request","#response","#errors","#user"],"sources":["../src/Contracts/AuthContract.ts","../src/Exceptions/AuthenticationException.ts","../src/CurrentSession.ts","../src/SessionDevice.ts","../src/Auth.ts","../src/TwoFactor.ts","../src/Contracts/PersonalAccessToken.ts","../src/Contracts/User.ts","../src/Contracts/UserTwoFactor.ts"],"sourcesContent":["import { CurrentSession } from '../CurrentSession'\nimport { PersonalAccessToken } from './PersonalAccessToken'\nimport { Request, type RequestSource } from '@arkstack/http'\nimport { User } from './User'\n\n/**\n * The Auth class provides methods for user authentication, including verifying \n * credentials, logging in, logging out, and managing personal access tokens. \n * \n * @author Legacy (3m1n3nc3)\n */\nexport abstract class AuthContract {\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth instance itself for method chaining.\n */\n abstract setRequest (req: Request<User> | RequestSource<User>): this\n\n /**\n * Get the current HTTP request instance being processed, which may contain\n * user information and other request-specific data relevant to authentication operations.\n * \n * @returns The current HTTP request instance or undefined if not set.\n */\n abstract getRequest (): Request<User> | undefined\n\n /**\n * Get the currently authenticated user\n * \n * @returns The currently authenticated user or null if not authenticated.\n */\n abstract user (): User | null\n\n /**\n * Verify user credentials\n * \n * @param email The email address of the user.\n * @param password The password of the user.\n * @returns A boolean indicating whether the credentials are valid.\n */\n abstract verify (email: string, password: string): Promise<boolean>\n\n /**\n * Attempt to authenticate a user with the given email and password.\n * \n * @param email \n * @param password \n * @returns \n */\n abstract attempt (email: string, password: string): Promise<User>\n\n /**\n * Login a user and create a personal access token\n * \n * @param email \n * @param password \n * @returns \n */\n abstract login (email: string, password: string): Promise<PersonalAccessToken>\n\n /**\n * Create a temporary token for a user with a specific purpose, such as\n * two-factor authentication.\n * \n * @param user \n * @param purpose \n * @param expiresIn \n * @returns \n */\n abstract createTemporaryToken (user: User, purpose: string, expiresIn?: string): Promise<string>\n\n /**\n * Authorize a temporary token and return the associated user if the token is \n * valid and matches the expected purpose.\n * \n * @param token \n * @param purpose \n * @returns \n */\n abstract authorizeTemporaryToken (token: string, purpose: string): Promise<User>\n\n /**\n * Logout the currently authenticated user and delete all their personal access tokens\n * \n * @param token \n * @returns \n */\n abstract logout (token?: string | PersonalAccessToken): Promise<void>\n\n /**\n * Check if the user is authenticated\n * \n * @returns \n */\n abstract check (): Promise<boolean>\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n abstract currentSession (): CurrentSession\n\n /**\n * Create a personal access token for a user\n * \n * @param user \n * @returns \n */\n abstract create (user: User): Promise<PersonalAccessToken>\n\n /**\n * Authorize a token and return the associated user\n * \n * @param token \n * @returns \n */\n abstract authorizeToken (token: string): Promise<User>\n}\n","import { Request, Response, type RequestSource, type ResponseSource } from '@arkstack/http'\n\nimport { Exception } from '@arkstack/common'\n\nexport class AuthenticationException extends Exception {\n #errors?: Record<string, any>\n #request?: Request\n #response?: Response\n statusCode: number = 401\n name: string\n\n constructor(\n message: string = 'Authentication failed',\n ctx?: {\n req?: Request | RequestSource,\n res?: Response | ResponseSource,\n status?: number,\n errors?: Record<string, any>\n }\n ) {\n super(message)\n this.name = 'AuthenticationException'\n this.statusCode = ctx?.status ?? 401\n if (ctx) {\n this.#request = Request.from(ctx.req)\n this.#response = Response.from(ctx.res)\n this.#errors = ctx.errors\n }\n\n void this.#response\n void this.#request\n }\n\n errors (): Record<string, any> | undefined {\n return this.#errors\n }\n}\n","import { AuthContract } from './Contracts/AuthContract'\nimport { PersonalAccessToken } from './Contracts/PersonalAccessToken'\nimport { getModel } from '@arkstack/common'\n\n/**\n * The CurrentSession class represents the current authentication session and provides \n * methods to manage it, such as destroying the session (logging out) and retrieving \n * the current personal access token. It is used internally by the Auth class to \n * handle session-specific operations.\n * \n * @author Legacy (3m1n3nc3)\n * @since 1.0.0\n * @version 1.0.0\n * @see Auth\n */\nexport class CurrentSession {\n constructor(private auth: AuthContract) { }\n\n /**\n * Destroy the current session's personal access token, effectively \n * logging out the user from this session.\n */\n async destroy () {\n const pat = await this.token()\n\n if (pat) {\n await this.auth.logout(pat)\n }\n }\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n async token (): Promise<PersonalAccessToken | null> {\n if (!this.auth.getRequest()) {\n return null\n }\n\n const token = this.auth.getRequest()!.bearerToken()\n\n if (!token) {\n return null\n }\n\n const Model = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const pat = await Model.query().where({ token }).first()\n\n return pat\n }\n}\n","import { type Request } from '@arkstack/http'\nimport { AuthAgentPayload, SessionDeviceInfo } from './types/Session'\nimport { UAParser } from 'ua-parser-js'\n\nexport class SessionDevice {\n private static readonly uniqueIdentityFields = [\n 'deviceName',\n 'manufacturer',\n 'model',\n 'platform',\n 'os',\n 'osVersion',\n 'browser',\n 'deviceType',\n ] as const\n\n /**\n * Extracts device information from the incoming request to build a SessionDeviceInfo object.\n * \n * @param req The incoming HTTP request object.\n * @returns A SessionDeviceInfo object containing information about the client's device.\n */\n static fromRequest (req?: Request): SessionDeviceInfo {\n const userAgent = this.readUserAgent(req)\n const ua = new UAParser(userAgent ?? undefined).getResult()\n const ca = this.readAuthAgent(req)\n\n return {\n browser: this.readString(ua.browser.name) ?? this.detectBrowser(userAgent),\n os: ca?.os ?? this.readString(ua.os.name) ?? this.detectOs(userAgent),\n osVersion: ca?.osVersion ?? this.readString(ua.os.version),\n deviceType: ca?.deviceType ?? this.normalizeDeviceType(ua.device.type) ?? this.detectDeviceType(userAgent),\n deviceName: ca?.deviceName ?? null,\n manufacturer: ca?.manufacturer ?? this.readString(ua.device.vendor),\n model: ca?.model ?? this.readString(ua.device.model),\n platform: ca?.platform ?? null,\n ipAddress: this.detectIpAddress(req),\n userAgent,\n }\n }\n\n /**\n * Generates a human-readable display name for the device based on available information.\n * \n * @param deviceInfo A record containing device information.\n * @returns A string representing the display name of the device.\n */\n static getDisplayName (deviceInfo?: Record<string, unknown> | null) {\n const deviceName = this.readString(deviceInfo?.deviceName)\n const manufacturer = this.readString(deviceInfo?.manufacturer)\n const model = this.readString(deviceInfo?.model)\n const browser = this.readString(deviceInfo?.browser)\n const os = this.readString(deviceInfo?.os)\n const deviceType = this.readString(deviceInfo?.deviceType)\n\n if (manufacturer && model) {\n return model.startsWith(manufacturer) ? model : `${manufacturer} ${model}`\n }\n\n if (model) {\n return model\n }\n\n if (deviceName) {\n return deviceName\n }\n\n if (browser && os) {\n return `${browser} on ${os}`\n }\n\n if (os && deviceType && deviceType !== 'unknown') {\n return `${os} ${deviceType}`\n }\n\n if (browser) {\n return browser\n }\n\n return 'Unknown device'\n }\n\n /**\n * Builds a stable device key for matching previously issued sessions to the\n * current request device.\n *\n * @param deviceInfo A record containing device information.\n * @returns A normalized device key or null when there is not enough signal.\n */\n static getUniqueKey (deviceInfo?: Record<string, unknown> | null) {\n const parts = this.uniqueIdentityFields\n .map((field) => [field, this.readString(deviceInfo?.[field])?.toLowerCase()] as const)\n .filter(([, value]) => !!value)\n\n if (parts.length < 2) {\n const fallbackUserAgent = this.readString(deviceInfo?.userAgent)?.toLowerCase()\n\n return fallbackUserAgent ?? null\n }\n\n return parts.map(([field, value]) => `${field}:${value}`).join('|')\n }\n\n /**\n * Determines whether two device payloads represent the same device.\n *\n * @param left The first device payload.\n * @param right The second device payload.\n * @returns True when both payloads resolve to the same device key.\n */\n static matches (left?: Record<string, unknown> | null, right?: Record<string, unknown> | null) {\n const leftKey = this.getUniqueKey(left)\n const rightKey = this.getUniqueKey(right)\n\n if (!leftKey || !rightKey) {\n return false\n }\n\n return leftKey === rightKey\n }\n\n /**\n * Safely reads the user agent string from the request headers.\n * \n * @param req \n * @returns \n */\n private static readUserAgent (req?: Request) {\n const userAgent = req?.header('user-agent')\n\n return typeof userAgent === 'string' ? userAgent : null\n }\n\n /**\n * Safely reads a string value, ensuring it's a non-empty string or returns null.\n * \n * @param value \n * @returns \n */\n private static readString (value: unknown) {\n return typeof value === 'string' && value.length > 0 ? value : null\n }\n\n /**\n * Reads a specific device-related header from the request\n * \n * @param req \n * @param headerName \n * @returns \n */\n private static readAuthAgent (req?: Request): AuthAgentPayload | null {\n const value = req?.header('x-auth-agent')\n\n if (typeof value !== 'string' || value.length < 1) {\n return null\n }\n\n try {\n const parsed: AuthAgentPayload = JSON.parse(value)\n\n return {\n deviceName: this.readString(parsed.deviceName) ?? undefined,\n manufacturer: this.readString(parsed.manufacturer) ?? undefined,\n model: this.readString(parsed.model) ?? undefined,\n platform: this.readString(parsed.platform) ?? undefined,\n os: this.readString(parsed.os) ?? undefined,\n osVersion: this.readString(parsed.osVersion) ?? undefined,\n deviceType: this.normalizeDeviceType(parsed.deviceType) ?? undefined,\n }\n } catch {\n return null\n }\n }\n\n private static normalizeDeviceType (value: unknown): SessionDeviceInfo['deviceType'] | null {\n if (value === 'mobile' || value === 'tablet' || value === 'desktop' || value === 'bot' || value === 'unknown') {\n return value\n }\n\n return null\n }\n\n /**\n * Detects the client's IP address from the request, considering common headers set by proxies.\n * \n * @param req \n * @returns \n */\n private static detectIpAddress (req?: Request) {\n const forwarded = req?.header('x-forwarded-for')\n\n if (typeof forwarded === 'string' && forwarded.length > 0) {\n return forwarded.split(',')[0].trim()\n }\n\n return req?.ip ?? null\n }\n\n /**\n * Detects the browser from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectBrowser (userAgent: string | null) {\n if (!userAgent) return null\n if (/Edg\\//i.test(userAgent)) return 'Edge'\n if (/OPR\\//i.test(userAgent)) return 'Opera'\n if (/SamsungBrowser\\//i.test(userAgent)) return 'Samsung Internet'\n if (/Chrome\\//i.test(userAgent) && !/Edg\\//i.test(userAgent)) return 'Chrome'\n if (/Firefox\\//i.test(userAgent)) return 'Firefox'\n if (/Safari\\//i.test(userAgent) && !/Chrome\\//i.test(userAgent)) return 'Safari'\n if (/PostmanRuntime\\//i.test(userAgent)) return 'Postman'\n if (/okhttp\\//i.test(userAgent)) return 'OkHttp'\n if (/curl\\//i.test(userAgent)) return 'cURL'\n\n return null\n }\n\n /**\n * Detects the operating system from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectOs (userAgent: string | null) {\n if (!userAgent) return null\n if (/iPhone|iPad|iPod/i.test(userAgent)) return 'iOS'\n if (/Android/i.test(userAgent)) return 'Android'\n if (/Mac OS X|Macintosh/i.test(userAgent)) return 'macOS'\n if (/Windows NT/i.test(userAgent)) return 'Windows'\n if (/Linux/i.test(userAgent)) return 'Linux'\n\n return null\n }\n\n /**\n * Detects the device type from the user agent string.\n * \n * @param userAgent \n * @returns \n */\n private static detectDeviceType (userAgent: string | null): SessionDeviceInfo['deviceType'] {\n if (!userAgent) return 'unknown'\n if (/bot|spider|crawl/i.test(userAgent)) return 'bot'\n if (/iPad|Tablet/i.test(userAgent)) return 'tablet'\n if (/Mobile|iPhone|Android/i.test(userAgent)) return 'mobile'\n\n return 'desktop'\n }\n}\n","import { Hash, env, getModel } from '@arkstack/common'\nimport { JWTPayload, SignJWT, jwtVerify } from 'jose'\n\nimport { AuthContract } from './Contracts/AuthContract'\nimport { AuthenticationException } from './Exceptions/AuthenticationException'\nimport { CurrentSession } from './CurrentSession'\nimport { PersonalAccessToken } from './Contracts/PersonalAccessToken'\nimport { Request, type RequestSource } from '@arkstack/http'\nimport { SessionDevice } from './SessionDevice'\nimport { User } from './Contracts/User'\n\n/**\n * The Auth class provides methods for user authentication, including verifying \n * credentials, logging in, logging out, and managing personal access tokens. \n * \n * @author Legacy (3m1n3nc3)\n */\nexport class Auth extends AuthContract {\n protected static req?: Request<User>\n private configuredSecret?: string\n #user: User | null = null\n\n constructor(secret?: string, req?: Request<User> | RequestSource<User>) {\n super()\n Auth.req = Request.from<User>(req)\n this.configuredSecret = secret\n }\n\n /**\n * Create a new instance of the Auth class with an optional secret for JWT \n * signing and verification.\n * \n * @param secret The secret key used for signing and verifying JWTs.\n * @returns A new instance of the Auth class.\n */\n static make (secret?: string) {\n return new Auth(secret)\n }\n\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth class itself for method chaining.\n */\n static setRequest (req: Request<User> | RequestSource<User>) {\n this.req = Request.from<User>(req)\n\n return this\n }\n\n /**\n * Set the current HTTP request instance being processed.\n * \n * @param req The HTTP request instance to be set.\n * @returns The Auth instance itself for method chaining.\n */\n setRequest (req: Request<User> | RequestSource<User>) {\n Auth.req ??= Request.from<User>(req)\n\n return this\n }\n\n /**\n * Get the current HTTP request instance being processed, which may contain\n * user information and other request-specific data relevant to authentication operations.\n * \n * @returns The current HTTP request instance or undefined if not set.\n */\n getRequest (): Request<User> | undefined {\n return Auth.req\n }\n\n /**\n * Get the currently authenticated user\n * \n * @returns The currently authenticated user or null if not authenticated.\n */\n user (): User | null {\n return this.#user\n }\n\n /**\n * Verify user credentials\n * \n * @param email The email address of the user.\n * @param password The password of the user.\n * @returns A boolean indicating whether the credentials are valid.\n */\n async verify (email: string, password: string): Promise<boolean> {\n const user = await (await getModel<typeof User>('User')).query().where({ email }).first()\n\n return !!user && await Hash.verify(password, user.password)\n }\n\n /**\n * Attempt to authenticate a user with the given email and password.\n * \n * @param email \n * @param password \n * @returns \n */\n async attempt (email: string, password: string): Promise<User> {\n const user = await (await getModel<typeof User>('User')).query().where({ email }).first()\n\n if (!user) {\n throw new AuthenticationException('User account not found', { req: Auth.req, status: 422, errors: { email: ['No account found for this email address'] } })\n }\n\n const isValid = await Hash.verify(password, user.password)\n\n if (!isValid) {\n throw new AuthenticationException('Invalid credentials', { req: Auth.req, status: 422, errors: { password: ['Invalid password'] } })\n }\n\n Auth.req?.setUser(user)\n\n this.#user = user\n\n return user\n }\n\n /**\n * Login a user and create a personal access token\n * \n * @param email \n * @param password \n * @returns \n */\n async login (email: string, password: string): Promise<PersonalAccessToken> {\n const user = await this.attempt(email, password)\n\n return await this.create(user)\n }\n\n /**\n * Create a temporary token for a user with a specific purpose, such as\n * two-factor authentication.\n * \n * @param user \n * @param purpose \n * @param expiresIn \n * @returns \n */\n async createTemporaryToken (user: User, purpose: string, expiresIn: string = '10m'): Promise<string> {\n return await this.createJWT({\n sub: user.id.toString(),\n email: user.email,\n purpose,\n }, expiresIn)\n }\n\n /**\n * Authorize a temporary token and return the associated user if the token is \n * valid and matches the expected purpose.\n * \n * @param token \n * @param purpose \n * @returns \n */\n async authorizeTemporaryToken (token: string, purpose: string): Promise<User> {\n const payload = await this.verifyJWT(token)\n\n if (!payload || payload.purpose !== purpose || !payload.sub) {\n throw new AuthenticationException(\n 'Invalid or expired two-factor session',\n { req: Auth.req, status: 401 }\n )\n }\n\n const user = await (await getModel<typeof User>('User')).query().find(payload.sub)\n\n if (!user) {\n throw new AuthenticationException(\n 'User account not found',\n { req: Auth.req, status: 401 }\n )\n }\n\n Auth.req?.setUser(user)\n\n this.#user = user\n\n return user\n }\n\n /**\n * Logout the currently authenticated user and delete all their personal access tokens\n * \n * @param token \n * @returns \n */\n async logout (token?: string | PersonalAccessToken): Promise<void> {\n if (!this.#user && !token) {\n return\n }\n\n if (token) {\n if (typeof token === 'string') {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ token }).delete()\n } else {\n await token.delete()\n }\n } else {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ userId: this.#user!.id }).delete()\n }\n\n this.#user = null\n }\n\n /**\n * Check if the user is authenticated\n * \n * @returns \n */\n async check (): Promise<boolean> {\n return !!this.#user\n }\n\n /**\n * Get the current session's personal access token\n * \n * @returns \n */\n currentSession () {\n return new CurrentSession(this)\n }\n\n /**\n * Create a personal access token for a user\n * \n * @param user \n * @returns \n */\n async create (user: User): Promise<PersonalAccessToken> {\n const payload: JWTPayload = {\n sub: user.id.toString(),\n email: user.email,\n }\n\n Auth.req?.setUser(user)\n\n const token = await this.createJWT(payload)\n const deviceInfo = SessionDevice.fromRequest(Auth.req)\n\n const pat = await this.upsertDeviceToken(user, token, deviceInfo)\n\n pat.setLoadedRelation('user', user)\n\n return pat\n }\n\n /**\n * Create or replace the personal access token for the same user and device\n * while keeping a single active session record for that device.\n *\n * @param user The authenticated user.\n * @param token The new bearer token to persist.\n * @param deviceInfo The current request's device information.\n */\n private async upsertDeviceToken (user: User, token: string, deviceInfo: Record<string, unknown> | null) {\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const deviceKey = SessionDevice.getUniqueKey(deviceInfo)\n const payload = {\n abilities: [],\n token,\n name: SessionDevice.getDisplayName(deviceInfo),\n userId: user.id,\n lastUsedAt: new Date(),\n } as {\n abilities: string[]\n token: string\n name: string\n userId: User['id']\n deviceInfo?: Record<string, unknown> | null\n lastUsedAt: Date\n }\n\n if (!deviceKey) {\n return await TokenModel.query().create(payload)\n }\n\n payload.deviceInfo = deviceInfo\n\n const existingSessions = (await TokenModel.query().where({ userId: user.id }).get()).all()\n const matchingSessions = existingSessions\n .filter((session) => SessionDevice.matches(session.deviceInfo, deviceInfo))\n .sort((left, right) => {\n const leftTime = (left.lastUsedAt ?? left.createdAt).getTime()\n const rightTime = (right.lastUsedAt ?? right.createdAt).getTime()\n\n return rightTime - leftTime\n })\n\n if (matchingSessions.length < 1) {\n return await TokenModel.query().create(payload)\n }\n\n const [currentSession, ...duplicateSessions] = matchingSessions\n\n if (duplicateSessions.length > 0) {\n await Promise.all(duplicateSessions.map(async (session) => await session.delete()))\n }\n\n await TokenModel.query().where({ id: currentSession.id }).update(payload)\n\n currentSession.token = payload.token\n currentSession.name = payload.name\n currentSession.userId = payload.userId\n currentSession.deviceInfo = payload.deviceInfo\n currentSession.lastUsedAt = payload.lastUsedAt\n\n return currentSession\n }\n\n /**\n * Authorize a token and return the associated user\n * \n * @param token \n * @returns \n */\n async authorizeToken (token: string): Promise<User> {\n const payload = await this.verifyJWT(token)\n\n if (!payload) {\n throw new AuthenticationException(\n 'Invalid or expired session',\n { req: Auth.req, status: 401 }\n )\n }\n\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n const pat = await TokenModel.query().where({ token }).first()\n\n if (!pat) {\n throw new AuthenticationException(\n 'Invalid or expired access token',\n { req: Auth.req, status: 401 }\n )\n }\n\n const user = await (await getModel<typeof User>('User')).query().find(payload.sub!)\n\n if (!user) {\n throw new AuthenticationException(\n 'User account not found',\n { req: Auth.req, status: 401 }\n )\n }\n\n Auth.req?.setUser(user)\n\n void this.touchSession(pat).catch((error) => {\n if (env('NODE_ENV') === 'development') {\n console.error('Failed to update session activity', error)\n }\n })\n\n this.#user = user\n\n return user\n }\n\n /**\n * Create a JWT token\n * \n * @param payload \n * @returns \n */\n private async createJWT (payload: JWTPayload, expiresIn: string = env('JWT_EXPIRES_IN', '1h')): Promise<string> {\n const jwt = await new SignJWT(payload)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(expiresIn)\n .sign(new TextEncoder().encode(this.getSecret()))\n\n return jwt\n }\n\n /**\n * Verify a JWT token\n * \n * @param token \n * @returns \n */\n private async verifyJWT (token: string): Promise<JWTPayload | null> {\n try {\n const { payload } = await jwtVerify(token, new TextEncoder().encode(this.getSecret()))\n\n return payload\n } catch {\n return null\n }\n }\n\n private getSecret (): string {\n return this.configuredSecret ?? env('JWT_SECRET', 'default_secret')\n }\n\n /**\n * Update the last used timestamp and device information of a personal \n * access token to keep the session active and reflect the latest device details.\n * \n * @param pat The personal access token to update.\n * @returns A promise that resolves when the update is complete.\n */\n private async touchSession (pat: PersonalAccessToken) {\n const now = new Date()\n const currentDeviceInfo = SessionDevice.fromRequest(Auth.req)\n const shouldUpdateLastUsedAt = !pat.lastUsedAt || (now.getTime() - pat.lastUsedAt.getTime()) > 5 * 60 * 1000\n const hasDeviceInfo = !!pat.deviceInfo\n const currentDisplayName = SessionDevice.getDisplayName(currentDeviceInfo)\n const storedDisplayName = SessionDevice.getDisplayName(pat.deviceInfo)\n const shouldRefreshDeviceInfo = !hasDeviceInfo || storedDisplayName !== currentDisplayName\n\n if (!shouldUpdateLastUsedAt && !shouldRefreshDeviceInfo) {\n return\n }\n\n const payload: {\n lastUsedAt: Date\n deviceInfo?: Record<string, unknown> | null\n name?: string\n } = {\n lastUsedAt: now,\n }\n\n if (shouldRefreshDeviceInfo) {\n payload.deviceInfo = currentDeviceInfo\n payload.name = currentDisplayName\n }\n\n const TokenModel = await getModel<typeof PersonalAccessToken>('PersonalAccessToken')\n\n await TokenModel.query().where({ id: pat.id }).update(payload)\n\n pat.lastUsedAt = now\n\n if (payload.deviceInfo !== undefined) {\n pat.deviceInfo = payload.deviceInfo\n }\n\n if (payload.name !== undefined) {\n pat.name = payload.name\n }\n }\n}\n","import { Encryption, Hash, env, getModel } from '@arkstack/common'\nimport { randomBytes } from 'node:crypto'\n\nimport { User } from './Contracts/User'\nimport { UserTwoFactor } from './Contracts/UserTwoFactor'\nimport type { IssuedSmsCode, SmsCodePurpose, TwoFactorMethod, TwoFactorSetup, TwoFactorStatus } from './types/TwoFactor'\n\ntype TwoFactorUser = User & {\n phone?: string | null\n}\n\nconst secretAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'\nconst appName = () => env('APP_NAME', 'Arkstack')\nconst smsCodeTtlMinutes = () => Number(env('TWO_FACTOR_SMS_TTL_MINUTES', 10)) || 10\n\nexport class TwoFactor {\n private static async getModel () {\n return await getModel<typeof UserTwoFactor>('UserTwoFactor')\n }\n\n private static async getRecord (userId: User['id']) {\n const Model = await this.getModel()\n\n return await Model.query().where({ userId }).first()\n }\n\n private static async upsert (\n userId: User['id'],\n attributes: Partial<Pick<UserTwoFactor,\n | 'method' | 'secretCiphertext' | 'smsCodeHash' | 'smsCodeExpiresAt'\n | 'smsCodePurpose' | 'enabledAt' | 'recoveryCodeHashes'\n >>\n ) {\n const Model = await this.getModel()\n\n await Model.query().updateOrInsert({ userId }, attributes)\n }\n\n static normalizeMethod (method?: string | null): TwoFactorMethod | null {\n if (method === 'authenticator' || method === 'sms') {\n return method\n }\n\n return null\n }\n\n static maskPhone (phone?: string | null) {\n if (!phone) {\n return null\n }\n\n const normalized = phone.replace(/\\s+/g, '')\n\n if (normalized.length <= 4) {\n return normalized\n }\n\n return `${'*'.repeat(Math.max(normalized.length - 4, 2))}${normalized.slice(-4)}`\n }\n\n static getLabel (user: User) {\n return user.email || `${appName()}:${user.id}`\n }\n\n static getTotp (user: User, secret: string) {\n return Hash.totp(secret, this.getLabel(user), appName())\n }\n\n static generateSecret (size = 20) {\n const bytes = randomBytes(size)\n let secret = ''\n\n for (const byte of bytes) {\n secret += secretAlphabet[byte % secretAlphabet.length]\n }\n\n return secret\n }\n\n static createSetup (user: User, secret?: string): TwoFactorSetup {\n const resolvedSecret = secret ?? this.generateSecret()\n const totp = this.getTotp(user, resolvedSecret)\n\n return {\n secret: resolvedSecret,\n otpauthUrl: totp.toString(),\n }\n }\n\n static verifyCode (user: User, secret: string, code: string) {\n return this.getTotp(user, secret).validate({ token: code, window: 1 }) !== null\n }\n\n static async getMethod (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return this.normalizeMethod(record?.method)\n }\n\n static async setMethod (userId: User['id'], method: TwoFactorMethod) {\n await this.upsert(userId, { method })\n }\n\n static async getSecret (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.secretCiphertext ? Encryption.decrypt(record.secretCiphertext) : null\n }\n\n static async setSecret (userId: User['id'], secret: string) {\n await this.upsert(userId, { secretCiphertext: Encryption.encrypt(secret) })\n }\n\n static async clearSecret (userId: User['id']) {\n await this.upsert(userId, { secretCiphertext: null })\n }\n\n static async getEnabledAt (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.enabledAt?.toISOString() ?? null\n }\n\n static async setEnabledAt (userId: User['id'], enabledAt: string | Date = new Date()) {\n await this.upsert(userId, {\n enabledAt: typeof enabledAt === 'string' ? new Date(enabledAt) : enabledAt,\n })\n }\n\n static async clear (userId: User['id']) {\n const Model = await this.getModel()\n\n await Model.query().where({ userId }).delete()\n }\n\n static generateBackupCodes (count = 8) {\n return Array.from({ length: count }, () => {\n const left = randomBytes(3).toString('hex').slice(0, 4).toUpperCase()\n const right = randomBytes(3).toString('hex').slice(0, 4).toUpperCase()\n\n return `${left}-${right}`\n })\n }\n\n static async hashBackupCodes (codes: string[]) {\n return await Promise.all(codes.map(async code => await Hash.make(code)))\n }\n\n static async readRecoveryCodeHashes (userId: User['id']) {\n const record = await this.getRecord(userId)\n\n return record?.recoveryCodeHashes ?? []\n }\n\n static async writeRecoveryCodeHashes (userId: User['id'], hashes: string[]) {\n await this.upsert(userId, { recoveryCodeHashes: hashes })\n }\n\n static async consumeRecoveryCode (userId: User['id'], recoveryCode: string) {\n const hashes = await this.readRecoveryCodeHashes(userId)\n\n for (const [index, hash] of hashes.entries()) {\n if (await Hash.verify(recoveryCode, hash)) {\n await this.writeRecoveryCodeHashes(\n userId,\n hashes.filter((_, currentIndex) => currentIndex !== index),\n )\n\n return true\n }\n }\n\n return false\n }\n\n static async readStatus (userId: User['id']): Promise<TwoFactorStatus> {\n const record = await this.getRecord(userId)\n const enabledAt = record?.enabledAt?.toISOString() ?? null\n const recoveryCodes = record?.recoveryCodeHashes ?? []\n\n return {\n enabled: !!enabledAt,\n enabledAt,\n method: this.normalizeMethod(record?.method),\n recoveryCodesRemaining: recoveryCodes.length,\n }\n }\n\n static createSmsCode () {\n return Math.floor(100000 + Math.random() * 900000).toString()\n }\n\n static async issueSmsCode (user: User, purpose: SmsCodePurpose): Promise<IssuedSmsCode> {\n if (!(user as TwoFactorUser).phone) {\n throw new Error('A phone number is required to issue a two-factor SMS code.')\n }\n\n const code = this.createSmsCode()\n const smsCodeHash = await Hash.make(code)\n const expiresAt = new Date(Date.now() + smsCodeTtlMinutes() * 60 * 1000)\n\n await this.upsert(user.id, {\n smsCodeHash,\n smsCodeExpiresAt: expiresAt,\n smsCodePurpose: purpose,\n })\n\n return {\n code,\n expiresAt,\n purpose,\n }\n }\n\n static async clearSmsCode (userId: User['id']) {\n await this.upsert(userId, {\n smsCodeHash: null,\n smsCodeExpiresAt: null,\n smsCodePurpose: null,\n })\n }\n\n static async verifySmsCode (userId: User['id'], code: string, purpose: SmsCodePurpose) {\n const record = await this.getRecord(userId)\n\n if (!record?.smsCodeHash || !record.smsCodeExpiresAt || record.smsCodePurpose !== purpose) {\n return false\n }\n\n if (record.smsCodeExpiresAt.getTime() < Date.now()) {\n await this.clearSmsCode(userId)\n\n return false\n }\n\n const isValid = await Hash.verify(code, record.smsCodeHash)\n\n if (isValid) {\n await this.clearSmsCode(userId)\n }\n\n return isValid\n }\n}\n","import { Model } from '@arkstack/database'\n\nexport abstract class PersonalAccessToken extends Model {\n declare id: number\n declare name: string\n declare token: string\n declare abilities: string[]\n declare userId: number\n declare createdAt: Date\n declare expiresAt: Date | null\n declare lastUsedAt: Date | null\n declare deviceInfo: Record<string, unknown> | null\n}\n","import { Model } from '@arkstack/database'\n\nexport abstract class User extends Model {\n declare id: number\n declare email: string\n declare name: string\n declare password: string\n declare createdAt: Date\n declare updatedAt: Date\n\n protected static table?: string | undefined = 'users'\n}","import { Model } from '@arkstack/database'\n\nimport type { SmsCodePurpose, TwoFactorMethod } from '../types/TwoFactor'\nimport type { User } from './User'\n\nexport abstract class UserTwoFactor extends Model {\n declare id: number | string\n declare userId: User['id']\n declare method: TwoFactorMethod | null\n declare secretCiphertext: string | null\n declare smsCodeHash: string | null\n declare smsCodeExpiresAt: Date | null\n declare smsCodePurpose: SmsCodePurpose | null\n declare enabledAt: Date | null\n declare recoveryCodeHashes: string[] | null\n declare createdAt: Date\n declare updatedAt: Date\n\n protected static override table?: string | undefined = 'user_two_factors'\n\n protected casts = {\n recoveryCodeHashes: 'json',\n } as const\n}\n"],"mappings":"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"}
|