@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,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,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
|
+
}
|