@open-mercato/enterprise 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0
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/record_locks/backend/settings/record-locks/page.js +2 -2
- package/dist/modules/record_locks/backend/settings/record-locks/page.js.map +2 -2
- package/dist/modules/security/api/interceptors.js +2 -5
- package/dist/modules/security/api/interceptors.js.map +2 -2
- package/dist/modules/security/api/mfa/_shared.js +0 -1
- package/dist/modules/security/api/mfa/_shared.js.map +2 -2
- package/dist/modules/security/data/entities.js +0 -18
- package/dist/modules/security/data/entities.js.map +2 -2
- package/dist/modules/security/index.js +0 -3
- package/dist/modules/security/index.js.map +2 -2
- package/dist/modules/security/lib/enforcement-redirect.js +2 -28
- package/dist/modules/security/lib/enforcement-redirect.js.map +2 -2
- package/dist/modules/security/lib/security-config.js +0 -27
- package/dist/modules/security/lib/security-config.js.map +2 -2
- package/dist/modules/security/services/SudoChallengeService.js +41 -130
- package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
- package/dist/modules/sso/backend/page.js +0 -1
- package/dist/modules/sso/backend/page.js.map +2 -2
- package/package.json +6 -7
- package/src/modules/record_locks/backend/settings/record-locks/page.tsx +4 -4
- package/src/modules/security/api/interceptors.ts +2 -6
- package/src/modules/security/api/mfa/_shared.ts +0 -1
- package/src/modules/security/data/entities.ts +0 -18
- package/src/modules/security/index.ts +0 -4
- package/src/modules/security/lib/enforcement-redirect.ts +2 -30
- package/src/modules/security/lib/security-config.ts +0 -29
- package/src/modules/security/migrations/.snapshot-open-mercato.json +276 -849
- package/src/modules/security/services/SudoChallengeService.ts +49 -170
- package/src/modules/sso/backend/page.tsx +0 -2
- package/dist/modules/security/lib/mfaCompletionRoutes.js +0 -17
- package/dist/modules/security/lib/mfaCompletionRoutes.js.map +0 -7
- package/dist/modules/security/migrations/Migration20260811143757_security.js +0 -13
- package/dist/modules/security/migrations/Migration20260811143757_security.js.map +0 -7
- package/src/modules/security/lib/mfaCompletionRoutes.ts +0 -25
- package/src/modules/security/migrations/Migration20260811143757_security.ts +0 -13
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/lib/security-config.ts"],
|
|
4
|
-
"sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst TOTP_PERIOD_SECONDS = 30\nconst TOTP_DIGITS = 6\nconst DEFAULT_SETUP_TTL_MS = 10 * 60 * 1000\nconst DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000\nconst DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000\nconst MIN_SUDO_TTL_SECONDS = 30\n\nconst securityBypassLogger = createLogger('security').child({ component: 'mfa-emergency-bypass' })\nlet startupBypassWarningEmitted = false\n\nexport type SecurityModuleConfig = {\n totp: {\n issuer: string\n window: number\n digits: number\n periodSeconds: number\n setupTtlMs: number\n }\n otpEmail: {\n expirySeconds: number\n maxAttempts: number\n setupTtlMs: number\n challengeTtlMs: number\n subject: string\n }\n mfa: {\n challengeTtlMs: number\n maxAttempts: number\n emergencyBypass: boolean\n }\n sudo: {\n minTtlSeconds: number\n defaultTtlSeconds: number\n maxTtlSeconds: number\n pendingChallengeTtlMs: number\n }\n webauthn: {\n rpName: string\n rpId: string\n expectedOrigins: string[]\n setupTtlMs: number\n challengeTtlMs: number\n }\n recoveryCodes: {\n count: number\n bcryptCost: number\n }\n}\n\nexport const defaultSecurityModuleConfig: SecurityModuleConfig = {\n totp: {\n issuer: 'Open Mercato',\n window: 1,\n digits: TOTP_DIGITS,\n periodSeconds: TOTP_PERIOD_SECONDS,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n },\n otpEmail: {\n expirySeconds: 600,\n maxAttempts: 5,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: 600 * 1000,\n subject: 'Your Open Mercato verification code',\n },\n mfa: {\n challengeTtlMs: 600 * 1000,\n maxAttempts: 5,\n emergencyBypass: false,\n },\n sudo: {\n minTtlSeconds: MIN_SUDO_TTL_SECONDS,\n defaultTtlSeconds: 300,\n maxTtlSeconds: 1800,\n pendingChallengeTtlMs: DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS,\n },\n webauthn: {\n rpName: 'Open Mercato',\n rpId: 'localhost',\n expectedOrigins: ['http://localhost:3000'],\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS,\n },\n recoveryCodes: {\n count: 10,\n bcryptCost: 10,\n },\n}\n\nexport function readSecurityModuleConfig(\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const otpExpirySeconds = parsePositiveInteger(env.OM_SECURITY_OTP_EXPIRY_SECONDS)\n ?? defaultSecurityModuleConfig.otpEmail.expirySeconds\n const otpMaxAttempts = parsePositiveInteger(env.OM_SECURITY_OTP_MAX_ATTEMPTS)\n ?? defaultSecurityModuleConfig.otpEmail.maxAttempts\n const sudoMaxTtlSeconds = Math.max(\n parsePositiveInteger(env.OM_SECURITY_SUDO_MAX_TTL) ?? defaultSecurityModuleConfig.sudo.maxTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n )\n const sudoDefaultTtlSeconds = clamp(\n parsePositiveInteger(env.OM_SECURITY_SUDO_DEFAULT_TTL) ?? defaultSecurityModuleConfig.sudo.defaultTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n sudoMaxTtlSeconds,\n )\n const webauthnRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n ?? readHostname(env.APP_URL)\n ?? readHostname(env.NEXT_PUBLIC_APP_URL)\n ?? defaultSecurityModuleConfig.webauthn.rpId\n\n return {\n totp: {\n ...defaultSecurityModuleConfig.totp,\n issuer: readText(env.OM_SECURITY_TOTP_ISSUER) ?? defaultSecurityModuleConfig.totp.issuer,\n window: parseNonNegativeInteger(env.OM_SECURITY_TOTP_WINDOW) ?? defaultSecurityModuleConfig.totp.window,\n },\n otpEmail: {\n ...defaultSecurityModuleConfig.otpEmail,\n expirySeconds: otpExpirySeconds,\n maxAttempts: otpMaxAttempts,\n challengeTtlMs: otpExpirySeconds * 1000,\n },\n mfa: {\n challengeTtlMs: otpExpirySeconds * 1000,\n maxAttempts: otpMaxAttempts,\n emergencyBypass: parseBooleanWithDefault(\n env.OM_SECURITY_MFA_EMERGENCY_BYPASS,\n defaultSecurityModuleConfig.mfa.emergencyBypass,\n ),\n },\n sudo: {\n ...defaultSecurityModuleConfig.sudo,\n defaultTtlSeconds: sudoDefaultTtlSeconds,\n maxTtlSeconds: sudoMaxTtlSeconds,\n },\n webauthn: {\n ...defaultSecurityModuleConfig.webauthn,\n rpName: readText(env.OM_SECURITY_WEBAUTHN_RP_NAME) ?? defaultSecurityModuleConfig.webauthn.rpName,\n rpId: webauthnRpId,\n expectedOrigins: readWebAuthnOrigins(env, webauthnRpId),\n },\n recoveryCodes: {\n ...defaultSecurityModuleConfig.recoveryCodes,\n count: parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODE_COUNT)\n ?? parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODES_COUNT)\n ?? defaultSecurityModuleConfig.recoveryCodes.count,\n },\n }\n}\n\nexport function resolveSecurityModuleConfigForRequest(\n baseConfig: SecurityModuleConfig,\n request?: Request,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const explicitRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n const requestRpId = readRequestHostname(request)\n const rpId = explicitRpId ?? requestRpId ?? baseConfig.webauthn.rpId\n const expectedOrigins = readWebAuthnOrigins(env, rpId, request, baseConfig.webauthn.expectedOrigins)\n\n if (rpId === baseConfig.webauthn.rpId && areOriginsEqual(expectedOrigins, baseConfig.webauthn.expectedOrigins)) {\n return baseConfig\n }\n\n return {\n ...baseConfig,\n webauthn: {\n ...baseConfig.webauthn,\n rpId,\n expectedOrigins,\n },\n }\n}\n\nexport function emitMfaEmergencyBypassActiveWarning(context: string, extra?: Record<string, unknown>): void {\n securityBypassLogger.warn('MFA emergency bypass is active', {\n emergencyBypass: true,\n context,\n ...extra,\n })\n}\n\nexport function emitMfaEmergencyBypassStartupWarningIfNeeded(env: NodeJS.ProcessEnv = process.env): void {\n if (startupBypassWarningEmitted) return\n const bypass = parseBooleanWithDefault(\n env.OM_SECURITY_MFA_EMERGENCY_BYPASS,\n defaultSecurityModuleConfig.mfa.emergencyBypass,\n )\n if (!bypass) return\n startupBypassWarningEmitted = true\n emitMfaEmergencyBypassActiveWarning('startup', { startup: true })\n}\n\nexport function resetMfaEmergencyBypassStartupWarningForTests(): void {\n startupBypassWarningEmitted = false\n}\n\nemitMfaEmergencyBypassStartupWarningIfNeeded()\n\nexport function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {\n const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)\n ?? readText(env.AUTH_JWT_SECRET)\n ?? readText(env.AUTH_SECRET)\n ?? readText(env.JWT_SECRET)\n\n if (secret) {\n return secret\n }\n\n throw new Error(\n 'Security MFA setup tokens require OM_SECURITY_MFA_SETUP_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET.',\n )\n}\n\nfunction readWebAuthnOrigins(\n env: NodeJS.ProcessEnv,\n rpId: string,\n request?: Request,\n fallbackOrigins?: string[],\n): string[] {\n const fromList = (env.OM_SECURITY_WEBAUTHN_ORIGINS ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value.length > 0)\n const explicitOrigin = readText(env.OM_SECURITY_WEBAUTHN_ORIGIN)\n\n if (fromList.length > 0 || explicitOrigin) {\n return [...new Set([...fromList, ...(explicitOrigin ? [explicitOrigin] : [])])]\n }\n\n const requestOrigin = readRequestOrigin(request)\n const singletons = [\n requestOrigin,\n env.APP_URL,\n env.NEXT_PUBLIC_APP_URL,\n ]\n .map((value) => readText(value))\n .filter((value): value is string => typeof value === 'string')\n\n const origins = [...new Set([...fromList, ...singletons])]\n if (origins.length > 0) {\n return origins\n }\n\n if (fallbackOrigins && fallbackOrigins.length > 0) {\n return [...fallbackOrigins]\n }\n\n if (rpId === 'localhost') {\n return [...defaultSecurityModuleConfig.webauthn.expectedOrigins]\n }\n\n return [`https://${rpId}`]\n}\n\nfunction readText(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\n}\n\nfunction readHostname(value: string | undefined): string | undefined {\n const trimmed = readText(value)\n if (!trimmed) return undefined\n\n try {\n const url = new URL(trimmed)\n return readText(url.hostname)\n } catch {\n return undefined\n }\n}\n\nfunction readRequestHostname(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.hostname) : undefined\n}\n\nfunction readRequestOrigin(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.origin) : undefined\n}\n\nfunction readRequestUrl(request: Request | undefined): URL | null {\n if (!request) return null\n\n const forwardedHost = readForwardedHeader(request.headers.get('x-forwarded-host'))\n const forwardedProto = readForwardedHeader(request.headers.get('x-forwarded-proto'))\n if (forwardedHost) {\n const protocol = forwardedProto ?? 'https'\n try {\n return new URL(`${protocol}://${forwardedHost}`)\n } catch {\n return null\n }\n }\n\n try {\n return new URL(request.url)\n } catch {\n return null\n }\n}\n\nfunction readForwardedHeader(value: string | null): string | undefined {\n const trimmed = readText(value ?? undefined)\n if (!trimmed) return undefined\n const [first] = trimmed.split(',')\n return readText(first)\n}\n\nfunction areOriginsEqual(left: string[], right: string[]): boolean {\n if (left.length !== right.length) return false\n return left.every((value, index) => value === right[index])\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction parseNonNegativeInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed < 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,+BAA+B;
|
|
4
|
+
"sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\n\nconst TOTP_PERIOD_SECONDS = 30\nconst TOTP_DIGITS = 6\nconst DEFAULT_SETUP_TTL_MS = 10 * 60 * 1000\nconst DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000\nconst DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000\nconst MIN_SUDO_TTL_SECONDS = 30\n\nexport type SecurityModuleConfig = {\n totp: {\n issuer: string\n window: number\n digits: number\n periodSeconds: number\n setupTtlMs: number\n }\n otpEmail: {\n expirySeconds: number\n maxAttempts: number\n setupTtlMs: number\n challengeTtlMs: number\n subject: string\n }\n mfa: {\n challengeTtlMs: number\n maxAttempts: number\n emergencyBypass: boolean\n }\n sudo: {\n minTtlSeconds: number\n defaultTtlSeconds: number\n maxTtlSeconds: number\n pendingChallengeTtlMs: number\n }\n webauthn: {\n rpName: string\n rpId: string\n expectedOrigins: string[]\n setupTtlMs: number\n challengeTtlMs: number\n }\n recoveryCodes: {\n count: number\n bcryptCost: number\n }\n}\n\nexport const defaultSecurityModuleConfig: SecurityModuleConfig = {\n totp: {\n issuer: 'Open Mercato',\n window: 1,\n digits: TOTP_DIGITS,\n periodSeconds: TOTP_PERIOD_SECONDS,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n },\n otpEmail: {\n expirySeconds: 600,\n maxAttempts: 5,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: 600 * 1000,\n subject: 'Your Open Mercato verification code',\n },\n mfa: {\n challengeTtlMs: 600 * 1000,\n maxAttempts: 5,\n emergencyBypass: false,\n },\n sudo: {\n minTtlSeconds: MIN_SUDO_TTL_SECONDS,\n defaultTtlSeconds: 300,\n maxTtlSeconds: 1800,\n pendingChallengeTtlMs: DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS,\n },\n webauthn: {\n rpName: 'Open Mercato',\n rpId: 'localhost',\n expectedOrigins: ['http://localhost:3000'],\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS,\n },\n recoveryCodes: {\n count: 10,\n bcryptCost: 10,\n },\n}\n\nexport function readSecurityModuleConfig(\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const otpExpirySeconds = parsePositiveInteger(env.OM_SECURITY_OTP_EXPIRY_SECONDS)\n ?? defaultSecurityModuleConfig.otpEmail.expirySeconds\n const otpMaxAttempts = parsePositiveInteger(env.OM_SECURITY_OTP_MAX_ATTEMPTS)\n ?? defaultSecurityModuleConfig.otpEmail.maxAttempts\n const sudoMaxTtlSeconds = Math.max(\n parsePositiveInteger(env.OM_SECURITY_SUDO_MAX_TTL) ?? defaultSecurityModuleConfig.sudo.maxTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n )\n const sudoDefaultTtlSeconds = clamp(\n parsePositiveInteger(env.OM_SECURITY_SUDO_DEFAULT_TTL) ?? defaultSecurityModuleConfig.sudo.defaultTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n sudoMaxTtlSeconds,\n )\n const webauthnRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n ?? readHostname(env.APP_URL)\n ?? readHostname(env.NEXT_PUBLIC_APP_URL)\n ?? defaultSecurityModuleConfig.webauthn.rpId\n\n return {\n totp: {\n ...defaultSecurityModuleConfig.totp,\n issuer: readText(env.OM_SECURITY_TOTP_ISSUER) ?? defaultSecurityModuleConfig.totp.issuer,\n window: parseNonNegativeInteger(env.OM_SECURITY_TOTP_WINDOW) ?? defaultSecurityModuleConfig.totp.window,\n },\n otpEmail: {\n ...defaultSecurityModuleConfig.otpEmail,\n expirySeconds: otpExpirySeconds,\n maxAttempts: otpMaxAttempts,\n challengeTtlMs: otpExpirySeconds * 1000,\n },\n mfa: {\n challengeTtlMs: otpExpirySeconds * 1000,\n maxAttempts: otpMaxAttempts,\n emergencyBypass: parseBooleanWithDefault(\n env.OM_SECURITY_MFA_EMERGENCY_BYPASS,\n defaultSecurityModuleConfig.mfa.emergencyBypass,\n ),\n },\n sudo: {\n ...defaultSecurityModuleConfig.sudo,\n defaultTtlSeconds: sudoDefaultTtlSeconds,\n maxTtlSeconds: sudoMaxTtlSeconds,\n },\n webauthn: {\n ...defaultSecurityModuleConfig.webauthn,\n rpName: readText(env.OM_SECURITY_WEBAUTHN_RP_NAME) ?? defaultSecurityModuleConfig.webauthn.rpName,\n rpId: webauthnRpId,\n expectedOrigins: readWebAuthnOrigins(env, webauthnRpId),\n },\n recoveryCodes: {\n ...defaultSecurityModuleConfig.recoveryCodes,\n count: parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODE_COUNT)\n ?? parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODES_COUNT)\n ?? defaultSecurityModuleConfig.recoveryCodes.count,\n },\n }\n}\n\nexport function resolveSecurityModuleConfigForRequest(\n baseConfig: SecurityModuleConfig,\n request?: Request,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const explicitRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n const requestRpId = readRequestHostname(request)\n const rpId = explicitRpId ?? requestRpId ?? baseConfig.webauthn.rpId\n const expectedOrigins = readWebAuthnOrigins(env, rpId, request, baseConfig.webauthn.expectedOrigins)\n\n if (rpId === baseConfig.webauthn.rpId && areOriginsEqual(expectedOrigins, baseConfig.webauthn.expectedOrigins)) {\n return baseConfig\n }\n\n return {\n ...baseConfig,\n webauthn: {\n ...baseConfig.webauthn,\n rpId,\n expectedOrigins,\n },\n }\n}\n\nexport function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {\n const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)\n ?? readText(env.AUTH_JWT_SECRET)\n ?? readText(env.AUTH_SECRET)\n ?? readText(env.JWT_SECRET)\n\n if (secret) {\n return secret\n }\n\n throw new Error(\n 'Security MFA setup tokens require OM_SECURITY_MFA_SETUP_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET.',\n )\n}\n\nfunction readWebAuthnOrigins(\n env: NodeJS.ProcessEnv,\n rpId: string,\n request?: Request,\n fallbackOrigins?: string[],\n): string[] {\n const fromList = (env.OM_SECURITY_WEBAUTHN_ORIGINS ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value.length > 0)\n const explicitOrigin = readText(env.OM_SECURITY_WEBAUTHN_ORIGIN)\n\n if (fromList.length > 0 || explicitOrigin) {\n return [...new Set([...fromList, ...(explicitOrigin ? [explicitOrigin] : [])])]\n }\n\n const requestOrigin = readRequestOrigin(request)\n const singletons = [\n requestOrigin,\n env.APP_URL,\n env.NEXT_PUBLIC_APP_URL,\n ]\n .map((value) => readText(value))\n .filter((value): value is string => typeof value === 'string')\n\n const origins = [...new Set([...fromList, ...singletons])]\n if (origins.length > 0) {\n return origins\n }\n\n if (fallbackOrigins && fallbackOrigins.length > 0) {\n return [...fallbackOrigins]\n }\n\n if (rpId === 'localhost') {\n return [...defaultSecurityModuleConfig.webauthn.expectedOrigins]\n }\n\n return [`https://${rpId}`]\n}\n\nfunction readText(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\n}\n\nfunction readHostname(value: string | undefined): string | undefined {\n const trimmed = readText(value)\n if (!trimmed) return undefined\n\n try {\n const url = new URL(trimmed)\n return readText(url.hostname)\n } catch {\n return undefined\n }\n}\n\nfunction readRequestHostname(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.hostname) : undefined\n}\n\nfunction readRequestOrigin(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.origin) : undefined\n}\n\nfunction readRequestUrl(request: Request | undefined): URL | null {\n if (!request) return null\n\n const forwardedHost = readForwardedHeader(request.headers.get('x-forwarded-host'))\n const forwardedProto = readForwardedHeader(request.headers.get('x-forwarded-proto'))\n if (forwardedHost) {\n const protocol = forwardedProto ?? 'https'\n try {\n return new URL(`${protocol}://${forwardedHost}`)\n } catch {\n return null\n }\n }\n\n try {\n return new URL(request.url)\n } catch {\n return null\n }\n}\n\nfunction readForwardedHeader(value: string | null): string | undefined {\n const trimmed = readText(value ?? undefined)\n if (!trimmed) return undefined\n const [first] = trimmed.split(',')\n return readText(first)\n}\n\nfunction areOriginsEqual(left: string[], right: string[]): boolean {\n if (left.length !== right.length) return false\n return left.every((value, index) => value === right[index])\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction parseNonNegativeInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed < 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "../data/entities.js";
|
|
15
15
|
import { emitSecurityEvent } from "../events.js";
|
|
16
16
|
import { sudoTargets as defaultSudoTargets } from "../security.sudo.js";
|
|
17
|
-
import {
|
|
17
|
+
import { readSecurityModuleConfig } from "../lib/security-config.js";
|
|
18
18
|
const signedSudoTokenPayloadSchema = z.object({
|
|
19
19
|
sid: z.string(),
|
|
20
20
|
sub: z.string(),
|
|
@@ -74,25 +74,31 @@ class SudoChallengeService {
|
|
|
74
74
|
}
|
|
75
75
|
async isProtected(targetIdentifier, tenantId, organizationId) {
|
|
76
76
|
await this.ensureDeveloperDefaultsRegistered();
|
|
77
|
-
|
|
77
|
+
const candidates = await this.em.find(SudoChallengeConfig, {
|
|
78
|
+
targetIdentifier,
|
|
79
|
+
deletedAt: null
|
|
80
|
+
});
|
|
81
|
+
const resolved = candidates.filter((config) => this.matchesScope(config, tenantId ?? null, organizationId ?? null)).sort((left, right) => this.compareConfigPriority(left, right, tenantId ?? null, organizationId ?? null))[0];
|
|
82
|
+
if (!resolved || !resolved.isEnabled) {
|
|
83
|
+
return { protected: false };
|
|
84
|
+
}
|
|
85
|
+
return { protected: true, config: resolved };
|
|
78
86
|
}
|
|
79
87
|
async initiate(userId, targetIdentifier, options) {
|
|
80
|
-
const user = await this.findUserScope(userId);
|
|
81
|
-
if (!user?.tenantId) {
|
|
82
|
-
throw new SudoChallengeServiceError("User not found", 404);
|
|
83
|
-
}
|
|
84
|
-
const scopeTenantId = options?.tenantId !== void 0 ? options.tenantId : user.tenantId;
|
|
85
|
-
const scopeOrganizationId = options?.organizationId !== void 0 ? options.organizationId : user.organizationId;
|
|
86
88
|
const protection = await this.isProtected(
|
|
87
89
|
targetIdentifier,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
options?.tenantId ?? null,
|
|
91
|
+
options?.organizationId ?? null
|
|
90
92
|
);
|
|
91
93
|
if (!protection.protected || !protection.config) {
|
|
92
94
|
return { required: false };
|
|
93
95
|
}
|
|
96
|
+
const user = await this.findUserScope(userId);
|
|
97
|
+
if (!user?.tenantId) {
|
|
98
|
+
throw new SudoChallengeServiceError("User not found", 404);
|
|
99
|
+
}
|
|
94
100
|
const userMethods = await this.mfaService.getUserMethods(userId);
|
|
95
|
-
const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length
|
|
101
|
+
const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length);
|
|
96
102
|
let sessionToken = randomBytes(16).toString("hex");
|
|
97
103
|
let availableMfaMethods;
|
|
98
104
|
if (method === "mfa") {
|
|
@@ -104,15 +110,9 @@ class SudoChallengeService {
|
|
|
104
110
|
const session = this.em.create(SudoSession, {
|
|
105
111
|
userId,
|
|
106
112
|
tenantId: user.tenantId,
|
|
107
|
-
scopeTenantId,
|
|
108
|
-
scopeOrganizationId,
|
|
109
|
-
targetIdentifier,
|
|
110
|
-
sudoConfigId: protection.config.id,
|
|
111
|
-
sudoConfigUpdatedAt: protection.config.updatedAt,
|
|
112
113
|
sessionToken,
|
|
113
114
|
challengeMethod: method,
|
|
114
115
|
expiresAt,
|
|
115
|
-
verifiedAt: null,
|
|
116
116
|
createdAt: /* @__PURE__ */ new Date()
|
|
117
117
|
});
|
|
118
118
|
this.em.persist(session);
|
|
@@ -156,27 +156,16 @@ class SudoChallengeService {
|
|
|
156
156
|
if (!user?.tenantId) {
|
|
157
157
|
throw new SudoChallengeServiceError("User not found", 404);
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
const requestedTenantId = options.tenantId !== void 0 ? options.tenantId : user.tenantId;
|
|
163
|
-
const requestedOrganizationId = options.organizationId !== void 0 ? options.organizationId : user.organizationId;
|
|
164
|
-
if (options.targetIdentifier !== session.targetIdentifier || requestedTenantId !== session.scopeTenantId || requestedOrganizationId !== session.scopeOrganizationId) {
|
|
165
|
-
throw new SudoChallengeServiceError("Unable to verify sudo challenge", 403);
|
|
166
|
-
}
|
|
167
|
-
const scopeTenantId = session.scopeTenantId ?? null;
|
|
168
|
-
const scopeOrganizationId = session.scopeOrganizationId ?? null;
|
|
159
|
+
const scopeTenantId = options.tenantId !== void 0 ? options.tenantId : user.tenantId;
|
|
160
|
+
const scopeOrganizationId = options.organizationId !== void 0 ? options.organizationId : user.organizationId;
|
|
169
161
|
const protection = await this.isProtected(
|
|
170
|
-
|
|
162
|
+
options.targetIdentifier,
|
|
171
163
|
scopeTenantId,
|
|
172
164
|
scopeOrganizationId
|
|
173
165
|
);
|
|
174
166
|
if (!protection.protected || !protection.config) {
|
|
175
167
|
throw new SudoChallengeServiceError("Sudo protection is not configured for this target", 404);
|
|
176
168
|
}
|
|
177
|
-
if (protection.config.id !== session.sudoConfigId || protection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt?.getTime()) {
|
|
178
|
-
throw new SudoChallengeServiceError("Unable to verify sudo challenge", 403);
|
|
179
|
-
}
|
|
180
169
|
let verified = false;
|
|
181
170
|
let methodUsed = methodType;
|
|
182
171
|
if (session.challengeMethod === "password") {
|
|
@@ -197,65 +186,30 @@ class SudoChallengeService {
|
|
|
197
186
|
userId: session.userId,
|
|
198
187
|
tenantId: user.tenantId,
|
|
199
188
|
organizationId: user.organizationId,
|
|
200
|
-
targetIdentifier:
|
|
189
|
+
targetIdentifier: options.targetIdentifier,
|
|
201
190
|
method: methodUsed
|
|
202
191
|
});
|
|
203
192
|
throw new SudoChallengeServiceError("Unable to verify sudo challenge", 401);
|
|
204
193
|
}
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
214
|
-
if (!currentProtection.protected || !currentProtection.config || currentProtection.config.id !== session.sudoConfigId || currentProtection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt.getTime()) {
|
|
215
|
-
throw new SudoChallengeServiceError("Unable to verify sudo challenge", 403);
|
|
216
|
-
}
|
|
217
|
-
const ttlSeconds = this.normalizeTtl(currentProtection.config.ttlSeconds);
|
|
218
|
-
const currentVerifiedAt = /* @__PURE__ */ new Date();
|
|
219
|
-
const currentExpiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
220
|
-
const currentSudoToken = this.signToken({
|
|
221
|
-
sid: session.id,
|
|
222
|
-
sub: session.userId,
|
|
223
|
-
tid: scopeTenantId,
|
|
224
|
-
oid: scopeOrganizationId,
|
|
225
|
-
tgt: session.targetIdentifier,
|
|
226
|
-
exp: currentExpiresAt.getTime()
|
|
227
|
-
});
|
|
228
|
-
const consumedRows = await transactionalEm.nativeUpdate(
|
|
229
|
-
SudoSession,
|
|
230
|
-
{
|
|
231
|
-
id: session.id,
|
|
232
|
-
sessionToken: pendingToken,
|
|
233
|
-
verifiedAt: null,
|
|
234
|
-
expiresAt: { $gt: currentVerifiedAt }
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
sessionToken: currentSudoToken,
|
|
238
|
-
expiresAt: currentExpiresAt,
|
|
239
|
-
verifiedAt: currentVerifiedAt
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
if (consumedRows !== 1) {
|
|
243
|
-
throw new SudoChallengeServiceError("Unable to verify sudo challenge", 400);
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
sudoToken: currentSudoToken,
|
|
247
|
-
expiresAt: currentExpiresAt,
|
|
248
|
-
verifiedAt: currentVerifiedAt
|
|
249
|
-
};
|
|
194
|
+
const ttlSeconds = this.normalizeTtl(protection.config.ttlSeconds);
|
|
195
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
196
|
+
const sudoToken = this.signToken({
|
|
197
|
+
sid: session.id,
|
|
198
|
+
sub: session.userId,
|
|
199
|
+
tid: scopeTenantId,
|
|
200
|
+
oid: scopeOrganizationId,
|
|
201
|
+
tgt: options.targetIdentifier,
|
|
202
|
+
exp: expiresAt.getTime()
|
|
250
203
|
});
|
|
251
204
|
session.sessionToken = sudoToken;
|
|
205
|
+
session.challengeMethod = methodUsed;
|
|
252
206
|
session.expiresAt = expiresAt;
|
|
253
|
-
|
|
207
|
+
await this.em.flush();
|
|
254
208
|
await emitSecurityEvent("security.sudo.verified", {
|
|
255
209
|
userId: session.userId,
|
|
256
210
|
tenantId: scopeTenantId,
|
|
257
211
|
organizationId: scopeOrganizationId,
|
|
258
|
-
targetIdentifier:
|
|
212
|
+
targetIdentifier: options.targetIdentifier,
|
|
259
213
|
method: methodUsed,
|
|
260
214
|
expiresAt: expiresAt.toISOString()
|
|
261
215
|
});
|
|
@@ -273,11 +227,7 @@ class SudoChallengeService {
|
|
|
273
227
|
const session = await this.em.findOne(SudoSession, {
|
|
274
228
|
id: payload.sid,
|
|
275
229
|
userId: payload.sub,
|
|
276
|
-
sessionToken: token
|
|
277
|
-
targetIdentifier,
|
|
278
|
-
scopeTenantId: payload.tid,
|
|
279
|
-
scopeOrganizationId: payload.oid,
|
|
280
|
-
verifiedAt: { $ne: null }
|
|
230
|
+
sessionToken: token
|
|
281
231
|
});
|
|
282
232
|
return Boolean(session && session.expiresAt.getTime() > Date.now());
|
|
283
233
|
}
|
|
@@ -372,17 +322,12 @@ class SudoChallengeService {
|
|
|
372
322
|
isDeveloperDefault: true
|
|
373
323
|
});
|
|
374
324
|
if (existing) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
existing.ttlSeconds = ttlSeconds;
|
|
382
|
-
existing.challengeMethod = challengeMethod;
|
|
383
|
-
existing.updatedAt = /* @__PURE__ */ new Date();
|
|
384
|
-
await this.em.flush();
|
|
385
|
-
}
|
|
325
|
+
existing.isEnabled = true;
|
|
326
|
+
existing.deletedAt = null;
|
|
327
|
+
existing.ttlSeconds = this.normalizeTtl(input.ttlSeconds);
|
|
328
|
+
existing.challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO;
|
|
329
|
+
existing.updatedAt = /* @__PURE__ */ new Date();
|
|
330
|
+
await this.em.flush();
|
|
386
331
|
return;
|
|
387
332
|
}
|
|
388
333
|
const config = this.em.create(SudoChallengeConfig, {
|
|
@@ -460,27 +405,10 @@ class SudoChallengeService {
|
|
|
460
405
|
throw new SudoChallengeServiceError("Sudo configuration not found", 404);
|
|
461
406
|
}
|
|
462
407
|
}
|
|
463
|
-
resolveChallengeMethod(configuredMethod, availableMfaMethodCount
|
|
408
|
+
resolveChallengeMethod(configuredMethod, availableMfaMethodCount) {
|
|
464
409
|
if (this.securityConfig.mfa.emergencyBypass) {
|
|
465
|
-
let wouldHaveBeenMfa = false;
|
|
466
|
-
try {
|
|
467
|
-
wouldHaveBeenMfa = this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) === "mfa";
|
|
468
|
-
} catch {
|
|
469
|
-
wouldHaveBeenMfa = configuredMethod === ChallengeMethod.MFA;
|
|
470
|
-
}
|
|
471
|
-
if (wouldHaveBeenMfa) {
|
|
472
|
-
emitMfaEmergencyBypassActiveWarning("sudo challenge downgraded to password", {
|
|
473
|
-
configuredMethod,
|
|
474
|
-
availableMfaMethodCount,
|
|
475
|
-
userId,
|
|
476
|
-
targetIdentifier
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
410
|
return "password";
|
|
480
411
|
}
|
|
481
|
-
return this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount);
|
|
482
|
-
}
|
|
483
|
-
resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) {
|
|
484
412
|
if (configuredMethod === ChallengeMethod.PASSWORD) return "password";
|
|
485
413
|
if (configuredMethod === ChallengeMethod.MFA) {
|
|
486
414
|
if (availableMfaMethodCount === 0) {
|
|
@@ -490,17 +418,6 @@ class SudoChallengeService {
|
|
|
490
418
|
}
|
|
491
419
|
return availableMfaMethodCount > 0 ? "mfa" : "password";
|
|
492
420
|
}
|
|
493
|
-
async resolveProtection(em, targetIdentifier, tenantId, organizationId) {
|
|
494
|
-
const candidates = await em.find(SudoChallengeConfig, {
|
|
495
|
-
targetIdentifier,
|
|
496
|
-
deletedAt: null
|
|
497
|
-
});
|
|
498
|
-
const resolved = candidates.filter((config) => this.matchesScope(config, tenantId, organizationId)).sort((left, right) => this.compareConfigPriority(left, right, tenantId, organizationId))[0];
|
|
499
|
-
if (!resolved || !resolved.isEnabled) {
|
|
500
|
-
return { protected: false };
|
|
501
|
-
}
|
|
502
|
-
return { protected: true, config: resolved };
|
|
503
|
-
}
|
|
504
421
|
matchesScope(config, tenantId, organizationId) {
|
|
505
422
|
if (config.organizationId) {
|
|
506
423
|
return config.organizationId === organizationId && config.tenantId === tenantId;
|
|
@@ -531,12 +448,6 @@ class SudoChallengeService {
|
|
|
531
448
|
if (!session) {
|
|
532
449
|
throw new SudoChallengeServiceError("Sudo challenge session not found", 404);
|
|
533
450
|
}
|
|
534
|
-
if (!session.targetIdentifier || !session.sudoConfigId || !session.sudoConfigUpdatedAt) {
|
|
535
|
-
throw new SudoChallengeServiceError("Sudo challenge session not found", 404);
|
|
536
|
-
}
|
|
537
|
-
if (session.verifiedAt) {
|
|
538
|
-
throw new SudoChallengeServiceError("Unable to verify sudo challenge", 400);
|
|
539
|
-
}
|
|
540
451
|
if (session.expiresAt.getTime() <= Date.now()) {
|
|
541
452
|
throw new SudoChallengeServiceError("Sudo challenge session expired", 400);
|
|
542
453
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/services/SudoChallengeService.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager, FilterQuery } 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 dedupeSudoTargets,\n getSecuritySudoTargetEntries,\n type SecuritySudoTarget,\n} from '../lib/module-security-registry'\nimport {\n ChallengeMethod,\n SudoChallengeConfig,\n SudoChallengeMethodUsed,\n SudoSession,\n} from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type {\n SudoConfigInput,\n SudoConfigUpdateInput,\n} from '../data/validators'\nimport type { PasswordService } from './PasswordService'\nimport type { MfaService } from './MfaService'\nimport type { MfaVerificationService } from './MfaVerificationService'\nimport { sudoTargets as defaultSudoTargets } from '../security.sudo'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from '../lib/security-config'\n\ntype SudoMethod = 'password' | 'mfa'\n\nexport type SudoAvailableMethod = {\n type: string\n label: string\n icon: string\n}\n\nexport type SudoProtectionResolution = {\n protected: boolean\n config?: SudoChallengeConfig\n}\n\nconst signedSudoTokenPayloadSchema = z.object({\n sid: z.string(),\n sub: z.string(),\n tid: z.string().nullable(),\n oid: z.string().nullable(),\n tgt: z.string(),\n exp: z.number(),\n})\n\ntype SignedSudoTokenPayload = z.infer<typeof signedSudoTokenPayloadSchema>\n\ntype UserScope = {\n id: string\n tenantId: string | null\n organizationId: string | null\n}\n\ntype BoundPendingSudoSession = SudoSession & {\n targetIdentifier: string\n sudoConfigId: string\n sudoConfigUpdatedAt: Date\n}\n\nexport type SudoAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype DeveloperDefaultPayload = {\n targetIdentifier: string\n label?: string | null\n ttlSeconds?: number\n challengeMethod?: ChallengeMethod\n}\n\nfunction readTrimmedEnv(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\n}\n\nexport class SudoChallengeServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SudoChallengeServiceError'\n }\n}\n\nexport class SudoChallengeService {\n constructor(\n private readonly em: EntityManager,\n private readonly passwordService: PasswordService,\n private readonly mfaService: MfaService,\n private readonly mfaVerificationService: MfaVerificationService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async listConfigs(scope?: SudoAuthScope): Promise<SudoChallengeConfig[]> {\n await this.ensureDeveloperDefaultsRegistered()\n const filter: FilterQuery<SudoChallengeConfig> = { deletedAt: null }\n if (scope && !scope.isSuperAdmin) {\n if (!scope.tenantId) return []\n ;(filter as Record<string, unknown>).$or = [\n { tenantId: scope.tenantId },\n { tenantId: null },\n ]\n }\n return this.em.find(\n SudoChallengeConfig,\n filter,\n {\n orderBy: {\n targetIdentifier: 'asc',\n tenantId: 'asc',\n organizationId: 'asc',\n createdAt: 'asc',\n },\n },\n )\n }\n\n async getConfigById(id: string, scope?: SudoAuthScope): Promise<SudoChallengeConfig | null> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) return null\n if (scope && !this.isConfigVisibleToScope(config, scope)) return null\n return config\n }\n\n async isProtected(\n targetIdentifier: string,\n tenantId?: string | null,\n organizationId?: string | null,\n ): Promise<SudoProtectionResolution> {\n await this.ensureDeveloperDefaultsRegistered()\n return this.resolveProtection(this.em, targetIdentifier, tenantId ?? null, organizationId ?? null)\n }\n\n async initiate(\n userId: string,\n targetIdentifier: string,\n options?: { tenantId?: string | null; organizationId?: string | null },\n ): Promise<{\n required: boolean\n sessionId?: string\n method?: SudoMethod\n availableMfaMethods?: SudoAvailableMethod[]\n expiresAt?: Date\n }> {\n const user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n const scopeTenantId = options?.tenantId !== undefined ? options.tenantId : user.tenantId\n const scopeOrganizationId = options?.organizationId !== undefined ? options.organizationId : user.organizationId\n const protection = await this.isProtected(\n targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\n }\n\n const userMethods = await this.mfaService.getUserMethods(userId)\n const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length, userId, targetIdentifier)\n\n let sessionToken = randomBytes(16).toString('hex')\n let availableMfaMethods: SudoAvailableMethod[] | undefined\n if (method === 'mfa') {\n const challenge = await this.mfaVerificationService.createChallenge(userId)\n sessionToken = challenge.challengeId\n availableMfaMethods = challenge.availableMethods\n }\n\n const expiresAt = new Date(Date.now() + this.securityConfig.sudo.pendingChallengeTtlMs)\n const session = this.em.create(SudoSession, {\n userId,\n tenantId: user.tenantId,\n scopeTenantId,\n scopeOrganizationId,\n targetIdentifier,\n sudoConfigId: protection.config.id,\n sudoConfigUpdatedAt: protection.config.updatedAt,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n verifiedAt: null,\n createdAt: new Date(),\n })\n this.em.persist(session)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.challenged', {\n userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier,\n method,\n })\n\n return {\n required: true,\n sessionId: session.id,\n method,\n availableMfaMethods,\n expiresAt,\n }\n }\n\n async prepare(\n sessionId: string,\n methodType: string,\n options: { expectedUserId?: string } = {},\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(\n session.sessionToken,\n methodType,\n { request },\n { userId: session.userId },\n )\n }\n\n async verify(\n sessionId: string,\n methodType: string,\n payload: unknown,\n options: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n targetIdentifier: string\n },\n request?: Request,\n ): Promise<{ sudoToken: string; expiresAt: Date }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n const user = await this.findUserScope(session.userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n if (user.tenantId !== session.tenantId) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const requestedTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const requestedOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n if (\n options.targetIdentifier !== session.targetIdentifier\n || requestedTenantId !== session.scopeTenantId\n || requestedOrganizationId !== session.scopeOrganizationId\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const scopeTenantId = session.scopeTenantId ?? null\n const scopeOrganizationId = session.scopeOrganizationId ?? null\n const protection = await this.isProtected(\n session.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (!protection.protected || !protection.config) {\n throw new SudoChallengeServiceError('Sudo protection is not configured for this target', 404)\n }\n if (\n protection.config.id !== session.sudoConfigId\n || protection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt?.getTime()\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n let verified = false\n let methodUsed: string = methodType\n if (session.challengeMethod === 'password') {\n const password = this.readPassword(payload)\n verified = await this.passwordService.verifyPassword(session.userId, password)\n methodUsed = SudoChallengeMethodUsed.PASSWORD\n } else {\n verified = await this.mfaVerificationService.verifyChallenge(\n session.sessionToken,\n methodType,\n payload,\n { request },\n { userId: session.userId },\n )\n }\n\n if (!verified) {\n await emitSecurityEvent('security.sudo.failed', {\n userId: session.userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier: session.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const pendingToken = session.sessionToken\n const { sudoToken, expiresAt, verifiedAt } = await this.em.transactional(async (transactionalEm) => {\n await transactionalEm.execute('lock table \"sudo_challenge_configs\" in share mode')\n const currentProtection = await this.resolveProtection(\n transactionalEm,\n session.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (\n !currentProtection.protected\n || !currentProtection.config\n || currentProtection.config.id !== session.sudoConfigId\n || currentProtection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt.getTime()\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const ttlSeconds = this.normalizeTtl(currentProtection.config.ttlSeconds)\n const currentVerifiedAt = new Date()\n const currentExpiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const currentSudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: session.targetIdentifier,\n exp: currentExpiresAt.getTime(),\n })\n const consumedRows = await transactionalEm.nativeUpdate(\n SudoSession,\n {\n id: session.id,\n sessionToken: pendingToken,\n verifiedAt: null,\n expiresAt: { $gt: currentVerifiedAt },\n },\n {\n sessionToken: currentSudoToken,\n expiresAt: currentExpiresAt,\n verifiedAt: currentVerifiedAt,\n },\n )\n if (consumedRows !== 1) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)\n }\n return {\n sudoToken: currentSudoToken,\n expiresAt: currentExpiresAt,\n verifiedAt: currentVerifiedAt,\n }\n })\n session.sessionToken = sudoToken\n session.expiresAt = expiresAt\n session.verifiedAt = verifiedAt\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: session.targetIdentifier,\n method: methodUsed,\n expiresAt: expiresAt.toISOString(),\n })\n\n return { sudoToken, expiresAt }\n }\n\n async validateToken(\n token: string,\n targetIdentifier: string,\n options?: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n },\n ): Promise<boolean> {\n if (!token) return false\n const payload = this.readSignedToken(token)\n if (!payload) return false\n if (payload.exp <= Date.now()) return false\n if (payload.tgt !== targetIdentifier) return false\n if (options?.expectedUserId && payload.sub !== options.expectedUserId) return false\n if (options?.tenantId !== undefined && payload.tid !== (options.tenantId ?? null)) return false\n if (options?.organizationId !== undefined && payload.oid !== (options.organizationId ?? null)) return false\n\n const session = await this.em.findOne(SudoSession, {\n id: payload.sid,\n userId: payload.sub,\n sessionToken: token,\n targetIdentifier,\n scopeTenantId: payload.tid,\n scopeOrganizationId: payload.oid,\n verifiedAt: { $ne: null },\n } as FilterQuery<SudoSession>)\n\n return Boolean(session && session.expiresAt.getTime() > Date.now())\n }\n\n async createConfig(\n input: SudoConfigInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const inputTenantId = input.tenantId ?? null\n const inputOrganizationId = input.organizationId ?? null\n this.validateScope(inputTenantId, inputOrganizationId)\n if (scope) this.assertWriteScope(inputTenantId, inputOrganizationId, scope)\n await this.ensureUniqueConfig(input.targetIdentifier, inputTenantId, inputOrganizationId)\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: inputTenantId,\n organizationId: inputOrganizationId,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: input.isEnabled,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod,\n configuredBy,\n isDeveloperDefault: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.created', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async updateConfig(\n id: string,\n input: SudoConfigUpdateInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n const nextTenantId = input.tenantId !== undefined ? input.tenantId ?? null : config.tenantId ?? null\n const nextOrganizationId = input.organizationId !== undefined ? input.organizationId ?? null : config.organizationId ?? null\n const nextTargetIdentifier = input.targetIdentifier ?? config.targetIdentifier\n this.validateScope(nextTenantId, nextOrganizationId)\n if (scope) this.assertWriteScope(nextTenantId, nextOrganizationId, scope)\n await this.ensureUniqueConfig(nextTargetIdentifier, nextTenantId, nextOrganizationId, config.id)\n\n if (input.tenantId !== undefined) config.tenantId = input.tenantId ?? null\n if (input.organizationId !== undefined) config.organizationId = input.organizationId ?? null\n if (input.label !== undefined) config.label = input.label ?? null\n if (input.targetIdentifier !== undefined) config.targetIdentifier = input.targetIdentifier\n if (input.isEnabled !== undefined) config.isEnabled = input.isEnabled\n if (input.ttlSeconds !== undefined) config.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n if (input.challengeMethod !== undefined) config.challengeMethod = input.challengeMethod\n config.configuredBy = configuredBy\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.updated', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async deleteConfig(id: string, scope?: SudoAuthScope): Promise<void> {\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n config.deletedAt = new Date()\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.deleted', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n })\n }\n\n async registerDeveloperDefault(\n input: DeveloperDefaultPayload,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier: input.targetIdentifier,\n tenantId: null,\n organizationId: null,\n isDeveloperDefault: true,\n })\n\n if (existing) {\n const ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n const challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n const changed = !existing.isEnabled\n || Boolean(existing.deletedAt)\n || existing.ttlSeconds !== ttlSeconds\n || existing.challengeMethod !== challengeMethod\n if (changed) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = ttlSeconds\n existing.challengeMethod = challengeMethod\n existing.updatedAt = new Date()\n await this.em.flush()\n }\n return\n }\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: null,\n organizationId: null,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: true,\n isDeveloperDefault: true,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod ?? ChallengeMethod.AUTO,\n configuredBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n }\n\n async cleanupExpired(): Promise<number> {\n return this.em.nativeDelete(SudoSession, {\n expiresAt: { $lte: new Date() },\n })\n }\n\n private async ensureDeveloperDefaultsRegistered(): Promise<void> {\n const registryEntries = getSecuritySudoTargetEntries()\n const registryTargets = registryEntries.flatMap((entry) => entry.targets ?? [])\n const fallbackTargets = registryEntries.length === 0 ? defaultSudoTargets : []\n\n for (const target of dedupeSudoTargets([\n ...registryTargets,\n ...fallbackTargets,\n ])) {\n await this.registerDeveloperDefault(this.readDeveloperDefault(target))\n }\n }\n\n private readDeveloperDefault(target: SecuritySudoTarget): DeveloperDefaultPayload {\n return {\n targetIdentifier: target.identifier,\n label: target.label ?? null,\n ttlSeconds: target.ttlSeconds,\n challengeMethod: this.toChallengeMethod(target.challengeMethod),\n }\n }\n\n private async ensureUniqueConfig(\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ignoreId?: string,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (existing && existing.id !== ignoreId) {\n throw new SudoChallengeServiceError('A sudo configuration for this target and scope already exists', 409)\n }\n }\n\n private validateScope(tenantId: string | null, organizationId: string | null): void {\n if (organizationId && !tenantId) {\n throw new SudoChallengeServiceError('Organization-scoped sudo config requires a tenant', 400)\n }\n }\n\n private isConfigVisibleToScope(config: SudoChallengeConfig, scope: SudoAuthScope): boolean {\n if (scope.isSuperAdmin) return true\n if (!scope.tenantId) return false\n const configTenantId = config.tenantId ?? null\n if (configTenantId === null) return true\n return configTenantId === scope.tenantId\n }\n\n private assertWriteScope(\n targetTenantId: string | null,\n targetOrganizationId: string | null,\n scope: SudoAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n if (!scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if ((targetTenantId ?? null) !== scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && targetOrganizationId !== null\n && targetOrganizationId !== scope.organizationId\n ) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n private resolveChallengeMethod(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n userId?: string,\n targetIdentifier?: string,\n ): SudoMethod {\n if (this.securityConfig.mfa.emergencyBypass) {\n let wouldHaveBeenMfa = false\n try {\n wouldHaveBeenMfa = this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) === 'mfa'\n } catch {\n wouldHaveBeenMfa = configuredMethod === ChallengeMethod.MFA\n }\n if (wouldHaveBeenMfa) {\n emitMfaEmergencyBypassActiveWarning('sudo challenge downgraded to password', {\n configuredMethod,\n availableMfaMethodCount,\n userId,\n targetIdentifier,\n })\n }\n return 'password'\n }\n return this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount)\n }\n\n private resolveChallengeMethodWithoutBypass(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n ): SudoMethod {\n if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'\n if (configuredMethod === ChallengeMethod.MFA) {\n if (availableMfaMethodCount === 0) {\n throw new SudoChallengeServiceError('This sudo target requires MFA, but no MFA methods are configured', 400)\n }\n return 'mfa'\n }\n return availableMfaMethodCount > 0 ? 'mfa' : 'password'\n }\n\n private async resolveProtection(\n em: EntityManager,\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ): Promise<SudoProtectionResolution> {\n const candidates = await em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId, organizationId))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId, organizationId))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\n }\n\n private matchesScope(config: SudoChallengeConfig, tenantId: string | null, organizationId: string | null): boolean {\n if (config.organizationId) {\n return config.organizationId === organizationId && config.tenantId === tenantId\n }\n if (config.tenantId) {\n return config.tenantId === tenantId\n }\n return true\n }\n\n private compareConfigPriority(\n left: SudoChallengeConfig,\n right: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n const leftScore = this.getScopePriority(left, tenantId, organizationId)\n const rightScore = this.getScopePriority(right, tenantId, organizationId)\n if (leftScore !== rightScore) return leftScore - rightScore\n if (left.isDeveloperDefault !== right.isDeveloperDefault) {\n return left.isDeveloperDefault ? 1 : -1\n }\n return right.updatedAt.getTime() - left.updatedAt.getTime()\n }\n\n private getScopePriority(\n config: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n if (config.organizationId === organizationId && config.tenantId === tenantId) return 0\n if (!config.organizationId && config.tenantId === tenantId) return 1\n if (!config.organizationId && !config.tenantId && !config.isDeveloperDefault) return 2\n if (!config.organizationId && !config.tenantId && config.isDeveloperDefault) return 3\n return 4\n }\n\n private async getPendingSession(sessionId: string): Promise<BoundPendingSudoSession> {\n const session = await this.em.findOne(SudoSession, { id: sessionId })\n if (!session) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (!session.targetIdentifier || !session.sudoConfigId || !session.sudoConfigUpdatedAt) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.verifiedAt) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session as BoundPendingSudoSession\n }\n\n private async findUserScope(userId: string): Promise<UserScope | null> {\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n {},\n )\n\n if (!user) return null\n return {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n }\n }\n\n private normalizeTtl(value?: number | null): number {\n const rawValue = value ?? this.securityConfig.sudo.defaultTtlSeconds\n return Math.min(\n Math.max(rawValue, this.securityConfig.sudo.minTtlSeconds),\n this.securityConfig.sudo.maxTtlSeconds,\n )\n }\n\n private readPassword(payload: unknown): string {\n if (!payload || typeof payload !== 'object') {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n const maybePassword = (payload as Record<string, unknown>).password\n if (typeof maybePassword !== 'string' || maybePassword.trim().length === 0) {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n return maybePassword\n }\n\n private signToken(payload: SignedSudoTokenPayload): string {\n const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n return `${encodedPayload}.${signature}`\n }\n\n private readSignedToken(token: string): SignedSudoTokenPayload | null {\n const [encodedPayload, signature] = token.split('.')\n if (!encodedPayload || !signature) return null\n\n const expected = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n const signatureBuffer = Buffer.from(signature)\n const expectedBuffer = Buffer.from(expected)\n if (signatureBuffer.length !== expectedBuffer.length) return null\n if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null\n\n try {\n const parsed = signedSudoTokenPayloadSchema.safeParse(\n JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')),\n )\n if (!parsed.success) return null\n return parsed.data\n } catch {\n return null\n }\n }\n\n private getSudoSecret(): string {\n const secret =\n readTrimmedEnv(process.env.OM_SECURITY_SUDO_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_JWT_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_SECRET)\n ?? readTrimmedEnv(process.env.JWT_SECRET)\n if (secret) return secret\n throw new Error(\n 'Sudo step-up tokens require OM_SECURITY_SUDO_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET to be set.',\n )\n }\n\n private toChallengeMethod(\n method: SecuritySudoTarget['challengeMethod'],\n ): ChallengeMethod | undefined {\n switch (method) {\n case 'password':\n return ChallengeMethod.PASSWORD\n case 'mfa':\n return ChallengeMethod.MFA\n case 'auto':\n default:\n return ChallengeMethod.AUTO\n }\n }\n}\n\nexport default SudoChallengeService\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager, FilterQuery } 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 dedupeSudoTargets,\n getSecuritySudoTargetEntries,\n type SecuritySudoTarget,\n} from '../lib/module-security-registry'\nimport {\n ChallengeMethod,\n SudoChallengeConfig,\n SudoChallengeMethodUsed,\n SudoSession,\n} from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type {\n SudoConfigInput,\n SudoConfigUpdateInput,\n} from '../data/validators'\nimport type { PasswordService } from './PasswordService'\nimport type { MfaService } from './MfaService'\nimport type { MfaVerificationService } from './MfaVerificationService'\nimport { sudoTargets as defaultSudoTargets } from '../security.sudo'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype SudoMethod = 'password' | 'mfa'\n\nexport type SudoAvailableMethod = {\n type: string\n label: string\n icon: string\n}\n\nexport type SudoProtectionResolution = {\n protected: boolean\n config?: SudoChallengeConfig\n}\n\nconst signedSudoTokenPayloadSchema = z.object({\n sid: z.string(),\n sub: z.string(),\n tid: z.string().nullable(),\n oid: z.string().nullable(),\n tgt: z.string(),\n exp: z.number(),\n})\n\ntype SignedSudoTokenPayload = z.infer<typeof signedSudoTokenPayloadSchema>\n\ntype UserScope = {\n id: string\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type SudoAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype DeveloperDefaultPayload = {\n targetIdentifier: string\n label?: string | null\n ttlSeconds?: number\n challengeMethod?: ChallengeMethod\n}\n\nfunction readTrimmedEnv(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\n}\n\nexport class SudoChallengeServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SudoChallengeServiceError'\n }\n}\n\nexport class SudoChallengeService {\n constructor(\n private readonly em: EntityManager,\n private readonly passwordService: PasswordService,\n private readonly mfaService: MfaService,\n private readonly mfaVerificationService: MfaVerificationService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async listConfigs(scope?: SudoAuthScope): Promise<SudoChallengeConfig[]> {\n await this.ensureDeveloperDefaultsRegistered()\n const filter: FilterQuery<SudoChallengeConfig> = { deletedAt: null }\n if (scope && !scope.isSuperAdmin) {\n if (!scope.tenantId) return []\n ;(filter as Record<string, unknown>).$or = [\n { tenantId: scope.tenantId },\n { tenantId: null },\n ]\n }\n return this.em.find(\n SudoChallengeConfig,\n filter,\n {\n orderBy: {\n targetIdentifier: 'asc',\n tenantId: 'asc',\n organizationId: 'asc',\n createdAt: 'asc',\n },\n },\n )\n }\n\n async getConfigById(id: string, scope?: SudoAuthScope): Promise<SudoChallengeConfig | null> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) return null\n if (scope && !this.isConfigVisibleToScope(config, scope)) return null\n return config\n }\n\n async isProtected(\n targetIdentifier: string,\n tenantId?: string | null,\n organizationId?: string | null,\n ): Promise<SudoProtectionResolution> {\n await this.ensureDeveloperDefaultsRegistered()\n\n const candidates = await this.em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId ?? null, organizationId ?? null))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId ?? null, organizationId ?? null))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\n }\n\n async initiate(\n userId: string,\n targetIdentifier: string,\n options?: { tenantId?: string | null; organizationId?: string | null },\n ): Promise<{\n required: boolean\n sessionId?: string\n method?: SudoMethod\n availableMfaMethods?: SudoAvailableMethod[]\n expiresAt?: Date\n }> {\n const protection = await this.isProtected(\n targetIdentifier,\n options?.tenantId ?? null,\n options?.organizationId ?? null,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\n }\n\n const user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const userMethods = await this.mfaService.getUserMethods(userId)\n const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length)\n\n let sessionToken = randomBytes(16).toString('hex')\n let availableMfaMethods: SudoAvailableMethod[] | undefined\n if (method === 'mfa') {\n const challenge = await this.mfaVerificationService.createChallenge(userId)\n sessionToken = challenge.challengeId\n availableMfaMethods = challenge.availableMethods\n }\n\n const expiresAt = new Date(Date.now() + this.securityConfig.sudo.pendingChallengeTtlMs)\n const session = this.em.create(SudoSession, {\n userId,\n tenantId: user.tenantId,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n createdAt: new Date(),\n })\n this.em.persist(session)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.challenged', {\n userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier,\n method,\n })\n\n return {\n required: true,\n sessionId: session.id,\n method,\n availableMfaMethods,\n expiresAt,\n }\n }\n\n async prepare(\n sessionId: string,\n methodType: string,\n options: { expectedUserId?: string } = {},\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(\n session.sessionToken,\n methodType,\n { request },\n { userId: session.userId },\n )\n }\n\n async verify(\n sessionId: string,\n methodType: string,\n payload: unknown,\n options: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n targetIdentifier: string\n },\n request?: Request,\n ): Promise<{ sudoToken: string; expiresAt: Date }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n const user = await this.findUserScope(session.userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const scopeTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const scopeOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n const protection = await this.isProtected(\n options.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (!protection.protected || !protection.config) {\n throw new SudoChallengeServiceError('Sudo protection is not configured for this target', 404)\n }\n\n let verified = false\n let methodUsed: string = methodType\n if (session.challengeMethod === 'password') {\n const password = this.readPassword(payload)\n verified = await this.passwordService.verifyPassword(session.userId, password)\n methodUsed = SudoChallengeMethodUsed.PASSWORD\n } else {\n verified = await this.mfaVerificationService.verifyChallenge(\n session.sessionToken,\n methodType,\n payload,\n { request },\n { userId: session.userId },\n )\n }\n\n if (!verified) {\n await emitSecurityEvent('security.sudo.failed', {\n userId: session.userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const ttlSeconds = this.normalizeTtl(protection.config.ttlSeconds)\n const expiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const sudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: options.targetIdentifier,\n exp: expiresAt.getTime(),\n })\n\n session.sessionToken = sudoToken\n session.challengeMethod = methodUsed\n session.expiresAt = expiresAt\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n expiresAt: expiresAt.toISOString(),\n })\n\n return { sudoToken, expiresAt }\n }\n\n async validateToken(\n token: string,\n targetIdentifier: string,\n options?: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n },\n ): Promise<boolean> {\n if (!token) return false\n const payload = this.readSignedToken(token)\n if (!payload) return false\n if (payload.exp <= Date.now()) return false\n if (payload.tgt !== targetIdentifier) return false\n if (options?.expectedUserId && payload.sub !== options.expectedUserId) return false\n if (options?.tenantId !== undefined && payload.tid !== (options.tenantId ?? null)) return false\n if (options?.organizationId !== undefined && payload.oid !== (options.organizationId ?? null)) return false\n\n const session = await this.em.findOne(SudoSession, {\n id: payload.sid,\n userId: payload.sub,\n sessionToken: token,\n } as FilterQuery<SudoSession>)\n\n return Boolean(session && session.expiresAt.getTime() > Date.now())\n }\n\n async createConfig(\n input: SudoConfigInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const inputTenantId = input.tenantId ?? null\n const inputOrganizationId = input.organizationId ?? null\n this.validateScope(inputTenantId, inputOrganizationId)\n if (scope) this.assertWriteScope(inputTenantId, inputOrganizationId, scope)\n await this.ensureUniqueConfig(input.targetIdentifier, inputTenantId, inputOrganizationId)\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: inputTenantId,\n organizationId: inputOrganizationId,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: input.isEnabled,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod,\n configuredBy,\n isDeveloperDefault: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.created', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async updateConfig(\n id: string,\n input: SudoConfigUpdateInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n const nextTenantId = input.tenantId !== undefined ? input.tenantId ?? null : config.tenantId ?? null\n const nextOrganizationId = input.organizationId !== undefined ? input.organizationId ?? null : config.organizationId ?? null\n const nextTargetIdentifier = input.targetIdentifier ?? config.targetIdentifier\n this.validateScope(nextTenantId, nextOrganizationId)\n if (scope) this.assertWriteScope(nextTenantId, nextOrganizationId, scope)\n await this.ensureUniqueConfig(nextTargetIdentifier, nextTenantId, nextOrganizationId, config.id)\n\n if (input.tenantId !== undefined) config.tenantId = input.tenantId ?? null\n if (input.organizationId !== undefined) config.organizationId = input.organizationId ?? null\n if (input.label !== undefined) config.label = input.label ?? null\n if (input.targetIdentifier !== undefined) config.targetIdentifier = input.targetIdentifier\n if (input.isEnabled !== undefined) config.isEnabled = input.isEnabled\n if (input.ttlSeconds !== undefined) config.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n if (input.challengeMethod !== undefined) config.challengeMethod = input.challengeMethod\n config.configuredBy = configuredBy\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.updated', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async deleteConfig(id: string, scope?: SudoAuthScope): Promise<void> {\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n config.deletedAt = new Date()\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.deleted', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n })\n }\n\n async registerDeveloperDefault(\n input: DeveloperDefaultPayload,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier: input.targetIdentifier,\n tenantId: null,\n organizationId: null,\n isDeveloperDefault: true,\n })\n\n if (existing) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n existing.challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n existing.updatedAt = new Date()\n await this.em.flush()\n return\n }\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: null,\n organizationId: null,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: true,\n isDeveloperDefault: true,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod ?? ChallengeMethod.AUTO,\n configuredBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n }\n\n async cleanupExpired(): Promise<number> {\n return this.em.nativeDelete(SudoSession, {\n expiresAt: { $lte: new Date() },\n })\n }\n\n private async ensureDeveloperDefaultsRegistered(): Promise<void> {\n const registryEntries = getSecuritySudoTargetEntries()\n const registryTargets = registryEntries.flatMap((entry) => entry.targets ?? [])\n const fallbackTargets = registryEntries.length === 0 ? defaultSudoTargets : []\n\n for (const target of dedupeSudoTargets([\n ...registryTargets,\n ...fallbackTargets,\n ])) {\n await this.registerDeveloperDefault(this.readDeveloperDefault(target))\n }\n }\n\n private readDeveloperDefault(target: SecuritySudoTarget): DeveloperDefaultPayload {\n return {\n targetIdentifier: target.identifier,\n label: target.label ?? null,\n ttlSeconds: target.ttlSeconds,\n challengeMethod: this.toChallengeMethod(target.challengeMethod),\n }\n }\n\n private async ensureUniqueConfig(\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ignoreId?: string,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (existing && existing.id !== ignoreId) {\n throw new SudoChallengeServiceError('A sudo configuration for this target and scope already exists', 409)\n }\n }\n\n private validateScope(tenantId: string | null, organizationId: string | null): void {\n if (organizationId && !tenantId) {\n throw new SudoChallengeServiceError('Organization-scoped sudo config requires a tenant', 400)\n }\n }\n\n private isConfigVisibleToScope(config: SudoChallengeConfig, scope: SudoAuthScope): boolean {\n if (scope.isSuperAdmin) return true\n if (!scope.tenantId) return false\n const configTenantId = config.tenantId ?? null\n if (configTenantId === null) return true\n return configTenantId === scope.tenantId\n }\n\n private assertWriteScope(\n targetTenantId: string | null,\n targetOrganizationId: string | null,\n scope: SudoAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n if (!scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if ((targetTenantId ?? null) !== scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && targetOrganizationId !== null\n && targetOrganizationId !== scope.organizationId\n ) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n private resolveChallengeMethod(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n ): SudoMethod {\n if (this.securityConfig.mfa.emergencyBypass) {\n return 'password'\n }\n if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'\n if (configuredMethod === ChallengeMethod.MFA) {\n if (availableMfaMethodCount === 0) {\n throw new SudoChallengeServiceError('This sudo target requires MFA, but no MFA methods are configured', 400)\n }\n return 'mfa'\n }\n return availableMfaMethodCount > 0 ? 'mfa' : 'password'\n }\n\n private matchesScope(config: SudoChallengeConfig, tenantId: string | null, organizationId: string | null): boolean {\n if (config.organizationId) {\n return config.organizationId === organizationId && config.tenantId === tenantId\n }\n if (config.tenantId) {\n return config.tenantId === tenantId\n }\n return true\n }\n\n private compareConfigPriority(\n left: SudoChallengeConfig,\n right: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n const leftScore = this.getScopePriority(left, tenantId, organizationId)\n const rightScore = this.getScopePriority(right, tenantId, organizationId)\n if (leftScore !== rightScore) return leftScore - rightScore\n if (left.isDeveloperDefault !== right.isDeveloperDefault) {\n return left.isDeveloperDefault ? 1 : -1\n }\n return right.updatedAt.getTime() - left.updatedAt.getTime()\n }\n\n private getScopePriority(\n config: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n if (config.organizationId === organizationId && config.tenantId === tenantId) return 0\n if (!config.organizationId && config.tenantId === tenantId) return 1\n if (!config.organizationId && !config.tenantId && !config.isDeveloperDefault) return 2\n if (!config.organizationId && !config.tenantId && config.isDeveloperDefault) return 3\n return 4\n }\n\n private async getPendingSession(sessionId: string): Promise<SudoSession> {\n const session = await this.em.findOne(SudoSession, { id: sessionId })\n if (!session) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session\n }\n\n private async findUserScope(userId: string): Promise<UserScope | null> {\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n {},\n )\n\n if (!user) return null\n return {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n }\n }\n\n private normalizeTtl(value?: number | null): number {\n const rawValue = value ?? this.securityConfig.sudo.defaultTtlSeconds\n return Math.min(\n Math.max(rawValue, this.securityConfig.sudo.minTtlSeconds),\n this.securityConfig.sudo.maxTtlSeconds,\n )\n }\n\n private readPassword(payload: unknown): string {\n if (!payload || typeof payload !== 'object') {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n const maybePassword = (payload as Record<string, unknown>).password\n if (typeof maybePassword !== 'string' || maybePassword.trim().length === 0) {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n return maybePassword\n }\n\n private signToken(payload: SignedSudoTokenPayload): string {\n const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n return `${encodedPayload}.${signature}`\n }\n\n private readSignedToken(token: string): SignedSudoTokenPayload | null {\n const [encodedPayload, signature] = token.split('.')\n if (!encodedPayload || !signature) return null\n\n const expected = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n const signatureBuffer = Buffer.from(signature)\n const expectedBuffer = Buffer.from(expected)\n if (signatureBuffer.length !== expectedBuffer.length) return null\n if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null\n\n try {\n const parsed = signedSudoTokenPayloadSchema.safeParse(\n JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')),\n )\n if (!parsed.success) return null\n return parsed.data\n } catch {\n return null\n }\n }\n\n private getSudoSecret(): string {\n const secret =\n readTrimmedEnv(process.env.OM_SECURITY_SUDO_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_JWT_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_SECRET)\n ?? readTrimmedEnv(process.env.JWT_SECRET)\n if (secret) return secret\n throw new Error(\n 'Sudo step-up tokens require OM_SECURITY_SUDO_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET to be set.',\n )\n }\n\n private toChallengeMethod(\n method: SecuritySudoTarget['challengeMethod'],\n ): ChallengeMethod | undefined {\n switch (method) {\n case 'password':\n return ChallengeMethod.PASSWORD\n case 'mfa':\n return ChallengeMethod.MFA\n case 'auto':\n default:\n return ChallengeMethod.AUTO\n }\n }\n}\n\nexport default SudoChallengeService\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|