@open-mercato/enterprise 0.6.6-develop.5612.1.d382eb2f33 → 0.6.6-develop.5619.1.29f01e2c42
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/dist/modules/security/api/i18n.js +3 -0
- package/dist/modules/security/api/i18n.js.map +2 -2
- package/dist/modules/security/api/mfa/prepare/route.js +6 -1
- package/dist/modules/security/api/mfa/prepare/route.js.map +2 -2
- package/dist/modules/security/api/mfa/verify/route.js +7 -1
- package/dist/modules/security/api/mfa/verify/route.js.map +2 -2
- package/dist/modules/security/api/sudo/prepare/route.js +6 -1
- package/dist/modules/security/api/sudo/prepare/route.js.map +2 -2
- package/dist/modules/security/api/users/mfa/compliance/route.js +4 -2
- package/dist/modules/security/api/users/mfa/compliance/route.js.map +2 -2
- package/dist/modules/security/commands/createEnforcementPolicy.js +6 -4
- package/dist/modules/security/commands/createEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/commands/deleteEnforcementPolicy.js +6 -4
- package/dist/modules/security/commands/deleteEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/commands/resetUserMfa.js +11 -4
- package/dist/modules/security/commands/resetUserMfa.js.map +2 -2
- package/dist/modules/security/commands/updateEnforcementPolicy.js +6 -4
- package/dist/modules/security/commands/updateEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/i18n/de.json +3 -0
- package/dist/modules/security/i18n/en.json +3 -0
- package/dist/modules/security/i18n/es.json +3 -0
- package/dist/modules/security/i18n/pl.json +3 -0
- package/dist/modules/security/services/MfaAdminService.js +42 -8
- package/dist/modules/security/services/MfaAdminService.js.map +2 -2
- package/dist/modules/security/services/MfaEnforcementService.js +57 -7
- package/dist/modules/security/services/MfaEnforcementService.js.map +2 -2
- package/dist/modules/security/services/MfaVerificationService.js +15 -6
- package/dist/modules/security/services/MfaVerificationService.js.map +2 -2
- package/dist/modules/security/services/SudoChallengeService.js +26 -4
- package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/security/api/i18n.ts +3 -0
- package/src/modules/security/api/mfa/prepare/route.ts +6 -1
- package/src/modules/security/api/mfa/verify/route.ts +7 -1
- package/src/modules/security/api/sudo/prepare/route.ts +6 -1
- package/src/modules/security/api/users/mfa/compliance/route.ts +6 -3
- package/src/modules/security/commands/createEnforcementPolicy.ts +7 -5
- package/src/modules/security/commands/deleteEnforcementPolicy.ts +10 -5
- package/src/modules/security/commands/resetUserMfa.ts +12 -5
- package/src/modules/security/commands/updateEnforcementPolicy.ts +10 -5
- package/src/modules/security/i18n/de.json +3 -0
- package/src/modules/security/i18n/en.json +3 -0
- package/src/modules/security/i18n/es.json +3 -0
- package/src/modules/security/i18n/pl.json +3 -0
- package/src/modules/security/services/MfaAdminService.ts +101 -8
- package/src/modules/security/services/MfaEnforcementService.ts +174 -7
- package/src/modules/security/services/MfaVerificationService.ts +58 -4
- package/src/modules/security/services/SudoChallengeService.ts +31 -6
|
@@ -14,7 +14,7 @@ class MfaAdminService {
|
|
|
14
14
|
this.em = em;
|
|
15
15
|
this.mfaEnforcementService = mfaEnforcementService;
|
|
16
16
|
}
|
|
17
|
-
async resetUserMfa(adminId, userId, reason,
|
|
17
|
+
async resetUserMfa(adminId, userId, reason, scope) {
|
|
18
18
|
if (!adminId.trim()) {
|
|
19
19
|
throw new MfaAdminServiceError("Admin ID is required", 400);
|
|
20
20
|
}
|
|
@@ -25,11 +25,11 @@ class MfaAdminService {
|
|
|
25
25
|
if (!normalizedReason) {
|
|
26
26
|
throw new MfaAdminServiceError("Reset reason is required", 400);
|
|
27
27
|
}
|
|
28
|
-
const
|
|
28
|
+
const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
|
|
29
|
+
const user = await this.loadUserForScope(userId, effectiveScope);
|
|
29
30
|
if (!user) {
|
|
30
31
|
throw new MfaAdminServiceError("User not found", 404);
|
|
31
32
|
}
|
|
32
|
-
this.assertActorOwnsUser(user, actor);
|
|
33
33
|
const activeMethods = await this.em.find(UserMfaMethod, {
|
|
34
34
|
userId,
|
|
35
35
|
isActive: true,
|
|
@@ -97,24 +97,26 @@ class MfaAdminService {
|
|
|
97
97
|
compliant: compliance.compliant
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
-
async bulkComplianceCheck(tenantId,
|
|
100
|
+
async bulkComplianceCheck(tenantId, scope) {
|
|
101
101
|
if (!tenantId.trim()) {
|
|
102
102
|
throw new MfaAdminServiceError("Tenant ID is required", 400);
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
|
|
105
|
+
const effectiveTenantId = this.resolveTenantForScope(tenantId, effectiveScope);
|
|
106
|
+
if (!effectiveTenantId) {
|
|
107
|
+
throw new MfaAdminServiceError("Tenant not found", 404);
|
|
106
108
|
}
|
|
107
109
|
const users = await findWithDecryption(
|
|
108
110
|
this.em,
|
|
109
111
|
User,
|
|
110
112
|
{
|
|
111
|
-
tenantId,
|
|
113
|
+
tenantId: effectiveTenantId,
|
|
112
114
|
deletedAt: null
|
|
113
115
|
},
|
|
114
116
|
{
|
|
115
117
|
orderBy: { createdAt: "asc" }
|
|
116
118
|
},
|
|
117
|
-
{ tenantId, organizationId: null }
|
|
119
|
+
{ tenantId: effectiveTenantId, organizationId: null }
|
|
118
120
|
);
|
|
119
121
|
const userIds = users.map((user) => user.id);
|
|
120
122
|
const activeMethods = userIds.length ? await this.em.find(UserMfaMethod, {
|
|
@@ -158,6 +160,38 @@ class MfaAdminService {
|
|
|
158
160
|
{ tenantId: null, organizationId: null }
|
|
159
161
|
);
|
|
160
162
|
}
|
|
163
|
+
resolveTenantForScope(requestedTenantId, scope) {
|
|
164
|
+
if (scope.isSuperAdmin) {
|
|
165
|
+
return requestedTenantId;
|
|
166
|
+
}
|
|
167
|
+
if (!scope.tenantId) return null;
|
|
168
|
+
if (scope.tenantId !== requestedTenantId) return null;
|
|
169
|
+
return scope.tenantId;
|
|
170
|
+
}
|
|
171
|
+
async loadUserForScope(userId, scope) {
|
|
172
|
+
if (scope.isSuperAdmin) {
|
|
173
|
+
return findOneWithDecryption(
|
|
174
|
+
this.em,
|
|
175
|
+
User,
|
|
176
|
+
{ id: userId, deletedAt: null },
|
|
177
|
+
void 0,
|
|
178
|
+
{ tenantId: null, organizationId: null }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!scope.tenantId) return null;
|
|
182
|
+
const user = await findOneWithDecryption(
|
|
183
|
+
this.em,
|
|
184
|
+
User,
|
|
185
|
+
{ id: userId, tenantId: scope.tenantId, deletedAt: null },
|
|
186
|
+
void 0,
|
|
187
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }
|
|
188
|
+
);
|
|
189
|
+
if (!user) return null;
|
|
190
|
+
if (scope.organizationId !== void 0 && scope.organizationId !== null && user.organizationId !== null && user.organizationId !== scope.organizationId) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return user;
|
|
194
|
+
}
|
|
161
195
|
}
|
|
162
196
|
var MfaAdminService_default = MfaAdminService;
|
|
163
197
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/services/MfaAdminService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { MfaRecoveryCode, UserMfaMethod } from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\n\ntype MfaMethodStatus = {\n type: string\n label?: string\n lastUsed?: Date\n}\n\ntype UserMfaStatus = {\n enrolled: boolean\n methods: MfaMethodStatus[]\n recoveryCodesRemaining: number\n compliant: boolean\n}\n\ntype BulkComplianceStatus = {\n userId: string\n email: string\n enrolled: boolean\n methodCount: number\n compliant: boolean\n lastLoginAt?: Date\n}\n\ntype ActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\nexport class MfaAdminServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaAdminServiceError'\n }\n}\n\nexport class MfaAdminService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaEnforcementService: MfaEnforcementService,\n ) {}\n\n async resetUserMfa(adminId: string, userId: string, reason: string
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { MfaRecoveryCode, UserMfaMethod } from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\n\nexport type MfaAdminAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype MfaMethodStatus = {\n type: string\n label?: string\n lastUsed?: Date\n}\n\ntype UserMfaStatus = {\n enrolled: boolean\n methods: MfaMethodStatus[]\n recoveryCodesRemaining: number\n compliant: boolean\n}\n\ntype BulkComplianceStatus = {\n userId: string\n email: string\n enrolled: boolean\n methodCount: number\n compliant: boolean\n lastLoginAt?: Date\n}\n\ntype ActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\nexport class MfaAdminServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaAdminServiceError'\n }\n}\n\nexport class MfaAdminService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaEnforcementService: MfaEnforcementService,\n ) {}\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaAdminAuthScope} so the target user is\n * loaded with tenant/organization scoping. The no-scope overload now treats the\n * caller as a non-superadmin with unknown tenant and rejects every load with 404;\n * it will be removed in a future release.\n */\n async resetUserMfa(adminId: string, userId: string, reason: string): Promise<void>\n async resetUserMfa(\n adminId: string,\n userId: string,\n reason: string,\n scope: MfaAdminAuthScope,\n ): Promise<void>\n async resetUserMfa(\n adminId: string,\n userId: string,\n reason: string,\n scope?: MfaAdminAuthScope,\n ): Promise<void> {\n if (!adminId.trim()) {\n throw new MfaAdminServiceError('Admin ID is required', 400)\n }\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const normalizedReason = reason.trim()\n if (!normalizedReason) {\n throw new MfaAdminServiceError('Reset reason is required', 400)\n }\n\n const effectiveScope: MfaAdminAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n const user = await this.loadUserForScope(userId, effectiveScope)\n if (!user) {\n // Unified 404 for both \"missing\" and \"out of scope\" \u2014 prevents existence enumeration.\n throw new MfaAdminServiceError('User not found', 404)\n }\n\n const activeMethods = await this.em.find(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n })\n const activeRecoveryCodes = await this.em.find(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const now = new Date()\n for (const method of activeMethods) {\n method.isActive = false\n method.deletedAt = now\n method.updatedAt = now\n }\n for (const recoveryCode of activeRecoveryCodes) {\n recoveryCode.isUsed = true\n recoveryCode.usedAt = now\n }\n await this.em.flush()\n\n await emitSecurityEvent('security.mfa.reset', {\n adminId,\n targetUserId: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n reason: normalizedReason,\n methodCount: activeMethods.length,\n recoveryCodesInvalidated: activeRecoveryCodes.length,\n resetAt: new Date().toISOString(),\n })\n }\n\n async getUserMfaStatus(userId: string, actor?: ActorContext): Promise<UserMfaStatus> {\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n this.assertActorOwnsUser(user, actor)\n\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'desc' },\n },\n )\n\n const recoveryCodesRemaining = await this.em.count(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const compliance = await this.mfaEnforcementService.checkUserCompliance(userId)\n\n return {\n enrolled: methods.length > 0,\n methods: methods.map((method) => ({\n type: method.type,\n ...(method.label ? { label: method.label } : {}),\n ...(method.lastUsedAt ? { lastUsed: method.lastUsedAt } : {}),\n })),\n recoveryCodesRemaining,\n compliant: compliance.compliant,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaAdminAuthScope} so the tenant list is\n * enforced against the caller's actual scope rather than the caller-supplied\n * tenantId. The no-scope overload now treats the caller as a non-superadmin\n * with unknown tenant and rejects every request with 404; it will be removed\n * in a future release.\n */\n async bulkComplianceCheck(tenantId: string): Promise<BulkComplianceStatus[]>\n async bulkComplianceCheck(\n tenantId: string,\n scope: MfaAdminAuthScope,\n ): Promise<BulkComplianceStatus[]>\n async bulkComplianceCheck(\n tenantId: string,\n scope?: MfaAdminAuthScope,\n ): Promise<BulkComplianceStatus[]> {\n if (!tenantId.trim()) {\n throw new MfaAdminServiceError('Tenant ID is required', 400)\n }\n\n const effectiveScope: MfaAdminAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n const effectiveTenantId = this.resolveTenantForScope(tenantId, effectiveScope)\n if (!effectiveTenantId) {\n // Unified 404 for missing-scope, cross-tenant mismatch, and unknown tenant \u2014\n // prevents existence enumeration and matches the resetUserMfa contract.\n throw new MfaAdminServiceError('Tenant not found', 404)\n }\n\n const users = await findWithDecryption(\n this.em,\n User,\n {\n tenantId: effectiveTenantId,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n { tenantId: effectiveTenantId, organizationId: null },\n )\n\n const userIds = users.map((user) => user.id)\n const activeMethods = userIds.length\n ? await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n })\n : []\n const methodCountByUserId = new Map<string, number>()\n for (const method of activeMethods) {\n const currentCount = methodCountByUserId.get(method.userId) ?? 0\n methodCountByUserId.set(method.userId, currentCount + 1)\n }\n\n const complianceResults = await Promise.all(\n users.map((user) => this.mfaEnforcementService.checkUserCompliance(user.id)),\n )\n\n return users.map((user, index) => {\n const methodCount = methodCountByUserId.get(user.id) ?? 0\n const compliance = complianceResults[index]\n return {\n userId: user.id,\n email: user.email,\n enrolled: methodCount > 0,\n methodCount,\n compliant: compliance.compliant,\n ...(user.lastLoginAt ? { lastLoginAt: user.lastLoginAt } : {}),\n }\n })\n }\n\n private assertActorOwnsUser(user: User, actor?: ActorContext): void {\n if (!actor || actor.isSuperAdmin) return\n if (!user.tenantId || user.tenantId !== actor.tenantId) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n }\n\n private resolveTenantForScope(\n requestedTenantId: string,\n scope: MfaAdminAuthScope,\n ): string | null {\n if (scope.isSuperAdmin) {\n return requestedTenantId\n }\n if (!scope.tenantId) return null\n if (scope.tenantId !== requestedTenantId) return null\n return scope.tenantId\n }\n\n private async loadUserForScope(\n userId: string,\n scope: MfaAdminAuthScope,\n ): Promise<User | null> {\n if (scope.isSuperAdmin) {\n return findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n { tenantId: null, organizationId: null },\n )\n }\n\n if (!scope.tenantId) return null\n\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, tenantId: scope.tenantId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null },\n )\n if (!user) return null\n\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && user.organizationId !== null\n && user.organizationId !== scope.organizationId\n ) {\n return null\n }\n return user\n }\n}\n\nexport default MfaAdminService\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -81,9 +81,10 @@ class MfaEnforcementService {
|
|
|
81
81
|
overdue
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
-
async createPolicy(data, adminId,
|
|
84
|
+
async createPolicy(data, adminId, scope) {
|
|
85
85
|
const normalized = this.normalizePolicyInput(data);
|
|
86
|
-
|
|
86
|
+
const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
|
|
87
|
+
this.assertCreatePolicyScope(normalized, effectiveScope);
|
|
87
88
|
const existing = await this.findPolicyByScope(
|
|
88
89
|
normalized.scope,
|
|
89
90
|
normalized.tenantId ?? void 0,
|
|
@@ -126,7 +127,8 @@ class MfaEnforcementService {
|
|
|
126
127
|
await this.emitDeadlineReminderRequest(policy.id);
|
|
127
128
|
return policy;
|
|
128
129
|
}
|
|
129
|
-
async updatePolicy(id, data, adminId,
|
|
130
|
+
async updatePolicy(id, data, adminId, scope) {
|
|
131
|
+
const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
|
|
130
132
|
const policy = await this.em.findOne(MfaEnforcementPolicy, {
|
|
131
133
|
id,
|
|
132
134
|
deletedAt: null
|
|
@@ -134,7 +136,6 @@ class MfaEnforcementService {
|
|
|
134
136
|
if (!policy) {
|
|
135
137
|
throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
|
|
136
138
|
}
|
|
137
|
-
this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null);
|
|
138
139
|
const mergedInput = this.normalizePolicyInput({
|
|
139
140
|
scope: data.scope ?? policy.scope,
|
|
140
141
|
tenantId: data.tenantId ?? policy.tenantId ?? void 0,
|
|
@@ -143,7 +144,7 @@ class MfaEnforcementService {
|
|
|
143
144
|
allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,
|
|
144
145
|
enforcementDeadline: data.enforcementDeadline === void 0 ? policy.enforcementDeadline ?? null : data.enforcementDeadline
|
|
145
146
|
});
|
|
146
|
-
this.
|
|
147
|
+
this.assertUpdatePolicyScope(policy, mergedInput, effectiveScope);
|
|
147
148
|
if (mergedInput.scope !== policy.scope || mergedInput.tenantId !== (policy.tenantId ?? null) || mergedInput.organizationId !== (policy.organizationId ?? null)) {
|
|
148
149
|
const conflict = await this.findPolicyByScope(
|
|
149
150
|
mergedInput.scope,
|
|
@@ -171,7 +172,8 @@ class MfaEnforcementService {
|
|
|
171
172
|
await this.emitDeadlineReminderRequest(policy.id);
|
|
172
173
|
return policy;
|
|
173
174
|
}
|
|
174
|
-
async deletePolicy(id,
|
|
175
|
+
async deletePolicy(id, scope) {
|
|
176
|
+
const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
|
|
175
177
|
const policy = await this.em.findOne(MfaEnforcementPolicy, {
|
|
176
178
|
id,
|
|
177
179
|
deletedAt: null
|
|
@@ -179,7 +181,7 @@ class MfaEnforcementService {
|
|
|
179
181
|
if (!policy) {
|
|
180
182
|
throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
|
|
181
183
|
}
|
|
182
|
-
this.
|
|
184
|
+
this.assertDeletePolicyScope(policy, effectiveScope);
|
|
183
185
|
const now = /* @__PURE__ */ new Date();
|
|
184
186
|
policy.deletedAt = now;
|
|
185
187
|
policy.updatedAt = now;
|
|
@@ -223,6 +225,54 @@ class MfaEnforcementService {
|
|
|
223
225
|
if (tenantPolicy) return tenantPolicy;
|
|
224
226
|
return this.findPolicyByScope(EnforcementScope.PLATFORM, void 0, void 0);
|
|
225
227
|
}
|
|
228
|
+
assertCreatePolicyScope(normalized, scope) {
|
|
229
|
+
if (scope.isSuperAdmin) return;
|
|
230
|
+
if (!scope.tenantId) {
|
|
231
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
232
|
+
}
|
|
233
|
+
if (normalized.scope === EnforcementScope.PLATFORM) {
|
|
234
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
235
|
+
}
|
|
236
|
+
if (normalized.tenantId !== scope.tenantId) {
|
|
237
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
238
|
+
}
|
|
239
|
+
if (normalized.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && normalized.organizationId !== scope.organizationId) {
|
|
240
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
assertUpdatePolicyScope(existing, proposed, scope) {
|
|
244
|
+
if (scope.isSuperAdmin) return;
|
|
245
|
+
if (!scope.tenantId) {
|
|
246
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
247
|
+
}
|
|
248
|
+
if (existing.scope === EnforcementScope.PLATFORM || existing.tenantId !== scope.tenantId) {
|
|
249
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
250
|
+
}
|
|
251
|
+
if (proposed.scope === EnforcementScope.PLATFORM) {
|
|
252
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
253
|
+
}
|
|
254
|
+
if (proposed.tenantId !== scope.tenantId) {
|
|
255
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
256
|
+
}
|
|
257
|
+
if (proposed.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && proposed.organizationId !== scope.organizationId) {
|
|
258
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
259
|
+
}
|
|
260
|
+
if (existing.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && existing.organizationId !== scope.organizationId) {
|
|
261
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
assertDeletePolicyScope(existing, scope) {
|
|
265
|
+
if (scope.isSuperAdmin) return;
|
|
266
|
+
if (!scope.tenantId) {
|
|
267
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
268
|
+
}
|
|
269
|
+
if (existing.scope === EnforcementScope.PLATFORM || existing.tenantId !== scope.tenantId) {
|
|
270
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
271
|
+
}
|
|
272
|
+
if (existing.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && existing.organizationId !== scope.organizationId) {
|
|
273
|
+
throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
226
276
|
async findPolicyByScope(scope, tenantId, organizationId) {
|
|
227
277
|
return this.em.findOne(
|
|
228
278
|
MfaEnforcementPolicy,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/services/MfaEnforcementService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\nexport type EnforcementActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async getPolicyById(id: string): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null })\n }\n\n async listPolicies(\n filters?: EnforcementPolicyListFilters,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy[]> {\n const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {}\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n ...tenantConstraint,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n actor?: EnforcementActorContext,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n this.assertActorOwnsScopeFilters(actor, scope, tenantId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n this.assertActorOwnsScopeFilters(actor, normalized.scope, normalized.tenantId)\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n this.assertActorOwnsScopeFilters(actor, mergedInput.scope, mergedInput.tenantId)\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async deletePolicy(id: string, actor?: EnforcementActorContext): Promise<void> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private assertActorOwnsScopeFilters(\n actor: EnforcementActorContext | undefined,\n scope: EnforcementScope,\n tenantId: string | null | undefined,\n ): void {\n if (!actor || actor.isSuperAdmin) return\n if (scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError(\n 'Platform scope requires platform administrator privileges.',\n 403,\n )\n }\n if (!tenantId || tenantId !== actor.tenantId) {\n throw new MfaEnforcementServiceError('Not authorized for the requested scope.', 403)\n }\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKP,SAAS,yBAAyB;AA6B3B,SAAS,6BACd,UACA,MAAM,KAAK,IAAI,GACN;AACT,MAAI,EAAE,oBAAoB,MAAO,QAAO;AACxC,QAAM,eAAe,SAAS,QAAQ;AACtC,MAAI,OAAO,MAAM,YAAY,EAAG,QAAO;AACvC,SAAO,gBAAgB;AACzB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\nexport type EnforcementActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport type MfaEnforcementAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async getPolicyById(id: string): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null })\n }\n\n async listPolicies(\n filters?: EnforcementPolicyListFilters,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy[]> {\n const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {}\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n ...tenantConstraint,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n actor?: EnforcementActorContext,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n this.assertActorOwnsScopeFilters(actor, scope, tenantId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaEnforcementAuthScope} so the policy\n * write is enforced against the caller's actual tenant/organization scope\n * rather than caller-supplied body fields. The no-scope overload now treats\n * the caller as a non-superadmin with unknown tenant and rejects every write\n * with 403; it will be removed in a future release.\n */\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy>\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n scope: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy>\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n scope?: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n const effectiveScope: MfaEnforcementAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n this.assertCreatePolicyScope(normalized, effectiveScope)\n\n\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaEnforcementAuthScope} so the policy\n * update is enforced against the caller's actual tenant/organization scope\n * rather than caller-supplied body fields. The no-scope overload now treats\n * the caller as a non-superadmin with unknown tenant and rejects every write\n * with 403; it will be removed in a future release.\n */\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy>\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n scope: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy>\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n scope?: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy> {\n const effectiveScope: MfaEnforcementAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n this.assertUpdatePolicyScope(policy, mergedInput, effectiveScope)\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaEnforcementAuthScope} so the policy\n * delete is enforced against the caller's actual tenant/organization scope\n * rather than the caller-supplied id alone. The no-scope overload now treats\n * the caller as a non-superadmin with unknown tenant and rejects every delete\n * with 403; it will be removed in a future release.\n */\n async deletePolicy(id: string): Promise<void>\n async deletePolicy(id: string, scope: MfaEnforcementAuthScope): Promise<void>\n async deletePolicy(id: string, scope?: MfaEnforcementAuthScope): Promise<void> {\n const effectiveScope: MfaEnforcementAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n this.assertDeletePolicyScope(policy, effectiveScope)\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private assertCreatePolicyScope(\n normalized: {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n },\n scope: MfaEnforcementAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n\n // Fail closed when the caller has no concrete tenant context. Mirrors the\n // MfaAdminService.loadUserForScope precedent \u2014 a missing scope.tenantId for\n // a non-superadmin means we cannot prove ownership of anything.\n if (!scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (normalized.scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (normalized.tenantId !== scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n normalized.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && normalized.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n }\n\n private assertUpdatePolicyScope(\n existing: MfaEnforcementPolicy,\n proposed: {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n },\n scope: MfaEnforcementAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n\n if (!scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.PLATFORM\n || existing.tenantId !== scope.tenantId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (proposed.scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (proposed.tenantId !== scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n proposed.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && proposed.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && existing.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n }\n\n private assertDeletePolicyScope(\n existing: MfaEnforcementPolicy,\n scope: MfaEnforcementAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n\n if (!scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.PLATFORM\n || existing.tenantId !== scope.tenantId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && existing.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private assertActorOwnsScopeFilters(\n actor: EnforcementActorContext | undefined,\n scope: EnforcementScope,\n tenantId: string | null | undefined,\n ): void {\n if (!actor || actor.isSuperAdmin) return\n if (scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError(\n 'Platform scope requires platform administrator privileges.',\n 403,\n )\n }\n if (!tenantId || tenantId !== actor.tenantId) {\n throw new MfaEnforcementServiceError('Not authorized for the requested scope.', 403)\n }\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -48,8 +48,11 @@ class MfaVerificationService {
|
|
|
48
48
|
availableMethods
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
async prepareChallenge(challengeId, methodType, context) {
|
|
52
|
-
|
|
51
|
+
async prepareChallenge(challengeId, methodType, context, scope) {
|
|
52
|
+
if (!scope) {
|
|
53
|
+
throw new MfaVerificationServiceError("MFA challenge not found", 404);
|
|
54
|
+
}
|
|
55
|
+
const challenge = await this.getValidChallenge(challengeId, scope);
|
|
53
56
|
await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
|
|
54
57
|
const provider = this.mfaProviderRegistry.get(methodType);
|
|
55
58
|
if (!provider) {
|
|
@@ -69,8 +72,11 @@ class MfaVerificationService {
|
|
|
69
72
|
await this.em.flush();
|
|
70
73
|
return result;
|
|
71
74
|
}
|
|
72
|
-
async verifyChallenge(challengeId, methodType, payload, runtimeContext) {
|
|
73
|
-
|
|
75
|
+
async verifyChallenge(challengeId, methodType, payload, runtimeContext, scope) {
|
|
76
|
+
if (!scope) {
|
|
77
|
+
throw new MfaVerificationServiceError("MFA challenge not found", 404);
|
|
78
|
+
}
|
|
79
|
+
const challenge = await this.getValidChallenge(challengeId, scope);
|
|
74
80
|
if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
|
|
75
81
|
return false;
|
|
76
82
|
}
|
|
@@ -110,8 +116,11 @@ class MfaVerificationService {
|
|
|
110
116
|
async verifyRecoveryCode(userId, code) {
|
|
111
117
|
return this.mfaService.verifyRecoveryCode(userId, code);
|
|
112
118
|
}
|
|
113
|
-
async getValidChallenge(challengeId) {
|
|
114
|
-
const challenge = await this.em.findOne(MfaChallenge, {
|
|
119
|
+
async getValidChallenge(challengeId, scope) {
|
|
120
|
+
const challenge = await this.em.findOne(MfaChallenge, {
|
|
121
|
+
id: challengeId,
|
|
122
|
+
userId: scope.userId
|
|
123
|
+
});
|
|
115
124
|
if (!challenge) {
|
|
116
125
|
throw new MfaVerificationServiceError("MFA challenge not found", 404);
|
|
117
126
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/services/MfaVerificationService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { MfaChallenge, UserMfaMethod } from '../data/entities'\nimport type { MfaProviderRegistry } from '../lib/mfa-provider-registry'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaService } from './MfaService'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\nimport type { MfaProviderRuntimeContext, MfaVerifyContext } from '../lib/mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype AvailableMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\ntype ChallengeCreationResult = {\n challengeId: string\n availableMethods: AvailableMethod[]\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const challenge = await this.getValidChallenge(challengeId)\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean> {\n const challenge = await this.getValidChallenge(challengeId)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(challengeId: string): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, { id: challengeId })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return\n }\n if (!policy.allowedMethods.includes(methodType)) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)\n }\n }\n\n private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {\n const maxAttempts = this.securityConfig.mfa.maxAttempts\n const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(\n 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',\n [challenge.id, maxAttempts],\n )\n const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts\n challenge.attempts = updatedAttempts\n if (updatedAttempts >= maxAttempts) {\n const now = new Date()\n await this.em.getConnection().execute(\n 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',\n [now, challenge.id],\n )\n challenge.expiresAt = now\n }\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,cAAc,qBAAqB;AAE5C,SAAS,yBAAyB;AAKlC,SAAS,gCAAgC;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { MfaChallenge, UserMfaMethod } from '../data/entities'\nimport type { MfaProviderRegistry } from '../lib/mfa-provider-registry'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaService } from './MfaService'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\nimport type { MfaProviderRuntimeContext, MfaVerifyContext } from '../lib/mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype AvailableMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\ntype ChallengeCreationResult = {\n challengeId: string\n availableMethods: AvailableMethod[]\n}\n\nexport type MfaVerificationAuthScope = {\n userId: string\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<boolean> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(\n challengeId: string,\n scope: MfaVerificationAuthScope,\n ): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, {\n id: challengeId,\n userId: scope.userId,\n })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return\n }\n if (!policy.allowedMethods.includes(methodType)) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)\n }\n }\n\n private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {\n const maxAttempts = this.securityConfig.mfa.maxAttempts\n const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(\n 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',\n [challenge.id, maxAttempts],\n )\n const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts\n challenge.attempts = updatedAttempts\n if (updatedAttempts >= maxAttempts) {\n const now = new Date()\n await this.em.getConnection().execute(\n 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',\n [now, challenge.id],\n )\n challenge.expiresAt = now\n }\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -23,6 +23,10 @@ const signedSudoTokenPayloadSchema = z.object({
|
|
|
23
23
|
tgt: z.string(),
|
|
24
24
|
exp: z.number()
|
|
25
25
|
});
|
|
26
|
+
function readTrimmedEnv(value) {
|
|
27
|
+
const trimmed = value?.trim();
|
|
28
|
+
return trimmed ? trimmed : void 0;
|
|
29
|
+
}
|
|
26
30
|
class SudoChallengeServiceError extends Error {
|
|
27
31
|
constructor(message, statusCode) {
|
|
28
32
|
super(message);
|
|
@@ -128,12 +132,20 @@ class SudoChallengeService {
|
|
|
128
132
|
expiresAt
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
|
-
async prepare(sessionId, methodType, request) {
|
|
135
|
+
async prepare(sessionId, methodType, options = {}, request) {
|
|
132
136
|
const session = await this.getPendingSession(sessionId);
|
|
137
|
+
if (options.expectedUserId && session.userId !== options.expectedUserId) {
|
|
138
|
+
throw new SudoChallengeServiceError("Sudo challenge user mismatch", 403);
|
|
139
|
+
}
|
|
133
140
|
if (session.challengeMethod !== "mfa") {
|
|
134
141
|
throw new SudoChallengeServiceError("This sudo session does not require MFA", 400);
|
|
135
142
|
}
|
|
136
|
-
return this.mfaVerificationService.prepareChallenge(
|
|
143
|
+
return this.mfaVerificationService.prepareChallenge(
|
|
144
|
+
session.sessionToken,
|
|
145
|
+
methodType,
|
|
146
|
+
{ request },
|
|
147
|
+
{ userId: session.userId }
|
|
148
|
+
);
|
|
137
149
|
}
|
|
138
150
|
async verify(sessionId, methodType, payload, options, request) {
|
|
139
151
|
const session = await this.getPendingSession(sessionId);
|
|
@@ -161,7 +173,13 @@ class SudoChallengeService {
|
|
|
161
173
|
verified = await this.passwordService.verifyPassword(session.userId, password);
|
|
162
174
|
methodUsed = SudoChallengeMethodUsed.PASSWORD;
|
|
163
175
|
} else {
|
|
164
|
-
verified = await this.mfaVerificationService.verifyChallenge(
|
|
176
|
+
verified = await this.mfaVerificationService.verifyChallenge(
|
|
177
|
+
session.sessionToken,
|
|
178
|
+
methodType,
|
|
179
|
+
payload,
|
|
180
|
+
{ request },
|
|
181
|
+
{ userId: session.userId }
|
|
182
|
+
);
|
|
165
183
|
}
|
|
166
184
|
if (!verified) {
|
|
167
185
|
await emitSecurityEvent("security.sudo.failed", {
|
|
@@ -491,7 +509,11 @@ class SudoChallengeService {
|
|
|
491
509
|
}
|
|
492
510
|
}
|
|
493
511
|
getSudoSecret() {
|
|
494
|
-
|
|
512
|
+
const secret = readTrimmedEnv(process.env.OM_SECURITY_SUDO_SECRET) ?? readTrimmedEnv(process.env.AUTH_JWT_SECRET) ?? readTrimmedEnv(process.env.AUTH_SECRET) ?? readTrimmedEnv(process.env.JWT_SECRET);
|
|
513
|
+
if (secret) return secret;
|
|
514
|
+
throw new Error(
|
|
515
|
+
"Sudo step-up tokens require OM_SECURITY_SUDO_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET to be set."
|
|
516
|
+
);
|
|
495
517
|
}
|
|
496
518
|
toChallengeMethod(method) {
|
|
497
519
|
switch (method) {
|