@breadstone/archipel-platform-authentication 0.0.1
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 +47 -0
- package/package.json +67 -0
- package/src/AuthModule.d.ts +57 -0
- package/src/AuthModule.js +158 -0
- package/src/contracts/AuthSubjectPort.d.ts +33 -0
- package/src/contracts/AuthSubjectPort.js +15 -0
- package/src/contracts/IAuthSubject.d.ts +40 -0
- package/src/contracts/IAuthSubject.js +3 -0
- package/src/contracts/IMfaSubject.d.ts +48 -0
- package/src/contracts/IMfaSubject.js +3 -0
- package/src/contracts/IMfaSubjectUpdate.d.ts +16 -0
- package/src/contracts/IMfaSubjectUpdate.js +3 -0
- package/src/contracts/ISessionRecord.d.ts +22 -0
- package/src/contracts/ISessionRecord.js +3 -0
- package/src/contracts/ISocialProfile.d.ts +23 -0
- package/src/contracts/ISocialProfile.js +3 -0
- package/src/contracts/IStoreSessionArgs.d.ts +14 -0
- package/src/contracts/IStoreSessionArgs.js +4 -0
- package/src/contracts/IVerifiableSubject.d.ts +28 -0
- package/src/contracts/IVerifiableSubject.js +3 -0
- package/src/contracts/MfaSubjectPort.d.ts +26 -0
- package/src/contracts/MfaSubjectPort.js +15 -0
- package/src/contracts/SessionPersistencePort.d.ts +58 -0
- package/src/contracts/SessionPersistencePort.js +15 -0
- package/src/contracts/SocialAuthPort.d.ts +19 -0
- package/src/contracts/SocialAuthPort.js +15 -0
- package/src/contracts/TokenEnricherPort.d.ts +17 -0
- package/src/contracts/TokenEnricherPort.js +15 -0
- package/src/contracts/VerificationSubjectPort.d.ts +40 -0
- package/src/contracts/VerificationSubjectPort.js +15 -0
- package/src/contracts/index.d.ts +13 -0
- package/src/contracts/index.js +16 -0
- package/src/decorators/RolesDecorator.d.ts +12 -0
- package/src/decorators/RolesDecorator.js +23 -0
- package/src/decorators/TokenDecorator.d.ts +6 -0
- package/src/decorators/TokenDecorator.js +20 -0
- package/src/decorators/UserDecorator.d.ts +6 -0
- package/src/decorators/UserDecorator.js +16 -0
- package/src/decorators/index.d.ts +3 -0
- package/src/decorators/index.js +7 -0
- package/src/env.d.ts +57 -0
- package/src/env.js +179 -0
- package/src/guards/AnonymousAuthGuard.d.ts +9 -0
- package/src/guards/AnonymousAuthGuard.js +20 -0
- package/src/guards/GithubAuthGuard.d.ts +9 -0
- package/src/guards/GithubAuthGuard.js +20 -0
- package/src/guards/JwtAuthGuard.d.ts +9 -0
- package/src/guards/JwtAuthGuard.js +20 -0
- package/src/guards/LocalAuthGuard.d.ts +9 -0
- package/src/guards/LocalAuthGuard.js +20 -0
- package/src/guards/RolesGuard.d.ts +16 -0
- package/src/guards/RolesGuard.js +43 -0
- package/src/guards/SocialAuthGuard.d.ts +9 -0
- package/src/guards/SocialAuthGuard.js +35 -0
- package/src/guards/_RefreshTokenAuthGuard.d.ts +0 -0
- package/src/guards/_RefreshTokenAuthGuard.js +2 -0
- package/src/guards/_TwoFactorAuthGuard.d.ts +0 -0
- package/src/guards/_TwoFactorAuthGuard.js +2 -0
- package/src/guards/index.d.ts +6 -0
- package/src/guards/index.js +12 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +13 -0
- package/src/mappers/SessionMappingProfile.d.ts +42 -0
- package/src/mappers/SessionMappingProfile.js +68 -0
- package/src/middlewares/LastActiveMiddleware.d.ts +29 -0
- package/src/middlewares/LastActiveMiddleware.js +55 -0
- package/src/middlewares/index.d.ts +1 -0
- package/src/middlewares/index.js +5 -0
- package/src/models/IMfaChallengePayload.d.ts +31 -0
- package/src/models/IMfaChallengePayload.js +3 -0
- package/src/models/IMfaConfirmation.d.ts +11 -0
- package/src/models/IMfaConfirmation.js +3 -0
- package/src/models/IMfaSetup.d.ts +23 -0
- package/src/models/IMfaSetup.js +3 -0
- package/src/models/ISignInResult.d.ts +31 -0
- package/src/models/ISignInResult.js +3 -0
- package/src/models/JwtPayloadBase.d.ts +44 -0
- package/src/models/JwtPayloadBase.js +55 -0
- package/src/models/index.d.ts +6 -0
- package/src/models/index.js +10 -0
- package/src/models/responses/SessionResponse.d.ts +20 -0
- package/src/models/responses/SessionResponse.js +164 -0
- package/src/models/responses/index.d.ts +1 -0
- package/src/models/responses/index.js +5 -0
- package/src/serializer/SessionSerializer.d.ts +5 -0
- package/src/serializer/SessionSerializer.js +23 -0
- package/src/services/AuthTokenService.d.ts +30 -0
- package/src/services/AuthTokenService.js +58 -0
- package/src/services/ChallengeService.d.ts +41 -0
- package/src/services/ChallengeService.js +89 -0
- package/src/services/MfaService.d.ts +60 -0
- package/src/services/MfaService.js +251 -0
- package/src/services/SessionService.d.ts +66 -0
- package/src/services/SessionService.js +108 -0
- package/src/services/VerificationService.d.ts +56 -0
- package/src/services/VerificationService.js +124 -0
- package/src/services/index.d.ts +5 -0
- package/src/services/index.js +9 -0
- package/src/services/mfa/IMfaChannel.d.ts +40 -0
- package/src/services/mfa/IMfaChannel.js +4 -0
- package/src/services/mfa/MfaEncryptionService.d.ts +23 -0
- package/src/services/mfa/MfaEncryptionService.js +59 -0
- package/src/services/mfa/MfaState.d.ts +38 -0
- package/src/services/mfa/MfaState.js +23 -0
- package/src/services/mfa/MfaTokens.d.ts +4 -0
- package/src/services/mfa/MfaTokens.js +8 -0
- package/src/services/mfa/channels/EmailOtpMfaChannel.d.ts +21 -0
- package/src/services/mfa/channels/EmailOtpMfaChannel.js +152 -0
- package/src/services/mfa/channels/PushMfaChannel.d.ts +25 -0
- package/src/services/mfa/channels/PushMfaChannel.js +150 -0
- package/src/services/mfa/channels/SmsMfaChannel.d.ts +21 -0
- package/src/services/mfa/channels/SmsMfaChannel.js +158 -0
- package/src/services/mfa/channels/TotpMfaChannel.d.ts +19 -0
- package/src/services/mfa/channels/TotpMfaChannel.js +100 -0
- package/src/strategies/AnonymousStrategy.d.ts +32 -0
- package/src/strategies/AnonymousStrategy.js +53 -0
- package/src/strategies/AppleStrategy.d.ts +38 -0
- package/src/strategies/AppleStrategy.js +79 -0
- package/src/strategies/GithubStrategy.d.ts +24 -0
- package/src/strategies/GithubStrategy.js +65 -0
- package/src/strategies/GoogleStrategy.d.ts +34 -0
- package/src/strategies/GoogleStrategy.js +74 -0
- package/src/strategies/JwtStrategy.d.ts +31 -0
- package/src/strategies/JwtStrategy.js +55 -0
- package/src/strategies/LocalStrategy.d.ts +31 -0
- package/src/strategies/LocalStrategy.js +56 -0
- package/src/strategies/MicrosoftStrategy.d.ts +35 -0
- package/src/strategies/MicrosoftStrategy.js +75 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConfigService } from '@breadstone/archipel-platform-core';
|
|
2
|
+
/**
|
|
3
|
+
* Provides AES-GCM encryption for MFA secrets.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export declare class MfaEncryptionService {
|
|
8
|
+
private readonly _key;
|
|
9
|
+
constructor(configService: ConfigService);
|
|
10
|
+
/**
|
|
11
|
+
* Encrypts the provided secret using AES-256-GCM.
|
|
12
|
+
*
|
|
13
|
+
* @param secret Plaintext secret to encrypt.
|
|
14
|
+
* @returns Base64 encoded payload containing IV, auth tag, and ciphertext.
|
|
15
|
+
*/
|
|
16
|
+
encrypt(secret: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Decrypts the payload produced by {@link encrypt}.
|
|
19
|
+
*
|
|
20
|
+
* @param payload Base64 encoded payload with IV, auth tag, and ciphertext.
|
|
21
|
+
*/
|
|
22
|
+
decrypt(payload: string): string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// #region Imports
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.MfaEncryptionService = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const archipel_platform_core_1 = require("@breadstone/archipel-platform-core");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
// #endregion
|
|
10
|
+
/**
|
|
11
|
+
* Provides AES-GCM encryption for MFA secrets.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
let MfaEncryptionService = class MfaEncryptionService {
|
|
16
|
+
// #endregion
|
|
17
|
+
// #region Ctor
|
|
18
|
+
constructor(configService) {
|
|
19
|
+
const rawKey = configService.tryGet('AUTH_MFA_ENCRYPTION_KEY', '');
|
|
20
|
+
const material = rawKey && rawKey.length > 0 ? rawKey : 'strivio-mfa-default-key';
|
|
21
|
+
this._key = (0, node_crypto_1.createHash)('sha256').update(material).digest();
|
|
22
|
+
}
|
|
23
|
+
// #endregion
|
|
24
|
+
// #region Methods
|
|
25
|
+
/**
|
|
26
|
+
* Encrypts the provided secret using AES-256-GCM.
|
|
27
|
+
*
|
|
28
|
+
* @param secret Plaintext secret to encrypt.
|
|
29
|
+
* @returns Base64 encoded payload containing IV, auth tag, and ciphertext.
|
|
30
|
+
*/
|
|
31
|
+
encrypt(secret) {
|
|
32
|
+
const iv = (0, node_crypto_1.randomBytes)(12);
|
|
33
|
+
const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', this._key, iv);
|
|
34
|
+
const encrypted = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
|
|
35
|
+
const authTag = cipher.getAuthTag();
|
|
36
|
+
return Buffer.concat([iv, authTag, encrypted]).toString('base64');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Decrypts the payload produced by {@link encrypt}.
|
|
40
|
+
*
|
|
41
|
+
* @param payload Base64 encoded payload with IV, auth tag, and ciphertext.
|
|
42
|
+
*/
|
|
43
|
+
decrypt(payload) {
|
|
44
|
+
const buffer = Buffer.from(payload, 'base64');
|
|
45
|
+
const iv = buffer.subarray(0, 12);
|
|
46
|
+
const authTag = buffer.subarray(12, 28);
|
|
47
|
+
const ciphertext = buffer.subarray(28);
|
|
48
|
+
const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', this._key, iv);
|
|
49
|
+
decipher.setAuthTag(authTag);
|
|
50
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
51
|
+
return decrypted.toString('utf8');
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
exports.MfaEncryptionService = MfaEncryptionService;
|
|
55
|
+
exports.MfaEncryptionService = MfaEncryptionService = tslib_1.__decorate([
|
|
56
|
+
(0, common_1.Injectable)(),
|
|
57
|
+
tslib_1.__metadata("design:paramtypes", [archipel_platform_core_1.ConfigService])
|
|
58
|
+
], MfaEncryptionService);
|
|
59
|
+
//# sourceMappingURL=MfaEncryptionService.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents per-channel MFA configuration persisted on the user.
|
|
3
|
+
*
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface IUserMfaChannelState {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
secret?: string;
|
|
9
|
+
phoneNumber?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
codeHash?: string;
|
|
12
|
+
codeExpiresAt?: string;
|
|
13
|
+
lastSentAt?: string;
|
|
14
|
+
verifiedAt?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Represents the full MFA state payload stored with the user.
|
|
18
|
+
*
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export interface IUserMfaState {
|
|
22
|
+
preferredMethod?: string;
|
|
23
|
+
channels: Record<string, IUserMfaChannelState>;
|
|
24
|
+
trustedDevices?: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Optional channel-specific setup context.
|
|
28
|
+
*/
|
|
29
|
+
export interface IMfaSetupContext {
|
|
30
|
+
phoneNumber?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes a raw persisted MFA blob into a usable state object.
|
|
35
|
+
*
|
|
36
|
+
* @param raw Raw value from persistence.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveMfaState(raw: unknown): IUserMfaState;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveMfaState = resolveMfaState;
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes a raw persisted MFA blob into a usable state object.
|
|
6
|
+
*
|
|
7
|
+
* @param raw Raw value from persistence.
|
|
8
|
+
*/
|
|
9
|
+
function resolveMfaState(raw) {
|
|
10
|
+
if (raw && typeof raw === 'object' && 'channels' in raw) {
|
|
11
|
+
const typed = raw;
|
|
12
|
+
return {
|
|
13
|
+
preferredMethod: typed.preferredMethod,
|
|
14
|
+
channels: typed.channels ?? {},
|
|
15
|
+
trustedDevices: typed.trustedDevices ?? []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
channels: {},
|
|
20
|
+
trustedDevices: []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=MfaState.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MFA_CHANNELS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Injection token for pluggable MFA channel implementations.
|
|
6
|
+
*/
|
|
7
|
+
exports.MFA_CHANNELS = 'MFA_CHANNELS';
|
|
8
|
+
//# sourceMappingURL=MfaTokens.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BcryptService, ConfigService } from '@breadstone/archipel-platform-core';
|
|
2
|
+
import { MfaSubjectPort } from '../../../contracts/MfaSubjectPort';
|
|
3
|
+
import { IMfaSetup } from '../../../models';
|
|
4
|
+
import { IMfaChannel } from '../IMfaChannel';
|
|
5
|
+
import { IMfaSetupContext } from '../MfaState';
|
|
6
|
+
export declare class EmailOtpMfaChannel implements IMfaChannel {
|
|
7
|
+
readonly method = "email";
|
|
8
|
+
private readonly _mfaSubject;
|
|
9
|
+
private readonly _configService;
|
|
10
|
+
private readonly _bcryptService;
|
|
11
|
+
private readonly _logger;
|
|
12
|
+
private readonly _ttlMilliseconds;
|
|
13
|
+
private readonly _minResendMilliseconds;
|
|
14
|
+
constructor(mfaSubject: MfaSubjectPort, configService: ConfigService, bcryptService: BcryptService);
|
|
15
|
+
initSetup(userId: string, context?: IMfaSetupContext): Promise<IMfaSetup>;
|
|
16
|
+
verifySetup(userId: string, code: string): Promise<boolean>;
|
|
17
|
+
verifyChallenge(userId: string, code: string): Promise<boolean>;
|
|
18
|
+
issueChallenge(userId: string): Promise<void>;
|
|
19
|
+
private verifyCodeAndMaybeEnable;
|
|
20
|
+
private generateCode;
|
|
21
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// #region Imports
|
|
3
|
+
var EmailOtpMfaChannel_1;
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.EmailOtpMfaChannel = void 0;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const archipel_platform_core_1 = require("@breadstone/archipel-platform-core");
|
|
8
|
+
const common_1 = require("@nestjs/common");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const MfaSubjectPort_1 = require("../../../contracts/MfaSubjectPort");
|
|
11
|
+
const MfaState_1 = require("../MfaState");
|
|
12
|
+
// #endregion
|
|
13
|
+
let EmailOtpMfaChannel = EmailOtpMfaChannel_1 = class EmailOtpMfaChannel {
|
|
14
|
+
constructor(mfaSubject, configService, bcryptService) {
|
|
15
|
+
this.method = 'email';
|
|
16
|
+
this._logger = new common_1.Logger(EmailOtpMfaChannel_1.name);
|
|
17
|
+
this._mfaSubject = mfaSubject;
|
|
18
|
+
this._configService = configService;
|
|
19
|
+
this._bcryptService = bcryptService;
|
|
20
|
+
this._ttlMilliseconds = archipel_platform_core_1.DateTimeUtils.parse(this._configService.tryGet('AUTH_MFA_EMAIL_CODE_TTL', '10m'));
|
|
21
|
+
this._minResendMilliseconds = archipel_platform_core_1.DateTimeUtils.parse(this._configService.tryGet('AUTH_MFA_EMAIL_MIN_RESEND', '30s'));
|
|
22
|
+
}
|
|
23
|
+
async initSetup(userId, context) {
|
|
24
|
+
const user = await this._mfaSubject.findById(userId);
|
|
25
|
+
if (!user) {
|
|
26
|
+
throw new common_1.BadRequestException('User not found');
|
|
27
|
+
}
|
|
28
|
+
const email = context?.email ?? user.email;
|
|
29
|
+
if (!email) {
|
|
30
|
+
throw new common_1.BadRequestException('Email is required for email MFA');
|
|
31
|
+
}
|
|
32
|
+
const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
|
|
33
|
+
const channel = state.channels[this.method] ?? { enabled: false };
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
if (channel.lastSentAt && now - Date.parse(channel.lastSentAt) < this._minResendMilliseconds) {
|
|
36
|
+
throw new common_1.BadRequestException('OTP recently sent. Please wait before requesting another code.');
|
|
37
|
+
}
|
|
38
|
+
const code = this.generateCode();
|
|
39
|
+
const hashed = await this._bcryptService.hash(code);
|
|
40
|
+
state.channels[this.method] = {
|
|
41
|
+
enabled: false,
|
|
42
|
+
email: email,
|
|
43
|
+
codeHash: hashed,
|
|
44
|
+
codeExpiresAt: new Date(now + this._ttlMilliseconds).toISOString(),
|
|
45
|
+
lastSentAt: new Date(now).toISOString(),
|
|
46
|
+
};
|
|
47
|
+
state.preferredMethod = state.preferredMethod ?? this.method;
|
|
48
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
49
|
+
mfaChannels: state,
|
|
50
|
+
mfaPreferredMethod: state.preferredMethod,
|
|
51
|
+
mfaUpdatedAt: new Date(),
|
|
52
|
+
});
|
|
53
|
+
this._logger.log(`Sending Email MFA code to ${email} (mock): ${code}`);
|
|
54
|
+
return {
|
|
55
|
+
method: this.method,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async verifySetup(userId, code) {
|
|
59
|
+
return this.verifyCodeAndMaybeEnable(userId, code, true);
|
|
60
|
+
}
|
|
61
|
+
async verifyChallenge(userId, code) {
|
|
62
|
+
return this.verifyCodeAndMaybeEnable(userId, code, false);
|
|
63
|
+
}
|
|
64
|
+
async issueChallenge(userId) {
|
|
65
|
+
const user = await this._mfaSubject.findById(userId);
|
|
66
|
+
if (!user) {
|
|
67
|
+
throw new common_1.BadRequestException('User not found');
|
|
68
|
+
}
|
|
69
|
+
const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
|
|
70
|
+
const channel = state.channels[this.method];
|
|
71
|
+
if (!channel?.enabled) {
|
|
72
|
+
throw new common_1.UnauthorizedException('Email MFA is not enabled');
|
|
73
|
+
}
|
|
74
|
+
const email = channel.email ?? user.email;
|
|
75
|
+
if (!email) {
|
|
76
|
+
throw new common_1.BadRequestException('Email is required for email MFA');
|
|
77
|
+
}
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (channel.lastSentAt && now - Date.parse(channel.lastSentAt) < this._minResendMilliseconds) {
|
|
80
|
+
throw new common_1.BadRequestException('OTP recently sent. Please wait before requesting another code.');
|
|
81
|
+
}
|
|
82
|
+
const code = this.generateCode();
|
|
83
|
+
const hashed = await this._bcryptService.hash(code);
|
|
84
|
+
state.channels[this.method] = {
|
|
85
|
+
...channel,
|
|
86
|
+
email: email,
|
|
87
|
+
codeHash: hashed,
|
|
88
|
+
codeExpiresAt: new Date(now + this._ttlMilliseconds).toISOString(),
|
|
89
|
+
lastSentAt: new Date(now).toISOString(),
|
|
90
|
+
};
|
|
91
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
92
|
+
mfaChannels: state,
|
|
93
|
+
mfaUpdatedAt: new Date(),
|
|
94
|
+
});
|
|
95
|
+
this._logger.log(`Sending Email MFA code to ${email} (login): ${code}`);
|
|
96
|
+
}
|
|
97
|
+
async verifyCodeAndMaybeEnable(userId, code, enable) {
|
|
98
|
+
const user = await this._mfaSubject.findById(userId);
|
|
99
|
+
if (!user) {
|
|
100
|
+
throw new common_1.BadRequestException('User not found');
|
|
101
|
+
}
|
|
102
|
+
const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
|
|
103
|
+
const channel = state.channels[this.method];
|
|
104
|
+
if (!channel?.codeHash || !channel.codeExpiresAt) {
|
|
105
|
+
throw new common_1.UnauthorizedException('Email code not issued');
|
|
106
|
+
}
|
|
107
|
+
if (Date.parse(channel.codeExpiresAt) < Date.now()) {
|
|
108
|
+
throw new common_1.UnauthorizedException('Email code expired');
|
|
109
|
+
}
|
|
110
|
+
const isValid = await this._bcryptService.compare(code, channel.codeHash);
|
|
111
|
+
if (!isValid) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (enable) {
|
|
115
|
+
state.channels[this.method] = {
|
|
116
|
+
...channel,
|
|
117
|
+
enabled: true,
|
|
118
|
+
verifiedAt: new Date().toISOString(),
|
|
119
|
+
codeHash: undefined,
|
|
120
|
+
codeExpiresAt: undefined,
|
|
121
|
+
};
|
|
122
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
123
|
+
mfaChannels: state,
|
|
124
|
+
mfaEnabled: true,
|
|
125
|
+
mfaPreferredMethod: state.preferredMethod ?? this.method,
|
|
126
|
+
mfaVerifiedAt: new Date(),
|
|
127
|
+
mfaUpdatedAt: new Date(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
state.channels[this.method] = {
|
|
132
|
+
...channel,
|
|
133
|
+
codeHash: undefined,
|
|
134
|
+
codeExpiresAt: undefined,
|
|
135
|
+
};
|
|
136
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
137
|
+
mfaChannels: state,
|
|
138
|
+
mfaUpdatedAt: new Date(),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
generateCode() {
|
|
144
|
+
return (0, node_crypto_1.randomInt)(100000, 999999).toString();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
exports.EmailOtpMfaChannel = EmailOtpMfaChannel;
|
|
148
|
+
exports.EmailOtpMfaChannel = EmailOtpMfaChannel = EmailOtpMfaChannel_1 = tslib_1.__decorate([
|
|
149
|
+
(0, common_1.Injectable)(),
|
|
150
|
+
tslib_1.__metadata("design:paramtypes", [MfaSubjectPort_1.MfaSubjectPort, archipel_platform_core_1.ConfigService, archipel_platform_core_1.BcryptService])
|
|
151
|
+
], EmailOtpMfaChannel);
|
|
152
|
+
//# sourceMappingURL=EmailOtpMfaChannel.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BcryptService, ConfigService } from '@breadstone/archipel-platform-core';
|
|
2
|
+
import { MfaSubjectPort } from '../../../contracts/MfaSubjectPort';
|
|
3
|
+
import { IMfaSetup } from '../../../models';
|
|
4
|
+
import { IMfaChannel } from '../IMfaChannel';
|
|
5
|
+
import { IMfaSetupContext } from '../MfaState';
|
|
6
|
+
/**
|
|
7
|
+
* Simplified push-style MFA channel that simulates a device approval code.
|
|
8
|
+
* This is intentionally minimal so it can be replaced by a real push provider later.
|
|
9
|
+
*/
|
|
10
|
+
export declare class PushMfaChannel implements IMfaChannel {
|
|
11
|
+
readonly method = "push";
|
|
12
|
+
private readonly _mfaSubject;
|
|
13
|
+
private readonly _configService;
|
|
14
|
+
private readonly _bcryptService;
|
|
15
|
+
private readonly _logger;
|
|
16
|
+
private readonly _ttlMilliseconds;
|
|
17
|
+
private readonly _minResendMilliseconds;
|
|
18
|
+
constructor(mfaSubject: MfaSubjectPort, configService: ConfigService, bcryptService: BcryptService);
|
|
19
|
+
initSetup(userId: string, _context?: IMfaSetupContext): Promise<IMfaSetup>;
|
|
20
|
+
verifySetup(userId: string, code: string): Promise<boolean>;
|
|
21
|
+
verifyChallenge(userId: string, code: string): Promise<boolean>;
|
|
22
|
+
issueChallenge(userId: string): Promise<void>;
|
|
23
|
+
private verifyCodeAndMaybeEnable;
|
|
24
|
+
private generateCode;
|
|
25
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// #region Imports
|
|
3
|
+
var PushMfaChannel_1;
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.PushMfaChannel = void 0;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const archipel_platform_core_1 = require("@breadstone/archipel-platform-core");
|
|
8
|
+
const common_1 = require("@nestjs/common");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const MfaSubjectPort_1 = require("../../../contracts/MfaSubjectPort");
|
|
11
|
+
const MfaState_1 = require("../MfaState");
|
|
12
|
+
// #endregion
|
|
13
|
+
/**
|
|
14
|
+
* Simplified push-style MFA channel that simulates a device approval code.
|
|
15
|
+
* This is intentionally minimal so it can be replaced by a real push provider later.
|
|
16
|
+
*/
|
|
17
|
+
let PushMfaChannel = PushMfaChannel_1 = class PushMfaChannel {
|
|
18
|
+
// #endregion
|
|
19
|
+
// #region Ctor
|
|
20
|
+
constructor(mfaSubject, configService, bcryptService) {
|
|
21
|
+
// #region Fields
|
|
22
|
+
this.method = 'push';
|
|
23
|
+
this._logger = new common_1.Logger(PushMfaChannel_1.name);
|
|
24
|
+
this._mfaSubject = mfaSubject;
|
|
25
|
+
this._configService = configService;
|
|
26
|
+
this._bcryptService = bcryptService;
|
|
27
|
+
this._ttlMilliseconds = archipel_platform_core_1.DateTimeUtils.parse(this._configService.tryGet('AUTH_MFA_PUSH_CODE_TTL', '10m'));
|
|
28
|
+
this._minResendMilliseconds = archipel_platform_core_1.DateTimeUtils.parse(this._configService.tryGet('AUTH_MFA_PUSH_MIN_RESEND', '30s'));
|
|
29
|
+
}
|
|
30
|
+
// #endregion
|
|
31
|
+
// #region Methods
|
|
32
|
+
async initSetup(userId, _context) {
|
|
33
|
+
const user = await this._mfaSubject.findById(userId);
|
|
34
|
+
if (!user) {
|
|
35
|
+
throw new common_1.BadRequestException('User not found');
|
|
36
|
+
}
|
|
37
|
+
const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
|
|
38
|
+
const channel = state.channels[this.method] ?? { enabled: false };
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (channel.lastSentAt && now - Date.parse(channel.lastSentAt) < this._minResendMilliseconds) {
|
|
41
|
+
throw new common_1.BadRequestException('Approval request recently sent. Please wait before requesting another code.');
|
|
42
|
+
}
|
|
43
|
+
const code = this.generateCode();
|
|
44
|
+
const hashed = await this._bcryptService.hash(code);
|
|
45
|
+
state.channels[this.method] = {
|
|
46
|
+
enabled: false,
|
|
47
|
+
codeHash: hashed,
|
|
48
|
+
codeExpiresAt: new Date(now + this._ttlMilliseconds).toISOString(),
|
|
49
|
+
lastSentAt: new Date(now).toISOString(),
|
|
50
|
+
};
|
|
51
|
+
state.preferredMethod = state.preferredMethod ?? this.method;
|
|
52
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
53
|
+
mfaChannels: state,
|
|
54
|
+
mfaPreferredMethod: state.preferredMethod,
|
|
55
|
+
mfaUpdatedAt: new Date(),
|
|
56
|
+
});
|
|
57
|
+
// Replace with actual push notification send once available.
|
|
58
|
+
this._logger.log(`Simulating push approval code for user ${user.id}: ${code}`);
|
|
59
|
+
return { method: this.method };
|
|
60
|
+
}
|
|
61
|
+
async verifySetup(userId, code) {
|
|
62
|
+
return this.verifyCodeAndMaybeEnable(userId, code, true);
|
|
63
|
+
}
|
|
64
|
+
async verifyChallenge(userId, code) {
|
|
65
|
+
return this.verifyCodeAndMaybeEnable(userId, code, false);
|
|
66
|
+
}
|
|
67
|
+
async issueChallenge(userId) {
|
|
68
|
+
const user = await this._mfaSubject.findById(userId);
|
|
69
|
+
if (!user) {
|
|
70
|
+
throw new common_1.BadRequestException('User not found');
|
|
71
|
+
}
|
|
72
|
+
const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
|
|
73
|
+
const channel = state.channels[this.method];
|
|
74
|
+
if (!channel?.enabled) {
|
|
75
|
+
throw new common_1.UnauthorizedException('Push MFA is not enabled');
|
|
76
|
+
}
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (channel.lastSentAt && now - Date.parse(channel.lastSentAt) < this._minResendMilliseconds) {
|
|
79
|
+
throw new common_1.BadRequestException('Approval request recently sent. Please wait before requesting another code.');
|
|
80
|
+
}
|
|
81
|
+
const code = this.generateCode();
|
|
82
|
+
const hashed = await this._bcryptService.hash(code);
|
|
83
|
+
state.channels[this.method] = {
|
|
84
|
+
...channel,
|
|
85
|
+
codeHash: hashed,
|
|
86
|
+
codeExpiresAt: new Date(now + this._ttlMilliseconds).toISOString(),
|
|
87
|
+
lastSentAt: new Date(now).toISOString(),
|
|
88
|
+
};
|
|
89
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
90
|
+
mfaChannels: state,
|
|
91
|
+
mfaUpdatedAt: new Date(),
|
|
92
|
+
});
|
|
93
|
+
this._logger.log(`Simulating push approval for user ${user.id}: ${code}`);
|
|
94
|
+
}
|
|
95
|
+
async verifyCodeAndMaybeEnable(userId, code, enable) {
|
|
96
|
+
const user = await this._mfaSubject.findById(userId);
|
|
97
|
+
if (!user) {
|
|
98
|
+
throw new common_1.BadRequestException('User not found');
|
|
99
|
+
}
|
|
100
|
+
const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
|
|
101
|
+
const channel = state.channels[this.method];
|
|
102
|
+
if (!channel?.codeHash || !channel.codeExpiresAt) {
|
|
103
|
+
throw new common_1.UnauthorizedException('Push approval not issued');
|
|
104
|
+
}
|
|
105
|
+
if (Date.parse(channel.codeExpiresAt) < Date.now()) {
|
|
106
|
+
throw new common_1.UnauthorizedException('Push approval expired');
|
|
107
|
+
}
|
|
108
|
+
const isValid = await this._bcryptService.compare(code, channel.codeHash);
|
|
109
|
+
if (!isValid) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (enable) {
|
|
113
|
+
state.channels[this.method] = {
|
|
114
|
+
...channel,
|
|
115
|
+
enabled: true,
|
|
116
|
+
verifiedAt: new Date().toISOString(),
|
|
117
|
+
codeHash: undefined,
|
|
118
|
+
codeExpiresAt: undefined,
|
|
119
|
+
};
|
|
120
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
121
|
+
mfaChannels: state,
|
|
122
|
+
mfaEnabled: true,
|
|
123
|
+
mfaPreferredMethod: state.preferredMethod ?? this.method,
|
|
124
|
+
mfaVerifiedAt: new Date(),
|
|
125
|
+
mfaUpdatedAt: new Date(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
state.channels[this.method] = {
|
|
130
|
+
...channel,
|
|
131
|
+
codeHash: undefined,
|
|
132
|
+
codeExpiresAt: undefined,
|
|
133
|
+
};
|
|
134
|
+
await this._mfaSubject.updateMfaState(userId, {
|
|
135
|
+
mfaChannels: state,
|
|
136
|
+
mfaUpdatedAt: new Date(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
generateCode() {
|
|
142
|
+
return (0, node_crypto_1.randomInt)(100000, 999999).toString();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
exports.PushMfaChannel = PushMfaChannel;
|
|
146
|
+
exports.PushMfaChannel = PushMfaChannel = PushMfaChannel_1 = tslib_1.__decorate([
|
|
147
|
+
(0, common_1.Injectable)(),
|
|
148
|
+
tslib_1.__metadata("design:paramtypes", [MfaSubjectPort_1.MfaSubjectPort, archipel_platform_core_1.ConfigService, archipel_platform_core_1.BcryptService])
|
|
149
|
+
], PushMfaChannel);
|
|
150
|
+
//# sourceMappingURL=PushMfaChannel.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BcryptService, ConfigService } from '@breadstone/archipel-platform-core';
|
|
2
|
+
import { MfaSubjectPort } from '../../../contracts/MfaSubjectPort';
|
|
3
|
+
import { IMfaSetup } from '../../../models';
|
|
4
|
+
import { IMfaChannel } from '../IMfaChannel';
|
|
5
|
+
import { IMfaSetupContext } from '../MfaState';
|
|
6
|
+
export declare class SmsMfaChannel implements IMfaChannel {
|
|
7
|
+
readonly method = "sms";
|
|
8
|
+
private readonly _mfaSubject;
|
|
9
|
+
private readonly _configService;
|
|
10
|
+
private readonly _bcryptService;
|
|
11
|
+
private readonly _logger;
|
|
12
|
+
private readonly _ttlMilliseconds;
|
|
13
|
+
private readonly _minResendMilliseconds;
|
|
14
|
+
constructor(mfaSubject: MfaSubjectPort, configService: ConfigService, bcryptService: BcryptService);
|
|
15
|
+
initSetup(userId: string, context?: IMfaSetupContext): Promise<IMfaSetup>;
|
|
16
|
+
verifySetup(userId: string, code: string): Promise<boolean>;
|
|
17
|
+
verifyChallenge(userId: string, code: string): Promise<boolean>;
|
|
18
|
+
issueChallenge(userId: string): Promise<void>;
|
|
19
|
+
private verifyCodeAndMaybeEnable;
|
|
20
|
+
private generateCode;
|
|
21
|
+
}
|