@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.
Files changed (128) hide show
  1. package/README.md +47 -0
  2. package/package.json +67 -0
  3. package/src/AuthModule.d.ts +57 -0
  4. package/src/AuthModule.js +158 -0
  5. package/src/contracts/AuthSubjectPort.d.ts +33 -0
  6. package/src/contracts/AuthSubjectPort.js +15 -0
  7. package/src/contracts/IAuthSubject.d.ts +40 -0
  8. package/src/contracts/IAuthSubject.js +3 -0
  9. package/src/contracts/IMfaSubject.d.ts +48 -0
  10. package/src/contracts/IMfaSubject.js +3 -0
  11. package/src/contracts/IMfaSubjectUpdate.d.ts +16 -0
  12. package/src/contracts/IMfaSubjectUpdate.js +3 -0
  13. package/src/contracts/ISessionRecord.d.ts +22 -0
  14. package/src/contracts/ISessionRecord.js +3 -0
  15. package/src/contracts/ISocialProfile.d.ts +23 -0
  16. package/src/contracts/ISocialProfile.js +3 -0
  17. package/src/contracts/IStoreSessionArgs.d.ts +14 -0
  18. package/src/contracts/IStoreSessionArgs.js +4 -0
  19. package/src/contracts/IVerifiableSubject.d.ts +28 -0
  20. package/src/contracts/IVerifiableSubject.js +3 -0
  21. package/src/contracts/MfaSubjectPort.d.ts +26 -0
  22. package/src/contracts/MfaSubjectPort.js +15 -0
  23. package/src/contracts/SessionPersistencePort.d.ts +58 -0
  24. package/src/contracts/SessionPersistencePort.js +15 -0
  25. package/src/contracts/SocialAuthPort.d.ts +19 -0
  26. package/src/contracts/SocialAuthPort.js +15 -0
  27. package/src/contracts/TokenEnricherPort.d.ts +17 -0
  28. package/src/contracts/TokenEnricherPort.js +15 -0
  29. package/src/contracts/VerificationSubjectPort.d.ts +40 -0
  30. package/src/contracts/VerificationSubjectPort.js +15 -0
  31. package/src/contracts/index.d.ts +13 -0
  32. package/src/contracts/index.js +16 -0
  33. package/src/decorators/RolesDecorator.d.ts +12 -0
  34. package/src/decorators/RolesDecorator.js +23 -0
  35. package/src/decorators/TokenDecorator.d.ts +6 -0
  36. package/src/decorators/TokenDecorator.js +20 -0
  37. package/src/decorators/UserDecorator.d.ts +6 -0
  38. package/src/decorators/UserDecorator.js +16 -0
  39. package/src/decorators/index.d.ts +3 -0
  40. package/src/decorators/index.js +7 -0
  41. package/src/env.d.ts +57 -0
  42. package/src/env.js +179 -0
  43. package/src/guards/AnonymousAuthGuard.d.ts +9 -0
  44. package/src/guards/AnonymousAuthGuard.js +20 -0
  45. package/src/guards/GithubAuthGuard.d.ts +9 -0
  46. package/src/guards/GithubAuthGuard.js +20 -0
  47. package/src/guards/JwtAuthGuard.d.ts +9 -0
  48. package/src/guards/JwtAuthGuard.js +20 -0
  49. package/src/guards/LocalAuthGuard.d.ts +9 -0
  50. package/src/guards/LocalAuthGuard.js +20 -0
  51. package/src/guards/RolesGuard.d.ts +16 -0
  52. package/src/guards/RolesGuard.js +43 -0
  53. package/src/guards/SocialAuthGuard.d.ts +9 -0
  54. package/src/guards/SocialAuthGuard.js +35 -0
  55. package/src/guards/_RefreshTokenAuthGuard.d.ts +0 -0
  56. package/src/guards/_RefreshTokenAuthGuard.js +2 -0
  57. package/src/guards/_TwoFactorAuthGuard.d.ts +0 -0
  58. package/src/guards/_TwoFactorAuthGuard.js +2 -0
  59. package/src/guards/index.d.ts +6 -0
  60. package/src/guards/index.js +12 -0
  61. package/src/index.d.ts +9 -0
  62. package/src/index.js +13 -0
  63. package/src/mappers/SessionMappingProfile.d.ts +42 -0
  64. package/src/mappers/SessionMappingProfile.js +68 -0
  65. package/src/middlewares/LastActiveMiddleware.d.ts +29 -0
  66. package/src/middlewares/LastActiveMiddleware.js +55 -0
  67. package/src/middlewares/index.d.ts +1 -0
  68. package/src/middlewares/index.js +5 -0
  69. package/src/models/IMfaChallengePayload.d.ts +31 -0
  70. package/src/models/IMfaChallengePayload.js +3 -0
  71. package/src/models/IMfaConfirmation.d.ts +11 -0
  72. package/src/models/IMfaConfirmation.js +3 -0
  73. package/src/models/IMfaSetup.d.ts +23 -0
  74. package/src/models/IMfaSetup.js +3 -0
  75. package/src/models/ISignInResult.d.ts +31 -0
  76. package/src/models/ISignInResult.js +3 -0
  77. package/src/models/JwtPayloadBase.d.ts +44 -0
  78. package/src/models/JwtPayloadBase.js +55 -0
  79. package/src/models/index.d.ts +6 -0
  80. package/src/models/index.js +10 -0
  81. package/src/models/responses/SessionResponse.d.ts +20 -0
  82. package/src/models/responses/SessionResponse.js +164 -0
  83. package/src/models/responses/index.d.ts +1 -0
  84. package/src/models/responses/index.js +5 -0
  85. package/src/serializer/SessionSerializer.d.ts +5 -0
  86. package/src/serializer/SessionSerializer.js +23 -0
  87. package/src/services/AuthTokenService.d.ts +30 -0
  88. package/src/services/AuthTokenService.js +58 -0
  89. package/src/services/ChallengeService.d.ts +41 -0
  90. package/src/services/ChallengeService.js +89 -0
  91. package/src/services/MfaService.d.ts +60 -0
  92. package/src/services/MfaService.js +251 -0
  93. package/src/services/SessionService.d.ts +66 -0
  94. package/src/services/SessionService.js +108 -0
  95. package/src/services/VerificationService.d.ts +56 -0
  96. package/src/services/VerificationService.js +124 -0
  97. package/src/services/index.d.ts +5 -0
  98. package/src/services/index.js +9 -0
  99. package/src/services/mfa/IMfaChannel.d.ts +40 -0
  100. package/src/services/mfa/IMfaChannel.js +4 -0
  101. package/src/services/mfa/MfaEncryptionService.d.ts +23 -0
  102. package/src/services/mfa/MfaEncryptionService.js +59 -0
  103. package/src/services/mfa/MfaState.d.ts +38 -0
  104. package/src/services/mfa/MfaState.js +23 -0
  105. package/src/services/mfa/MfaTokens.d.ts +4 -0
  106. package/src/services/mfa/MfaTokens.js +8 -0
  107. package/src/services/mfa/channels/EmailOtpMfaChannel.d.ts +21 -0
  108. package/src/services/mfa/channels/EmailOtpMfaChannel.js +152 -0
  109. package/src/services/mfa/channels/PushMfaChannel.d.ts +25 -0
  110. package/src/services/mfa/channels/PushMfaChannel.js +150 -0
  111. package/src/services/mfa/channels/SmsMfaChannel.d.ts +21 -0
  112. package/src/services/mfa/channels/SmsMfaChannel.js +158 -0
  113. package/src/services/mfa/channels/TotpMfaChannel.d.ts +19 -0
  114. package/src/services/mfa/channels/TotpMfaChannel.js +100 -0
  115. package/src/strategies/AnonymousStrategy.d.ts +32 -0
  116. package/src/strategies/AnonymousStrategy.js +53 -0
  117. package/src/strategies/AppleStrategy.d.ts +38 -0
  118. package/src/strategies/AppleStrategy.js +79 -0
  119. package/src/strategies/GithubStrategy.d.ts +24 -0
  120. package/src/strategies/GithubStrategy.js +65 -0
  121. package/src/strategies/GoogleStrategy.d.ts +34 -0
  122. package/src/strategies/GoogleStrategy.js +74 -0
  123. package/src/strategies/JwtStrategy.d.ts +31 -0
  124. package/src/strategies/JwtStrategy.js +55 -0
  125. package/src/strategies/LocalStrategy.d.ts +31 -0
  126. package/src/strategies/LocalStrategy.js +56 -0
  127. package/src/strategies/MicrosoftStrategy.d.ts +35 -0
  128. package/src/strategies/MicrosoftStrategy.js +75 -0
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ // #region Imports
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.MfaService = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const archipel_platform_core_1 = require("@breadstone/archipel-platform-core");
7
+ const common_1 = require("@nestjs/common");
8
+ const node_crypto_1 = require("node:crypto");
9
+ const MfaSubjectPort_1 = require("../contracts/MfaSubjectPort");
10
+ const MfaEncryptionService_1 = require("./mfa/MfaEncryptionService");
11
+ const MfaState_1 = require("./mfa/MfaState");
12
+ const MfaTokens_1 = require("./mfa/MfaTokens");
13
+ // #endregion
14
+ /**
15
+ * Orchestrates MFA channels, backup codes, and user preferences.
16
+ *
17
+ * @public
18
+ */
19
+ let MfaService = class MfaService {
20
+ // #endregion
21
+ // #region Ctor
22
+ constructor(bcryptService, mfaSubject, encryptionService, channels) {
23
+ this._bcryptService = bcryptService;
24
+ this._mfaSubject = mfaSubject;
25
+ this._channels = new Map(channels.map((channel) => [channel.method, channel]));
26
+ this._backupCodeCount = 10;
27
+ this._encryptionService = encryptionService;
28
+ }
29
+ // #endregion
30
+ // #region Methods
31
+ /**
32
+ * Initializes setup for a specific MFA method.
33
+ */
34
+ async initSetup(userId, method, context) {
35
+ const channel = this.requireChannel(method);
36
+ const user = await this.getUser(userId);
37
+ const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
38
+ if (state.channels[method]?.enabled) {
39
+ throw new common_1.BadRequestException('Selected MFA method is already enabled');
40
+ }
41
+ if (state.channels[method]?.enabled) {
42
+ throw new common_1.BadRequestException('Selected MFA method is already enabled');
43
+ }
44
+ return channel.initSetup(userId, context);
45
+ }
46
+ /**
47
+ * Verifies the initial code for a method and issues backup codes.
48
+ */
49
+ async verifySetup(userId, method, code) {
50
+ const channel = this.requireChannel(method);
51
+ if (!code?.trim()) {
52
+ throw new common_1.BadRequestException('A verification code is required');
53
+ }
54
+ const isValid = await channel.verifySetup(userId, code.trim());
55
+ if (!isValid) {
56
+ throw new common_1.UnauthorizedException('Invalid verification code');
57
+ }
58
+ const backupCodes = await this.generateBackupCodes(userId);
59
+ const latestUser = await this.getUser(userId);
60
+ const latestState = (0, MfaState_1.resolveMfaState)(latestUser.mfaChannels);
61
+ await this._mfaSubject.updateMfaState(userId, {
62
+ mfaEnabled: true,
63
+ mfaPreferredMethod: latestState.preferredMethod ?? method,
64
+ mfaBackupCodes: backupCodes.hashed,
65
+ mfaVerifiedAt: new Date(),
66
+ mfaUpdatedAt: new Date(),
67
+ mfaChannels: latestState,
68
+ });
69
+ return { backupCodes: backupCodes.plain };
70
+ }
71
+ /**
72
+ * Verifies a login challenge using the selected method or backup code.
73
+ */
74
+ async verifyChallenge(userId, method, code) {
75
+ const sanitizedCode = code.trim();
76
+ const user = await this.getUser(userId);
77
+ const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
78
+ if (!user.mfaEnabled) {
79
+ throw new common_1.BadRequestException('Multi-factor authentication is not enabled');
80
+ }
81
+ if (method === 'backup') {
82
+ const success = await this.tryConsumeBackupCode(user, sanitizedCode);
83
+ if (success) {
84
+ await this.touchVerification(userId);
85
+ }
86
+ return success;
87
+ }
88
+ const channel = this.requireChannel(method);
89
+ const enabledMethods = Object.entries(state.channels)
90
+ .filter(([, value]) => value.enabled)
91
+ .map(([key]) => key);
92
+ if (!enabledMethods.includes(method)) {
93
+ throw new common_1.UnauthorizedException('Selected method is not enabled');
94
+ }
95
+ const isValid = await channel.verifyChallenge(userId, sanitizedCode);
96
+ if (isValid) {
97
+ await this.touchVerification(userId);
98
+ return true;
99
+ }
100
+ const fallback = await this.tryConsumeBackupCode(user, sanitizedCode);
101
+ if (fallback) {
102
+ await this.touchVerification(userId);
103
+ }
104
+ return fallback;
105
+ }
106
+ /**
107
+ * Returns enabled methods and preferred method for a user.
108
+ */
109
+ async getEnabledMethods(userId) {
110
+ const user = await this.getUser(userId);
111
+ const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
112
+ const methods = Object.entries(state.channels)
113
+ .filter(([, value]) => value.enabled)
114
+ .map(([key]) => key);
115
+ if (methods.length === 0 && this._channels.has('totp') && user.mfaSecret) {
116
+ state.channels['totp'] = {
117
+ enabled: true,
118
+ secret: this._encryptionService.encrypt(user.mfaSecret),
119
+ verifiedAt: user.mfaVerifiedAt?.toISOString() ?? new Date().toISOString(),
120
+ };
121
+ state.preferredMethod = state.preferredMethod ?? 'totp';
122
+ await this._mfaSubject.updateMfaState(userId, {
123
+ mfaChannels: state,
124
+ mfaPreferredMethod: state.preferredMethod,
125
+ mfaSecret: null,
126
+ mfaUpdatedAt: new Date(),
127
+ });
128
+ methods.push('totp');
129
+ }
130
+ const preferredFromUser = user.mfaPreferredMethod;
131
+ return {
132
+ methods: methods,
133
+ preferredMethod: state.preferredMethod ?? preferredFromUser ?? methods[0],
134
+ };
135
+ }
136
+ /**
137
+ * Issues out-of-band challenge codes for login flows for applicable channels.
138
+ */
139
+ async issueChallenges(userId, methods) {
140
+ const uniqueMethods = Array.from(new Set(methods));
141
+ for (const method of uniqueMethods) {
142
+ const channel = this._channels.get(method);
143
+ if (!channel?.issueChallenge) {
144
+ continue;
145
+ }
146
+ await channel.issueChallenge(userId);
147
+ }
148
+ }
149
+ /**
150
+ * Updates the preferred MFA method.
151
+ */
152
+ async setPreferredMethod(userId, method) {
153
+ const enabled = await this.getEnabledMethods(userId);
154
+ if (!enabled.methods.includes(method)) {
155
+ throw new common_1.BadRequestException('Preferred method must be enabled');
156
+ }
157
+ const user = await this.getUser(userId);
158
+ const state = (0, MfaState_1.resolveMfaState)(user.mfaChannels);
159
+ state.preferredMethod = method;
160
+ await this._mfaSubject.updateMfaState(userId, {
161
+ mfaPreferredMethod: method,
162
+ mfaChannels: state,
163
+ mfaUpdatedAt: new Date(),
164
+ });
165
+ }
166
+ /**
167
+ * Regenerates backup codes.
168
+ */
169
+ async regenerateBackupCodes(userId) {
170
+ const user = await this.getUser(userId);
171
+ if (!user.mfaEnabled) {
172
+ throw new common_1.BadRequestException('Multi-factor authentication is not enabled');
173
+ }
174
+ const backupCodes = await this.generateBackupCodes(userId);
175
+ await this._mfaSubject.updateMfaState(userId, {
176
+ mfaBackupCodes: backupCodes.hashed,
177
+ mfaUpdatedAt: new Date(),
178
+ });
179
+ return backupCodes.plain;
180
+ }
181
+ /**
182
+ * Disables MFA for a user and clears channel secrets.
183
+ */
184
+ async disable(userId) {
185
+ await this._mfaSubject.updateMfaState(userId, {
186
+ mfaEnabled: false,
187
+ mfaPreferredMethod: null,
188
+ mfaBackupCodes: [],
189
+ mfaChannels: { channels: {}, trustedDevices: [] },
190
+ mfaSecret: null,
191
+ mfaVerifiedAt: null,
192
+ mfaUpdatedAt: new Date(),
193
+ });
194
+ }
195
+ get registeredMethods() {
196
+ return Array.from(this._channels.keys());
197
+ }
198
+ requireChannel(method) {
199
+ const channel = this._channels.get(method);
200
+ if (!channel) {
201
+ throw new common_1.BadRequestException(`Unsupported MFA method: ${method}`);
202
+ }
203
+ return channel;
204
+ }
205
+ async generateBackupCodes(userId) {
206
+ const plain = Array.from({ length: this._backupCodeCount }, () => (0, node_crypto_1.randomBytes)(5).toString('hex'));
207
+ const hashed = await Promise.all(plain.map((value) => this._bcryptService.hash(value)));
208
+ await this._mfaSubject.updateMfaState(userId, {
209
+ mfaBackupCodes: hashed,
210
+ mfaUpdatedAt: new Date(),
211
+ });
212
+ return { plain, hashed };
213
+ }
214
+ async tryConsumeBackupCode(user, code) {
215
+ const storedBackupCodes = user.mfaBackupCodes ?? [];
216
+ for (const storedBackupCode of storedBackupCodes) {
217
+ const isMatch = await this._bcryptService.compare(code, storedBackupCode);
218
+ if (isMatch) {
219
+ const remainingCodes = storedBackupCodes.filter((value) => value !== storedBackupCode);
220
+ await this._mfaSubject.updateMfaState(user.id, {
221
+ mfaBackupCodes: remainingCodes,
222
+ mfaUpdatedAt: new Date(),
223
+ });
224
+ return true;
225
+ }
226
+ }
227
+ return false;
228
+ }
229
+ async getUser(userId) {
230
+ const user = await this._mfaSubject.findById(userId);
231
+ if (!user) {
232
+ throw new common_1.BadRequestException('User not found');
233
+ }
234
+ return user;
235
+ }
236
+ async touchVerification(userId) {
237
+ await this._mfaSubject.updateMfaState(userId, {
238
+ mfaVerifiedAt: new Date(),
239
+ mfaUpdatedAt: new Date(),
240
+ });
241
+ }
242
+ };
243
+ exports.MfaService = MfaService;
244
+ exports.MfaService = MfaService = tslib_1.__decorate([
245
+ (0, common_1.Injectable)(),
246
+ tslib_1.__param(3, (0, common_1.Inject)(MfaTokens_1.MFA_CHANNELS)),
247
+ tslib_1.__metadata("design:paramtypes", [archipel_platform_core_1.BcryptService,
248
+ MfaSubjectPort_1.MfaSubjectPort,
249
+ MfaEncryptionService_1.MfaEncryptionService, Array])
250
+ ], MfaService);
251
+ //# sourceMappingURL=MfaService.js.map
@@ -0,0 +1,66 @@
1
+ import type { IDeviceInfo } from '@breadstone/archipel-platform-core';
2
+ import { MappingService } from '@breadstone/archipel-platform-core';
3
+ import { SessionPersistencePort } from '../contracts/SessionPersistencePort';
4
+ import { SessionResponse } from '../models';
5
+ /**
6
+ * Manages user sessions via the {@link SessionPersistencePort}.
7
+ *
8
+ * @public
9
+ */
10
+ export declare class SessionService {
11
+ private readonly _sessionPersistence;
12
+ private readonly _mappingService;
13
+ /**
14
+ * Constructs a new instance of the `SessionService` class.
15
+ *
16
+ * @public
17
+ * @param sessionPersistence - The session persistence port.
18
+ * @param mappingService - The mapping service.
19
+ */
20
+ constructor(sessionPersistence: SessionPersistencePort, mappingService: MappingService);
21
+ /**
22
+ * Stores or updates a session.
23
+ *
24
+ * @public
25
+ * @param token - The session token.
26
+ * @param userId - The user identifier.
27
+ * @param ipAddress - The client IP address.
28
+ * @param deviceInfo - Optional device information.
29
+ * @param location - Optional geolocation string.
30
+ * @param clientName - Optional client application name.
31
+ */
32
+ store(token: string, userId: string, ipAddress: string, deviceInfo?: IDeviceInfo, location?: string, clientName?: string): Promise<void>;
33
+ /**
34
+ * Updates the last-active timestamp for a session.
35
+ *
36
+ * @public
37
+ * @param token - The session token.
38
+ */
39
+ updateLastActive(token: string): Promise<void>;
40
+ /**
41
+ * Lists all sessions for a user, marking the current one.
42
+ *
43
+ * @public
44
+ * @param userId - The user identifier.
45
+ * @param token - The current session token.
46
+ * @returns Array of session responses.
47
+ */
48
+ listSessions(userId: string, token: string): Promise<Array<SessionResponse>>;
49
+ /**
50
+ * Invalidates a session by its record id.
51
+ *
52
+ * @public
53
+ * @param sessionId - The session record identifier.
54
+ * @param userId - The owning user identifier.
55
+ * @returns `true` if a session was invalidated.
56
+ */
57
+ invalidateById(sessionId: string, userId: string): Promise<boolean>;
58
+ /**
59
+ * Invalidates a session by its token.
60
+ *
61
+ * @public
62
+ * @param token - The session token.
63
+ * @returns `true` if a session was invalidated.
64
+ */
65
+ invalidate(token: string): Promise<boolean>;
66
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ // #region Imports
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SessionService = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const archipel_platform_core_1 = require("@breadstone/archipel-platform-core");
7
+ const common_1 = require("@nestjs/common");
8
+ const SessionPersistencePort_1 = require("../contracts/SessionPersistencePort");
9
+ const SessionMappingProfile_1 = require("../mappers/SessionMappingProfile");
10
+ // #endregion
11
+ /**
12
+ * Manages user sessions via the {@link SessionPersistencePort}.
13
+ *
14
+ * @public
15
+ */
16
+ let SessionService = class SessionService {
17
+ // #endregion
18
+ // #region Ctor
19
+ /**
20
+ * Constructs a new instance of the `SessionService` class.
21
+ *
22
+ * @public
23
+ * @param sessionPersistence - The session persistence port.
24
+ * @param mappingService - The mapping service.
25
+ */
26
+ constructor(sessionPersistence, mappingService) {
27
+ this._sessionPersistence = sessionPersistence;
28
+ this._mappingService = mappingService;
29
+ }
30
+ // #endregion
31
+ // #region Methods
32
+ /**
33
+ * Stores or updates a session.
34
+ *
35
+ * @public
36
+ * @param token - The session token.
37
+ * @param userId - The user identifier.
38
+ * @param ipAddress - The client IP address.
39
+ * @param deviceInfo - Optional device information.
40
+ * @param location - Optional geolocation string.
41
+ * @param clientName - Optional client application name.
42
+ */
43
+ async store(token, userId, ipAddress, deviceInfo, location, clientName) {
44
+ await this._sessionPersistence.store({
45
+ token,
46
+ userId,
47
+ ipAddress,
48
+ deviceInfo,
49
+ location,
50
+ clientName,
51
+ });
52
+ }
53
+ /**
54
+ * Updates the last-active timestamp for a session.
55
+ *
56
+ * @public
57
+ * @param token - The session token.
58
+ */
59
+ async updateLastActive(token) {
60
+ const session = await this._sessionPersistence.findByToken(token);
61
+ if (session) {
62
+ await this._sessionPersistence.updateLastActive(token, new Date());
63
+ }
64
+ }
65
+ /**
66
+ * Lists all sessions for a user, marking the current one.
67
+ *
68
+ * @public
69
+ * @param userId - The user identifier.
70
+ * @param token - The current session token.
71
+ * @returns Array of session responses.
72
+ */
73
+ async listSessions(userId, token) {
74
+ const result = await this._sessionPersistence.findAllByUserId(userId);
75
+ const input = result.map((x) => ({
76
+ isCurrent: x.token === token,
77
+ session: x,
78
+ }));
79
+ return this._mappingService.map(SessionMappingProfile_1.SESSION_LIST_MAPPING_KEY, input);
80
+ }
81
+ /**
82
+ * Invalidates a session by its record id.
83
+ *
84
+ * @public
85
+ * @param sessionId - The session record identifier.
86
+ * @param userId - The owning user identifier.
87
+ * @returns `true` if a session was invalidated.
88
+ */
89
+ async invalidateById(sessionId, userId) {
90
+ return this._sessionPersistence.invalidateById(sessionId, userId);
91
+ }
92
+ /**
93
+ * Invalidates a session by its token.
94
+ *
95
+ * @public
96
+ * @param token - The session token.
97
+ * @returns `true` if a session was invalidated.
98
+ */
99
+ async invalidate(token) {
100
+ return this._sessionPersistence.invalidate(token);
101
+ }
102
+ };
103
+ exports.SessionService = SessionService;
104
+ exports.SessionService = SessionService = tslib_1.__decorate([
105
+ (0, common_1.Injectable)(),
106
+ tslib_1.__metadata("design:paramtypes", [SessionPersistencePort_1.SessionPersistencePort, archipel_platform_core_1.MappingService])
107
+ ], SessionService);
108
+ //# sourceMappingURL=SessionService.js.map
@@ -0,0 +1,56 @@
1
+ import { ConfigService, HostService } from '@breadstone/archipel-platform-core';
2
+ import { JwtService } from '@nestjs/jwt';
3
+ import { VerificationSubjectPort } from '../contracts/VerificationSubjectPort';
4
+ /**
5
+ * Manages email / PIN verification tokens via the {@link VerificationSubjectPort}.
6
+ *
7
+ * @public
8
+ */
9
+ export declare class VerificationService {
10
+ private readonly _jwtService;
11
+ private readonly _hostService;
12
+ private readonly _verificationSubject;
13
+ private readonly _configService;
14
+ /**
15
+ * Constructs a new instance of the `VerificationService` class.
16
+ *
17
+ * @public
18
+ * @param jwtService - JWT service instance.
19
+ * @param hostService - Host service instance.
20
+ * @param verificationSubject - Verification subject port.
21
+ * @param configService - Configuration service instance.
22
+ */
23
+ constructor(jwtService: JwtService, hostService: HostService, verificationSubject: VerificationSubjectPort, configService: ConfigService);
24
+ /**
25
+ * Exhibits a new verification token.
26
+ *
27
+ * @public
28
+ * @param data - The data to exhibit.
29
+ * @returns The verification token.
30
+ */
31
+ exhibitToken(data: Record<string, string>): Promise<string>;
32
+ /**
33
+ * Redeems the user's token.
34
+ *
35
+ * @public
36
+ * @param token - The token to redeem.
37
+ * @returns The token data.
38
+ */
39
+ redeemToken(token: string): Promise<Record<string, string>>;
40
+ /**
41
+ * Verifies the user's token.
42
+ *
43
+ * @public
44
+ * @param token - The verification token.
45
+ */
46
+ verifyToken(token: string): Promise<boolean>;
47
+ verifyPin(pin: string, login: string): Promise<boolean>;
48
+ /**
49
+ * Constructs the verification link.
50
+ *
51
+ * @public
52
+ * @param token The verification token.
53
+ * @returns The URL to verify the email.
54
+ */
55
+ buildVerificationLink(token: string): string;
56
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ // #region Imports
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.VerificationService = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const archipel_platform_core_1 = require("@breadstone/archipel-platform-core");
7
+ const common_1 = require("@nestjs/common");
8
+ const jwt_1 = require("@nestjs/jwt");
9
+ const VerificationSubjectPort_1 = require("../contracts/VerificationSubjectPort");
10
+ // #endregion
11
+ /**
12
+ * Manages email / PIN verification tokens via the {@link VerificationSubjectPort}.
13
+ *
14
+ * @public
15
+ */
16
+ let VerificationService = class VerificationService {
17
+ // #endregion
18
+ // #region Ctor
19
+ /**
20
+ * Constructs a new instance of the `VerificationService` class.
21
+ *
22
+ * @public
23
+ * @param jwtService - JWT service instance.
24
+ * @param hostService - Host service instance.
25
+ * @param verificationSubject - Verification subject port.
26
+ * @param configService - Configuration service instance.
27
+ */
28
+ constructor(jwtService, hostService, verificationSubject, configService) {
29
+ this._jwtService = jwtService;
30
+ this._hostService = hostService;
31
+ this._verificationSubject = verificationSubject;
32
+ this._configService = configService;
33
+ }
34
+ // #endregion
35
+ // #region Methods
36
+ /**
37
+ * Exhibits a new verification token.
38
+ *
39
+ * @public
40
+ * @param data - The data to exhibit.
41
+ * @returns The verification token.
42
+ */
43
+ exhibitToken(data) {
44
+ return this._jwtService.signAsync(data, {
45
+ expiresIn: archipel_platform_core_1.DateTimeUtils.parse(this._configService.get('AUTH_VERIFY_JWT_EXPIRES_IN')),
46
+ });
47
+ }
48
+ /**
49
+ * Redeems the user's token.
50
+ *
51
+ * @public
52
+ * @param token - The token to redeem.
53
+ * @returns The token data.
54
+ */
55
+ redeemToken(token) {
56
+ return this._jwtService.verifyAsync(token);
57
+ }
58
+ /**
59
+ * Verifies the user's token.
60
+ *
61
+ * @public
62
+ * @param token - The verification token.
63
+ */
64
+ async verifyToken(token) {
65
+ if (this._jwtService.verify(token)) {
66
+ const jwt = this._jwtService.decode(token);
67
+ // check if the token is expired
68
+ if (jwt.exp && Number(jwt.exp) < Math.floor(Date.now() / 1000)) {
69
+ throw new common_1.BadRequestException('Invalid or expired token');
70
+ }
71
+ }
72
+ const auth = await this._verificationSubject.findByVerificationToken(token);
73
+ if (!auth || auth.isVerified) {
74
+ return false;
75
+ }
76
+ await this._verificationSubject.markVerified(auth.id);
77
+ return true;
78
+ }
79
+ async verifyPin(pin, login) {
80
+ const user = await this._verificationSubject.findByLogin(login);
81
+ if (!user) {
82
+ throw new common_1.BadRequestException('User not found');
83
+ }
84
+ if (user.isVerified) {
85
+ throw new common_1.BadRequestException('User already verified');
86
+ }
87
+ if (user.verificationToken) {
88
+ const jwt = await this.redeemToken(user.verificationToken);
89
+ if (jwt['email'] !== user.email) {
90
+ throw new common_1.BadRequestException('Invalid verification token');
91
+ }
92
+ if (jwt['pin'] !== pin) {
93
+ throw new common_1.BadRequestException('Invalid verification token');
94
+ }
95
+ if (jwt['exp'] && Number(jwt['exp']) < Math.floor(Date.now() / 1000)) {
96
+ throw new common_1.BadRequestException('Expired verification token');
97
+ }
98
+ if (jwt['pin'] === pin && jwt['email'] === user.email) {
99
+ await this._verificationSubject.markVerified(user.id);
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+ /**
106
+ * Constructs the verification link.
107
+ *
108
+ * @public
109
+ * @param token The verification token.
110
+ * @returns The URL to verify the email.
111
+ */
112
+ buildVerificationLink(token) {
113
+ return `${this._hostService.host}/api/auth/verify-token?token=${token}`; // get this dynamically or from config
114
+ }
115
+ };
116
+ exports.VerificationService = VerificationService;
117
+ exports.VerificationService = VerificationService = tslib_1.__decorate([
118
+ (0, common_1.Injectable)(),
119
+ tslib_1.__metadata("design:paramtypes", [jwt_1.JwtService,
120
+ archipel_platform_core_1.HostService,
121
+ VerificationSubjectPort_1.VerificationSubjectPort,
122
+ archipel_platform_core_1.ConfigService])
123
+ ], VerificationService);
124
+ //# sourceMappingURL=VerificationService.js.map
@@ -0,0 +1,5 @@
1
+ export * from './AuthTokenService';
2
+ export * from './ChallengeService';
3
+ export * from './MfaService';
4
+ export * from './SessionService';
5
+ export * from './VerificationService';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./AuthTokenService"), exports);
5
+ tslib_1.__exportStar(require("./ChallengeService"), exports);
6
+ tslib_1.__exportStar(require("./MfaService"), exports);
7
+ tslib_1.__exportStar(require("./SessionService"), exports);
8
+ tslib_1.__exportStar(require("./VerificationService"), exports);
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,40 @@
1
+ import { IMfaSetup } from '../../models';
2
+ import { IMfaSetupContext } from './MfaState';
3
+ /**
4
+ * Defines the contract each MFA channel must implement.
5
+ *
6
+ * @public
7
+ */
8
+ export interface IMfaChannel {
9
+ /**
10
+ * Unique identifier of the MFA method (e.g., `totp`, `sms`, `email`).
11
+ */
12
+ readonly method: string;
13
+ /**
14
+ * Initializes the setup flow for the channel and persists any channel-specific bootstrap state.
15
+ *
16
+ * @param userId Identifier of the user starting setup.
17
+ */
18
+ initSetup(userId: string, context?: IMfaSetupContext): Promise<IMfaSetup>;
19
+ /**
20
+ * Verifies the first setup code and marks the channel as ready for challenges.
21
+ *
22
+ * @param userId Identifier of the user finishing setup.
23
+ * @param code User supplied setup code.
24
+ */
25
+ verifySetup(userId: string, code: string): Promise<boolean>;
26
+ /**
27
+ * Verifies a challenge code during login.
28
+ *
29
+ * @param userId Identifier of the challenged user.
30
+ * @param code User supplied challenge code.
31
+ */
32
+ verifyChallenge(userId: string, code: string): Promise<boolean>;
33
+ /**
34
+ * Issues or refreshes a one-time challenge code for out-of-band channels (SMS, email, push).
35
+ * Channels that do not require pre-issued codes can implement this as a no-op.
36
+ *
37
+ * @param userId Identifier of the challenged user.
38
+ */
39
+ issueChallenge?(userId: string): Promise<void>;
40
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // #region Imports
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ //# sourceMappingURL=IMfaChannel.js.map