@open-mercato/enterprise 0.7.0 → 0.7.1-develop.7102.1.b41f7e3e51
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/security/api/interceptors.js +5 -2
- package/dist/modules/security/api/interceptors.js.map +2 -2
- package/dist/modules/security/api/mfa/_shared.js +1 -0
- package/dist/modules/security/api/mfa/_shared.js.map +2 -2
- package/dist/modules/security/data/entities.js +18 -0
- package/dist/modules/security/data/entities.js.map +2 -2
- package/dist/modules/security/index.js +3 -0
- package/dist/modules/security/index.js.map +2 -2
- package/dist/modules/security/lib/enforcement-redirect.js +28 -2
- package/dist/modules/security/lib/enforcement-redirect.js.map +2 -2
- package/dist/modules/security/lib/mfaCompletionRoutes.js +17 -0
- package/dist/modules/security/lib/mfaCompletionRoutes.js.map +7 -0
- package/dist/modules/security/lib/security-config.js +27 -0
- package/dist/modules/security/lib/security-config.js.map +2 -2
- package/dist/modules/security/migrations/Migration20260811143757_security.js +13 -0
- package/dist/modules/security/migrations/Migration20260811143757_security.js.map +7 -0
- package/dist/modules/security/services/SudoChallengeService.js +130 -41
- package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
- package/dist/modules/sso/backend/page.js +1 -0
- package/dist/modules/sso/backend/page.js.map +2 -2
- package/package.json +7 -6
- package/src/modules/security/api/interceptors.ts +6 -2
- package/src/modules/security/api/mfa/_shared.ts +1 -0
- package/src/modules/security/data/entities.ts +18 -0
- package/src/modules/security/index.ts +4 -0
- package/src/modules/security/lib/enforcement-redirect.ts +30 -2
- package/src/modules/security/lib/mfaCompletionRoutes.ts +25 -0
- package/src/modules/security/lib/security-config.ts +29 -0
- package/src/modules/security/migrations/.snapshot-open-mercato.json +848 -275
- package/src/modules/security/migrations/Migration20260811143757_security.ts +13 -0
- package/src/modules/security/services/SudoChallengeService.ts +170 -49
- package/src/modules/sso/backend/page.tsx +2 -0
|
@@ -3,6 +3,7 @@ import { z } from 'zod'
|
|
|
3
3
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
|
+
import '../../lib/mfaCompletionRoutes'
|
|
6
7
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
7
8
|
import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
8
9
|
import type { MfaService, MfaServiceError } from '../../services/MfaService'
|
|
@@ -174,6 +174,21 @@ export class SudoSession {
|
|
|
174
174
|
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
175
175
|
tenantId!: string
|
|
176
176
|
|
|
177
|
+
@Property({ name: 'scope_tenant_id', type: 'uuid', nullable: true })
|
|
178
|
+
scopeTenantId?: string | null
|
|
179
|
+
|
|
180
|
+
@Property({ name: 'scope_organization_id', type: 'uuid', nullable: true })
|
|
181
|
+
scopeOrganizationId?: string | null
|
|
182
|
+
|
|
183
|
+
@Property({ name: 'target_identifier', type: 'text', nullable: true })
|
|
184
|
+
targetIdentifier?: string | null
|
|
185
|
+
|
|
186
|
+
@Property({ name: 'sudo_config_id', type: 'uuid', nullable: true })
|
|
187
|
+
sudoConfigId?: string | null
|
|
188
|
+
|
|
189
|
+
@Property({ name: 'sudo_config_updated_at', type: Date, nullable: true })
|
|
190
|
+
sudoConfigUpdatedAt?: Date | null
|
|
191
|
+
|
|
177
192
|
@Property({ name: 'session_token', type: 'text' })
|
|
178
193
|
sessionToken!: string
|
|
179
194
|
|
|
@@ -183,6 +198,9 @@ export class SudoSession {
|
|
|
183
198
|
@Property({ name: 'expires_at', type: Date })
|
|
184
199
|
expiresAt!: Date
|
|
185
200
|
|
|
201
|
+
@Property({ name: 'verified_at', type: Date, nullable: true })
|
|
202
|
+
verifiedAt?: Date | null
|
|
203
|
+
|
|
186
204
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
187
205
|
createdAt: Date = new Date()
|
|
188
206
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
2
2
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
3
|
import { isEnforcementDeadlineOverdue } from '../services/MfaEnforcementService'
|
|
4
|
-
import { readSecurityModuleConfig } from './security-config'
|
|
4
|
+
import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from './security-config'
|
|
5
5
|
|
|
6
6
|
const MFA_ENROLLMENT_PATH = '/backend/profile/security/mfa'
|
|
7
7
|
const logger = createLogger('security').child({ component: 'mfa-enforcement-redirect' })
|
|
@@ -93,11 +93,20 @@ export async function resolveMfaEnrollmentRedirect(args: {
|
|
|
93
93
|
if (!auth || typeof auth.sub !== 'string' || auth.sub.length === 0) return null
|
|
94
94
|
if (auth.mfa_pending === true) return null
|
|
95
95
|
if (isExemptPath(pathname)) return null
|
|
96
|
-
|
|
96
|
+
|
|
97
|
+
const emergencyBypass = readSecurityModuleConfig().mfa.emergencyBypass
|
|
97
98
|
|
|
98
99
|
const enforcementService = resolveEnforcementService(container)
|
|
99
100
|
if (!enforcementService.service) {
|
|
100
101
|
if (!canFailClosed(auth)) return null
|
|
102
|
+
if (emergencyBypass) {
|
|
103
|
+
emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
|
|
104
|
+
userId: auth.sub,
|
|
105
|
+
pathname,
|
|
106
|
+
reason: 'enforcement-service-unavailable',
|
|
107
|
+
})
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
101
110
|
logger.error('Unable to resolve MFA enforcement service; redirecting to enrollment', {
|
|
102
111
|
err: enforcementService.error,
|
|
103
112
|
})
|
|
@@ -111,9 +120,28 @@ export async function resolveMfaEnrollmentRedirect(args: {
|
|
|
111
120
|
const deadlineState = resolveDeadlineRedirectState(compliance.deadline)
|
|
112
121
|
if (!deadlineState.shouldRedirect) return null
|
|
113
122
|
|
|
123
|
+
if (emergencyBypass) {
|
|
124
|
+
emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
|
|
125
|
+
userId: auth.sub,
|
|
126
|
+
pathname,
|
|
127
|
+
enforced: compliance.enforced,
|
|
128
|
+
compliant: compliance.compliant,
|
|
129
|
+
overdue: deadlineState.overdue,
|
|
130
|
+
})
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
return createEnrollmentRedirect(pathname, deadlineState.overdue)
|
|
115
135
|
} catch (error) {
|
|
116
136
|
if (canFailClosed(auth)) {
|
|
137
|
+
if (emergencyBypass) {
|
|
138
|
+
emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
|
|
139
|
+
userId: auth.sub,
|
|
140
|
+
pathname,
|
|
141
|
+
reason: 'compliance-check-failed',
|
|
142
|
+
})
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
117
145
|
logger.error('Unable to verify MFA enforcement compliance; redirecting to enrollment', { err: error })
|
|
118
146
|
return createEnrollmentRedirect(pathname)
|
|
119
147
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { registerMfaPendingAccessRoutes } from '@open-mercato/shared/lib/auth/mfaPendingAccess'
|
|
2
|
+
|
|
3
|
+
const CANONICAL_MFA_COMPLETION_ROUTES = [
|
|
4
|
+
{ path: '/api/security/mfa/prepare', methods: ['POST'] },
|
|
5
|
+
{ path: '/api/security/mfa/verify', methods: ['POST'] },
|
|
6
|
+
{ path: '/api/security/mfa/recovery', methods: ['POST'] },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
let registered = false
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register the canonical MFA challenge-completion routes with the platform-wide pending-token
|
|
13
|
+
* allowlist. The enterprise security module owns these routes, so it owns their registration:
|
|
14
|
+
* shared stays generic (SPEC-ENT-007 § Amendment 2026-08-21). Idempotent; runs once per process
|
|
15
|
+
* on first import of this module — which happens at security-module bootstrap and, as a
|
|
16
|
+
* belt-and-braces guarantee, on every load of the completion route modules themselves, so the
|
|
17
|
+
* routes are always registered before request authentication resolves.
|
|
18
|
+
*/
|
|
19
|
+
export function registerCanonicalMfaCompletionRoutes(): void {
|
|
20
|
+
if (registered) return
|
|
21
|
+
registerMfaPendingAccessRoutes(CANONICAL_MFA_COMPLETION_ROUTES)
|
|
22
|
+
registered = true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
registerCanonicalMfaCompletionRoutes()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
2
3
|
|
|
3
4
|
const TOTP_PERIOD_SECONDS = 30
|
|
4
5
|
const TOTP_DIGITS = 6
|
|
@@ -7,6 +8,9 @@ const DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000
|
|
|
7
8
|
const DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000
|
|
8
9
|
const MIN_SUDO_TTL_SECONDS = 30
|
|
9
10
|
|
|
11
|
+
const securityBypassLogger = createLogger('security').child({ component: 'mfa-emergency-bypass' })
|
|
12
|
+
let startupBypassWarningEmitted = false
|
|
13
|
+
|
|
10
14
|
export type SecurityModuleConfig = {
|
|
11
15
|
totp: {
|
|
12
16
|
issuer: string
|
|
@@ -170,6 +174,31 @@ export function resolveSecurityModuleConfigForRequest(
|
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
export function emitMfaEmergencyBypassActiveWarning(context: string, extra?: Record<string, unknown>): void {
|
|
178
|
+
securityBypassLogger.warn('MFA emergency bypass is active', {
|
|
179
|
+
emergencyBypass: true,
|
|
180
|
+
context,
|
|
181
|
+
...extra,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function emitMfaEmergencyBypassStartupWarningIfNeeded(env: NodeJS.ProcessEnv = process.env): void {
|
|
186
|
+
if (startupBypassWarningEmitted) return
|
|
187
|
+
const bypass = parseBooleanWithDefault(
|
|
188
|
+
env.OM_SECURITY_MFA_EMERGENCY_BYPASS,
|
|
189
|
+
defaultSecurityModuleConfig.mfa.emergencyBypass,
|
|
190
|
+
)
|
|
191
|
+
if (!bypass) return
|
|
192
|
+
startupBypassWarningEmitted = true
|
|
193
|
+
emitMfaEmergencyBypassActiveWarning('startup', { startup: true })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resetMfaEmergencyBypassStartupWarningForTests(): void {
|
|
197
|
+
startupBypassWarningEmitted = false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
emitMfaEmergencyBypassStartupWarningIfNeeded()
|
|
201
|
+
|
|
173
202
|
export function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {
|
|
174
203
|
const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)
|
|
175
204
|
?? readText(env.AUTH_JWT_SECRET)
|