@puzzle-ui/auth 0.0.0-stage → 0.5.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/README.md +301 -2
- package/dist/LICENSE +21 -0
- package/dist/auth.lifecycle.manager.cjs +835 -0
- package/dist/auth.lifecycle.manager.cjs.map +1 -0
- package/dist/auth.lifecycle.manager.d.cts +115 -0
- package/dist/auth.lifecycle.manager.d.ts +115 -0
- package/dist/auth.lifecycle.manager.js +835 -0
- package/dist/auth.lifecycle.manager.js.map +1 -0
- package/dist/auth.manager.cjs +454 -0
- package/dist/auth.manager.cjs.map +1 -0
- package/dist/auth.manager.d.cts +54 -0
- package/dist/auth.manager.d.ts +54 -0
- package/dist/auth.manager.js +454 -0
- package/dist/auth.manager.js.map +1 -0
- package/dist/auth.mfa.manager.cjs +1488 -0
- package/dist/auth.mfa.manager.cjs.map +1 -0
- package/dist/auth.mfa.manager.d.cts +100 -0
- package/dist/auth.mfa.manager.d.ts +100 -0
- package/dist/auth.mfa.manager.js +1488 -0
- package/dist/auth.mfa.manager.js.map +1 -0
- package/dist/auth.rate-limit.cjs +78 -0
- package/dist/auth.rate-limit.cjs.map +1 -0
- package/dist/auth.rate-limit.d.cts +24 -0
- package/dist/auth.rate-limit.d.ts +24 -0
- package/dist/auth.rate-limit.js +78 -0
- package/dist/auth.rate-limit.js.map +1 -0
- package/dist/auth.step-up.manager.cjs +269 -0
- package/dist/auth.step-up.manager.cjs.map +1 -0
- package/dist/auth.step-up.manager.d.cts +74 -0
- package/dist/auth.step-up.manager.d.ts +74 -0
- package/dist/auth.step-up.manager.js +268 -0
- package/dist/auth.step-up.manager.js.map +1 -0
- package/dist/auth.storage.cjs +164 -0
- package/dist/auth.storage.cjs.map +1 -0
- package/dist/auth.storage.d.cts +29 -0
- package/dist/auth.storage.d.ts +29 -0
- package/dist/auth.storage.js +148 -0
- package/dist/auth.storage.js.map +1 -0
- package/dist/errors/auth.errors.cjs +101 -0
- package/dist/errors/auth.errors.cjs.map +1 -0
- package/dist/errors/auth.errors.d.cts +62 -0
- package/dist/errors/auth.errors.d.ts +62 -0
- package/dist/errors/auth.errors.js +90 -0
- package/dist/errors/auth.errors.js.map +1 -0
- package/dist/index.cjs +54 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +13 -0
- package/dist/providers/mfa-provider.types.d.cts +66 -0
- package/dist/providers/mfa-provider.types.d.ts +66 -0
- package/dist/providers/provider.types.d.cts +34 -0
- package/dist/providers/provider.types.d.ts +34 -0
- package/dist/providers/simplewebauthn-provider.cjs +185 -0
- package/dist/providers/simplewebauthn-provider.cjs.map +1 -0
- package/dist/providers/simplewebauthn-provider.d.cts +35 -0
- package/dist/providers/simplewebauthn-provider.d.ts +35 -0
- package/dist/providers/simplewebauthn-provider.js +185 -0
- package/dist/providers/simplewebauthn-provider.js.map +1 -0
- package/dist/providers/webcrypto-secret-protector.cjs +118 -0
- package/dist/providers/webcrypto-secret-protector.cjs.map +1 -0
- package/dist/providers/webcrypto-secret-protector.d.cts +14 -0
- package/dist/providers/webcrypto-secret-protector.d.ts +14 -0
- package/dist/providers/webcrypto-secret-protector.js +118 -0
- package/dist/providers/webcrypto-secret-protector.js.map +1 -0
- package/dist/types/auth-lifecycle.types.d.cts +53 -0
- package/dist/types/auth-lifecycle.types.d.ts +53 -0
- package/dist/types/auth-manager.types.d.cts +106 -0
- package/dist/types/auth-manager.types.d.ts +106 -0
- package/dist/types/credential.types.d.cts +9 -0
- package/dist/types/credential.types.d.ts +9 -0
- package/dist/types/mfa.types.d.cts +244 -0
- package/dist/types/mfa.types.d.ts +244 -0
- package/dist/types/state.types.d.cts +33 -0
- package/dist/types/state.types.d.ts +33 -0
- package/dist/types/user.types.d.cts +9 -0
- package/dist/types/user.types.d.ts +9 -0
- package/dist/utils/auth-crypto.utils.cjs +38 -0
- package/dist/utils/auth-crypto.utils.cjs.map +1 -0
- package/dist/utils/auth-crypto.utils.d.cts +4 -0
- package/dist/utils/auth-crypto.utils.d.ts +4 -0
- package/dist/utils/auth-crypto.utils.js +35 -0
- package/dist/utils/auth-crypto.utils.js.map +1 -0
- package/dist/utils/email.utils.cjs +20 -0
- package/dist/utils/email.utils.cjs.map +1 -0
- package/dist/utils/email.utils.d.cts +2 -0
- package/dist/utils/email.utils.d.ts +2 -0
- package/dist/utils/email.utils.js +19 -0
- package/dist/utils/email.utils.js.map +1 -0
- package/dist/utils/totp.utils.cjs +145 -0
- package/dist/utils/totp.utils.cjs.map +1 -0
- package/dist/utils/totp.utils.d.cts +26 -0
- package/dist/utils/totp.utils.d.ts +26 -0
- package/dist/utils/totp.utils.js +140 -0
- package/dist/utils/totp.utils.js.map +1 -0
- package/package.json +45 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.manager.cjs","names":["AuthSessionError","AuthConfigurationError","AuthValidationError","initializeAuthStorage","generateAuthId","AuthProviderError","createRateLimitKey","AuthRateLimitError","normalizeEmail","normalizeDisplayName","StorageDuplicateError","AuthAccountConflictError","AuthInvalidCredentialsError","AuthEmailVerificationRequiredError"],"sources":["../src/auth.manager.ts"],"sourcesContent":["import {\n StorageDuplicateError,\n type StorageAdapter,\n type StorageBatchOperation,\n} from \"@puzzle-ui/storage\";\n\nimport type { SessionRecord, SessionManager } from \"@puzzle-ui/session\";\n\nimport type {\n PasswordHash,\n PasswordHasher,\n RateLimitProvider,\n} from \"./providers/provider.types\";\n\nimport type {\n AuthManager,\n AuthManagerOptions,\n AuthRegisterInput,\n AuthSignInInput,\n AuthSignInResult,\n AuthSession,\n AuthRateLimitPolicies,\n AuthRateLimitRule,\n AuthMfaPolicy,\n AuthRegisterResult,\n} from \"./types/auth-manager.types\";\n\nimport type {\n AuthAuditAction,\n AuthAuditEventStorageRecord,\n} from \"./types/auth-lifecycle.types\";\n\nimport type { AuthState } from \"./types/state.types\";\nimport type { AuthUser } from \"./types/user.types\";\nimport type { PasswordCredentialStorageRecord } from \"./types/credential.types\";\n\nimport {\n AUTH_AUDIT_COLLECTION,\n AUTH_CREDENTIAL_COLLECTION,\n AUTH_USER_COLLECTION,\n initializeAuthStorage,\n type AuthUserStorageRecord,\n} from \"./auth.storage\";\n\nimport {\n AuthAccountConflictError,\n AuthConfigurationError,\n AuthEmailVerificationRequiredError,\n AuthInvalidCredentialsError,\n AuthProviderError,\n AuthRateLimitError,\n AuthSessionError,\n AuthValidationError,\n} from \"./errors/auth.errors\";\n\nimport { normalizeDisplayName, normalizeEmail } from \"./utils/email.utils\";\n\nimport { createRateLimitKey, generateAuthId } from \"./utils/auth-crypto.utils\";\n\nimport type { AuthMfaGateway } from \"./types/mfa.types\";\n\nexport type StorageAuthManagerDependencies = {\n readonly storage: StorageAdapter;\n readonly passwordHasher: PasswordHasher;\n readonly rateLimitProvider: RateLimitProvider;\n readonly sessions: SessionManager;\n readonly mfa?: AuthMfaGateway;\n};\n\nconst DEFAULT_RATE_LIMITS: AuthRateLimitPolicies = {\n registration: {\n email: { limit: 5, windowSeconds: 3600 },\n ip: { limit: 10, windowSeconds: 3600 },\n },\n signIn: {\n email: { limit: 10, windowSeconds: 900 },\n ip: { limit: 30, windowSeconds: 900 },\n },\n};\n\nconst DEFAULT_MINIMUM_PASSWORD_LENGTH = 8;\nconst DEFAULT_MAXIMUM_PASSWORD_LENGTH = 1024;\n\nconst toPublicUser = (record: AuthUserStorageRecord): AuthUser => ({\n id: record.id,\n email: record.email,\n emailVerifiedAt: record.emailVerifiedAt,\n name: record.name,\n image: record.image,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n});\n\nconst toPublicSession = (record: SessionRecord): AuthSession => {\n if (record.kind !== \"authenticated\" || !record.userId) {\n throw new AuthSessionError(\n \"An authenticated session must contain a user ID.\",\n );\n }\n\n return {\n id: record.id,\n userId: record.userId,\n kind: \"authenticated\",\n createdAt: record.createdAt,\n lastSeenAt: record.lastSeenAt,\n expiresAt: record.expiresAt,\n revokedAt: record.revokedAt,\n };\n};\n\nconst validateRateLimitRule = (name: string, rule: AuthRateLimitRule): void => {\n if (\n !Number.isInteger(rule.limit) ||\n rule.limit < 1 ||\n !Number.isFinite(rule.windowSeconds) ||\n rule.windowSeconds <= 0\n ) {\n throw new AuthConfigurationError(\n `${name} must have a positive integer limit and a positive window.`,\n );\n }\n};\n\nconst validatePassword = (\n password: string,\n minimumLength: number,\n maximumLength: number,\n): void => {\n if (typeof password !== \"string\") {\n throw new AuthValidationError(\"A password is required.\");\n }\n\n const length = Array.from(password).length;\n const byteLength = new TextEncoder().encode(password).byteLength;\n\n if (length < minimumLength) {\n throw new AuthValidationError(\n `Password must contain at least ${minimumLength} characters.`,\n );\n }\n\n if (length > maximumLength || byteLength > 4096) {\n throw new AuthValidationError(\n `Password must contain no more than ${maximumLength} characters.`,\n );\n }\n};\n\nconst validateSignInPassword = (\n password: string,\n maximumLength: number,\n): void => {\n if (typeof password !== \"string\" || password.length === 0) {\n throw new AuthValidationError(\"A password is required.\");\n }\n\n if (\n Array.from(password).length > maximumLength ||\n new TextEncoder().encode(password).byteLength > 4096\n ) {\n throw new AuthValidationError(\"The supplied password is too long.\");\n }\n};\n\nconst validateIpAddress = (ipAddress: string): string => {\n if (\n typeof ipAddress !== \"string\" ||\n ipAddress.trim().length === 0 ||\n ipAddress.trim().length > 128\n ) {\n throw new AuthValidationError(\"A valid client IP address is required.\");\n }\n\n return ipAddress.trim();\n};\n\nexport class StorageAuthManager implements AuthManager {\n private readonly storage: StorageAdapter;\n private readonly passwordHasher: PasswordHasher;\n private readonly rateLimitProvider: RateLimitProvider;\n private readonly sessions: SessionManager;\n private readonly mfa?: AuthMfaGateway;\n private readonly mfaPolicy: AuthMfaPolicy;\n\n private readonly usersCollection: string;\n private readonly credentialsCollection: string;\n private readonly auditCollection: string;\n\n private readonly minimumPasswordLength: number;\n private readonly maximumPasswordLength: number;\n\n private readonly rateLimitPolicies: AuthRateLimitPolicies;\n private readonly rateLimitKeySecret: string;\n private readonly requireVerifiedEmail: boolean;\n private readonly clock: () => Date;\n\n private initializationPromise: Promise<void> | null = null;\n private dummyPasswordHash: PasswordHash | null = null;\n\n constructor(\n dependencies: StorageAuthManagerDependencies,\n options: AuthManagerOptions,\n ) {\n this.storage = dependencies.storage;\n this.passwordHasher = dependencies.passwordHasher;\n this.rateLimitProvider = dependencies.rateLimitProvider;\n this.sessions = dependencies.sessions;\n this.mfa = dependencies.mfa;\n this.mfaPolicy = options.mfaPolicy ?? {};\n\n if (\n typeof options.rateLimitKeySecret !== \"string\" ||\n options.rateLimitKeySecret.length < 32\n ) {\n throw new AuthConfigurationError(\n \"rateLimitKeySecret must contain at least 32 characters.\",\n );\n }\n\n this.rateLimitKeySecret = options.rateLimitKeySecret;\n this.requireVerifiedEmail = options.requireVerifiedEmail ?? false;\n\n this.usersCollection = options.usersCollection ?? AUTH_USER_COLLECTION;\n\n this.credentialsCollection =\n options.credentialsCollection ?? AUTH_CREDENTIAL_COLLECTION;\n\n this.auditCollection = options.auditCollection ?? AUTH_AUDIT_COLLECTION;\n\n this.minimumPasswordLength =\n options.minimumPasswordLength ?? DEFAULT_MINIMUM_PASSWORD_LENGTH;\n\n this.maximumPasswordLength =\n options.maximumPasswordLength ?? DEFAULT_MAXIMUM_PASSWORD_LENGTH;\n\n if (\n !Number.isInteger(this.minimumPasswordLength) ||\n !Number.isInteger(this.maximumPasswordLength) ||\n this.minimumPasswordLength < 1 ||\n this.maximumPasswordLength < this.minimumPasswordLength\n ) {\n throw new AuthConfigurationError(\n \"Password length configuration is invalid.\",\n );\n }\n\n this.rateLimitPolicies = {\n registration: {\n ...DEFAULT_RATE_LIMITS.registration,\n ...options.rateLimits?.registration,\n },\n signIn: {\n ...DEFAULT_RATE_LIMITS.signIn,\n ...options.rateLimits?.signIn,\n },\n };\n\n validateRateLimitRule(\n \"Registration email rate limit\",\n this.rateLimitPolicies.registration.email,\n );\n\n validateRateLimitRule(\n \"Registration IP rate limit\",\n this.rateLimitPolicies.registration.ip,\n );\n\n validateRateLimitRule(\n \"Sign-in email rate limit\",\n this.rateLimitPolicies.signIn.email,\n );\n\n validateRateLimitRule(\n \"Sign-in IP rate limit\",\n this.rateLimitPolicies.signIn.ip,\n );\n\n this.clock = options.clock ?? (() => new Date());\n }\n\n async initialize(): Promise<void> {\n if (!this.initializationPromise) {\n const initialization = (async () => {\n await initializeAuthStorage(this.storage, {\n usersCollection: this.usersCollection,\n credentialsCollection: this.credentialsCollection,\n auditCollection: this.auditCollection,\n });\n\n // MFA storage must be initialized before authentication is exposed.\n // An initialization failure therefore fails closed.\n await this.mfa?.initialize();\n\n this.dummyPasswordHash = await this.hashPassword(\n generateAuthId(\"auth-dummy\"),\n );\n })();\n\n this.initializationPromise = initialization.catch((error) => {\n this.initializationPromise = null;\n throw error;\n });\n }\n\n return this.initializationPromise;\n }\n\n private assertInitialized(): void {\n if (!this.dummyPasswordHash) {\n throw new AuthConfigurationError(\n \"AuthManager must be initialized before it can be used.\",\n );\n }\n }\n\n private now(): Date {\n const now = this.clock();\n\n if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {\n throw new AuthConfigurationError(\n \"The configured clock returned an invalid date.\",\n );\n }\n\n return now;\n }\n\n private createAuditRecord(\n action: AuthAuditAction,\n userId: string | null,\n sessionId: string | null = null,\n metadata?: AuthAuditEventStorageRecord[\"metadata\"],\n ): AuthAuditEventStorageRecord {\n const now = this.now();\n\n return {\n id: generateAuthId(\"auth-audit\"),\n action,\n userId,\n sessionId,\n metadata,\n createdAt: now,\n updatedAt: now,\n revision: 1,\n };\n }\n\n private async recordAudit(\n action: AuthAuditAction,\n userId: string | null,\n sessionId: string | null = null,\n metadata?: AuthAuditEventStorageRecord[\"metadata\"],\n ): Promise<void> {\n try {\n await this.storage.insert<AuthAuditEventStorageRecord>(\n this.auditCollection,\n this.createAuditRecord(action, userId, sessionId, metadata),\n );\n } catch (error) {\n throw new AuthProviderError(\n \"Could not record the authentication audit event.\",\n { cause: error },\n );\n }\n }\n\n private async hashPassword(password: string): Promise<PasswordHash> {\n try {\n const result = await this.passwordHasher.hash(password);\n\n if (\n !result ||\n result.algorithm !== \"argon2id\" ||\n typeof result.hash !== \"string\" ||\n result.hash.length === 0\n ) {\n throw new Error(\"PasswordHasher returned an invalid Argon2id hash.\");\n }\n\n return result;\n } catch (error) {\n throw new AuthProviderError(\"Password hashing failed.\", {\n cause: error,\n });\n }\n }\n\n private async verifyPassword(\n password: string,\n stored: PasswordHash,\n ): Promise<boolean> {\n try {\n return await this.passwordHasher.verify(password, stored);\n } catch (error) {\n throw new AuthProviderError(\"Password verification failed.\", {\n cause: error,\n });\n }\n }\n\n private async enforceRateLimits(\n action: \"registration\" | \"signIn\",\n email: string,\n ipAddress: string,\n ): Promise<void> {\n const policy = this.rateLimitPolicies[action];\n\n const checks = [\n {\n scope: \"email\",\n identifier: email,\n rule: policy.email,\n },\n {\n scope: \"ip\",\n identifier: ipAddress,\n rule: policy.ip,\n },\n ] as const;\n\n for (const check of checks) {\n const key = await createRateLimitKey(\n this.rateLimitKeySecret,\n `auth:${action}:${check.scope}:${check.identifier}`,\n );\n\n let decision: Awaited<ReturnType<RateLimitProvider[\"consume\"]>>;\n\n try {\n decision = await this.rateLimitProvider.consume({\n key,\n limit: check.rule.limit,\n windowSeconds: check.rule.windowSeconds,\n });\n } catch (error) {\n throw new AuthProviderError(\n \"The authentication rate limiter is unavailable.\",\n { cause: error },\n );\n }\n\n if (\n !decision ||\n typeof decision.allowed !== \"boolean\" ||\n !(decision.resetAt instanceof Date) ||\n !Number.isFinite(decision.resetAt.getTime())\n ) {\n throw new AuthProviderError(\n \"The rate limiter returned an invalid response.\",\n );\n }\n\n if (!decision.allowed) {\n throw new AuthRateLimitError(decision.resetAt);\n }\n }\n }\n\n private async findUserByEmail(\n normalizedEmail: string,\n ): Promise<AuthUserStorageRecord | null> {\n return this.storage.findOne<AuthUserStorageRecord>(this.usersCollection, {\n where: {\n normalizedEmail: {\n operator: \"eq\",\n value: normalizedEmail,\n },\n },\n });\n }\n\n private isSessionValidForUser(\n session: SessionRecord,\n user: AuthUserStorageRecord,\n ): boolean {\n if (user.deactivatedAt != null) return false;\n\n if (\n user.sessionsInvalidatedAt &&\n session.createdAt.getTime() <= user.sessionsInvalidatedAt.getTime()\n ) {\n return false;\n }\n\n return true;\n }\n\n private async isMfaRequired(user: AuthUser): Promise<boolean> {\n if (this.mfaPolicy.required === true) {\n return true;\n }\n\n if (this.mfaPolicy.requiredFor) {\n return Boolean(await this.mfaPolicy.requiredFor(user));\n }\n\n return false;\n }\n\n private async hasSatisfiedMfaPolicy(user: AuthUser): Promise<boolean> {\n const required = await this.isMfaRequired(user);\n\n if (!required) {\n return true;\n }\n\n if (!this.mfa) {\n throw new AuthConfigurationError(\n \"MFA is required by policy, but no MFA manager is configured.\",\n );\n }\n\n return this.mfa.hasActiveFactor(user.id);\n }\n\n private async createAuthenticatedSession(\n userId: string,\n metadata?: Readonly<Record<string, unknown>>,\n ): Promise<{\n readonly session: AuthSession;\n readonly token: string;\n }> {\n try {\n const result = await this.sessions.create({\n userId,\n kind: \"authenticated\",\n metadata,\n });\n\n if (\n !result.token ||\n result.session.userId !== userId ||\n result.session.kind !== \"authenticated\"\n ) {\n throw new Error(\n \"SessionManager returned an invalid authenticated session.\",\n );\n }\n\n return {\n session: toPublicSession(result.session),\n token: result.token,\n };\n } catch (error) {\n if (error instanceof AuthSessionError) {\n throw error;\n }\n\n throw new AuthSessionError(\"Could not create an authenticated session.\", {\n cause: error,\n });\n }\n }\n\n async register(input: AuthRegisterInput): Promise<AuthRegisterResult> {\n this.assertInitialized();\n\n const normalizedEmail = normalizeEmail(input.email);\n const name = normalizeDisplayName(input.name);\n const ipAddress = validateIpAddress(input.ipAddress);\n\n validatePassword(\n input.password,\n this.minimumPasswordLength,\n this.maximumPasswordLength,\n );\n\n await this.enforceRateLimits(\"registration\", normalizedEmail, ipAddress);\n\n const passwordHash = await this.hashPassword(input.password);\n const now = this.now();\n\n const userId = generateAuthId(\"user\");\n const credentialId = generateAuthId(\"credential\");\n\n const userRecord: AuthUserStorageRecord = {\n id: userId,\n email: normalizedEmail,\n normalizedEmail,\n emailVerifiedAt: null,\n name,\n image: null,\n deactivatedAt: null,\n sessionsInvalidatedAt: null,\n createdAt: now,\n updatedAt: now,\n revision: 1,\n };\n\n const credentialRecord: PasswordCredentialStorageRecord = {\n id: credentialId,\n userId,\n kind: \"password\",\n passwordHash,\n passwordUpdatedAt: now,\n createdAt: now,\n updatedAt: now,\n revokedAt: null,\n revision: 1,\n };\n\n const publicUser = toPublicUser(userRecord);\n const mfaRequired = await this.isMfaRequired(publicUser);\n\n if (mfaRequired && !this.mfa) {\n throw new AuthConfigurationError(\n \"MFA is required by policy, but no MFA manager is configured.\",\n );\n }\n\n const auditRecord = this.createAuditRecord(\"account-registered\", userId);\n\n const operations: StorageBatchOperation<\n | AuthUserStorageRecord\n | PasswordCredentialStorageRecord\n | AuthAuditEventStorageRecord\n >[] = [\n {\n type: \"insert\",\n collection: this.usersCollection,\n record: userRecord,\n },\n {\n type: \"insert\",\n collection: this.credentialsCollection,\n record: credentialRecord,\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: auditRecord,\n },\n ];\n\n try {\n await this.storage.atomicBatch(operations);\n } catch (error) {\n if (error instanceof StorageDuplicateError) {\n throw new AuthAccountConflictError({ cause: error });\n }\n\n throw error;\n }\n\n if (mfaRequired) {\n // No authenticated session is created here.\n // The user must complete their first MFA enrollment first.\n return this.mfa!.createEnrollmentChallenge({\n userId,\n ipAddress,\n });\n }\n\n const authenticated = await this.createAuthenticatedSession(\n userId,\n input.sessionMetadata,\n );\n\n return {\n user: publicUser,\n session: authenticated.session,\n token: authenticated.token,\n };\n }\n\n async signIn(input: AuthSignInInput): Promise<AuthSignInResult> {\n this.assertInitialized();\n\n const normalizedEmail = normalizeEmail(input.email);\n const ipAddress = validateIpAddress(input.ipAddress);\n\n validateSignInPassword(input.password, this.maximumPasswordLength);\n\n await this.enforceRateLimits(\"signIn\", normalizedEmail, ipAddress);\n\n const userRecord = await this.findUserByEmail(normalizedEmail);\n\n const credential = userRecord\n ? await this.storage.findOne<PasswordCredentialStorageRecord>(\n this.credentialsCollection,\n {\n where: {\n userId: {\n operator: \"eq\",\n value: userRecord.id,\n },\n kind: {\n operator: \"eq\",\n value: \"password\",\n },\n },\n },\n )\n : null;\n\n const activeCredential =\n credential && credential.revokedAt === null ? credential : null;\n\n const passwordHash =\n activeCredential?.passwordHash ?? this.dummyPasswordHash!;\n\n const passwordMatches = await this.verifyPassword(\n input.password,\n passwordHash,\n );\n\n if (!userRecord || !activeCredential || !passwordMatches) {\n await this.recordAudit(\"sign-in-failed\", userRecord?.id ?? null, null, {\n reason: \"invalid-credentials\",\n });\n\n throw new AuthInvalidCredentialsError();\n }\n\n if (userRecord.deactivatedAt != null) {\n await this.recordAudit(\"sign-in-failed\", userRecord.id, null, {\n reason: \"account-deactivated\",\n });\n\n throw new AuthInvalidCredentialsError();\n }\n\n if (this.requireVerifiedEmail && !userRecord.emailVerifiedAt) {\n await this.recordAudit(\"sign-in-failed\", userRecord.id, null, {\n reason: \"email-not-verified\",\n });\n\n throw new AuthEmailVerificationRequiredError();\n }\n\n // CRITICAL:\n // When active factors exist, stop here.\n // Do not create a normal authenticated session yet.\n // When active factors exist, the user must verify one before\n // an authenticated session can be created.\n const publicUser = toPublicUser(userRecord);\n const mfaRequired = await this.isMfaRequired(publicUser);\n\n const mfaPending = await this.mfa?.createSignInChallenge({\n userId: userRecord.id,\n ipAddress,\n });\n\n if (mfaPending) {\n return mfaPending;\n }\n\n if (mfaRequired) {\n if (!this.mfa) {\n throw new AuthConfigurationError(\n \"MFA is required by policy, but no MFA manager is configured.\",\n );\n }\n\n // The password was verified, but no active MFA factor exists.\n // Return an enrollment challenge instead of creating a session.\n return this.mfa.createEnrollmentChallenge({\n userId: userRecord.id,\n ipAddress,\n });\n }\n\n const authenticated = await this.createAuthenticatedSession(\n userRecord.id,\n input.sessionMetadata,\n );\n\n try {\n await this.recordAudit(\n \"sign-in\",\n userRecord.id,\n authenticated.session.id,\n );\n } catch (error) {\n try {\n await this.sessions.revoke(authenticated.session.id);\n } catch (revokeError) {\n throw new AuthSessionError(\n \"Could not revoke a session after an audit failure.\",\n { cause: revokeError },\n );\n }\n\n throw error;\n }\n\n return {\n user: toPublicUser(userRecord),\n session: authenticated.session,\n token: authenticated.token,\n };\n }\n\n private async validateSession(token: string): Promise<SessionRecord | null> {\n if (typeof token !== \"string\" || token.length === 0) {\n return null;\n }\n\n try {\n return await this.sessions.validate(token);\n } catch (error) {\n throw new AuthSessionError(\n \"Could not validate the authentication session.\",\n { cause: error },\n );\n }\n }\n\n async getCurrentUser(token: string): Promise<AuthUser | null> {\n this.assertInitialized();\n\n const session = await this.validateSession(token);\n\n if (!session || session.kind !== \"authenticated\" || !session.userId) {\n return null;\n }\n\n const user = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n session.userId,\n );\n\n if (!user) return null;\n\n if (!this.isSessionValidForUser(session, user)) return null;\n\n if (this.requireVerifiedEmail && !user.emailVerifiedAt) {\n return null;\n }\n\n const publicUser = toPublicUser(user);\n\n if (!(await this.hasSatisfiedMfaPolicy(publicUser))) {\n return null;\n }\n\n return publicUser;\n }\n\n async getAuthState(token: string): Promise<AuthState> {\n this.assertInitialized();\n\n // A temporary MFA token is never treated as an authenticated session.\n const pendingMfa = await this.mfa?.getPendingAuthState(token);\n\n if (pendingMfa) {\n return pendingMfa;\n }\n\n const session = await this.validateSession(token);\n\n if (!session || session.kind !== \"authenticated\" || !session.userId) {\n return { status: \"unauthenticated\" };\n }\n\n const user = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n session.userId,\n );\n\n if (!user) {\n return { status: \"unauthenticated\" };\n }\n\n if (!this.isSessionValidForUser(session, user)) {\n return { status: \"unauthenticated\" };\n }\n\n if (this.requireVerifiedEmail && !user.emailVerifiedAt) {\n return { status: \"unauthenticated\" };\n }\n\n const publicUser = toPublicUser(user);\n\n if (!(await this.hasSatisfiedMfaPolicy(publicUser))) {\n return {\n status: \"mfa-enrollment-required\",\n userId: user.id,\n sessionId: session.id,\n };\n }\n\n return {\n status: \"authenticated\",\n userId: user.id,\n sessionId: session.id,\n };\n }\n\n async signOut(token: string): Promise<void> {\n this.assertInitialized();\n\n const session = await this.validateSession(token);\n\n if (!session) return;\n\n try {\n await this.sessions.revoke(session.id);\n } catch (error) {\n throw new AuthSessionError(\n \"Could not revoke the authentication session.\",\n { cause: error },\n );\n }\n\n await this.recordAudit(\"sign-out\", session.userId, session.id);\n }\n\n async signOutEverywhere(userId: string): Promise<void> {\n this.assertInitialized();\n\n if (typeof userId !== \"string\" || userId.trim().length === 0) {\n throw new AuthValidationError(\"A valid user ID is required.\");\n }\n\n try {\n await this.sessions.revokeAll(userId);\n } catch (error) {\n throw new AuthSessionError(\"Could not revoke the user's sessions.\", {\n cause: error,\n });\n }\n\n try {\n await this.mfa?.revokeChallenges(userId);\n } catch (error) {\n throw new AuthProviderError(\"Could not revoke pending MFA challenges.\", {\n cause: error,\n });\n }\n\n await this.recordAudit(\"sign-out-everywhere\", userId);\n }\n}\n"],"mappings":";;;;;;AAqEA,MAAM,sBAA6C;CACjD,cAAc;EACZ,OAAO;GAAE,OAAO;GAAG,eAAe;EAAK;EACvC,IAAI;GAAE,OAAO;GAAI,eAAe;EAAK;CACvC;CACA,QAAQ;EACN,OAAO;GAAE,OAAO;GAAI,eAAe;EAAI;EACvC,IAAI;GAAE,OAAO;GAAI,eAAe;EAAI;CACtC;AACF;AAEA,MAAM,kCAAkC;AACxC,MAAM,kCAAkC;AAExC,MAAM,gBAAgB,YAA6C;CACjE,IAAI,OAAO;CACX,OAAO,OAAO;CACd,iBAAiB,OAAO;CACxB,MAAM,OAAO;CACb,OAAO,OAAO;CACd,WAAW,OAAO;CAClB,WAAW,OAAO;AACpB;AAEA,MAAM,mBAAmB,WAAuC;CAC9D,IAAI,OAAO,SAAS,mBAAmB,CAAC,OAAO,QAC7C,MAAM,IAAIA,oBAAAA,iBACR,kDACF;CAGF,OAAO;EACL,IAAI,OAAO;EACX,QAAQ,OAAO;EACf,MAAM;EACN,WAAW,OAAO;EAClB,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,WAAW,OAAO;CACpB;AACF;AAEA,MAAM,yBAAyB,MAAc,SAAkC;CAC7E,IACE,CAAC,OAAO,UAAU,KAAK,KAAK,KAC5B,KAAK,QAAQ,KACb,CAAC,OAAO,SAAS,KAAK,aAAa,KACnC,KAAK,iBAAiB,GAEtB,MAAM,IAAIC,oBAAAA,uBACR,GAAG,KAAK,2DACV;AAEJ;AAEA,MAAM,oBACJ,UACA,eACA,kBACS;CACT,IAAI,OAAO,aAAa,UACtB,MAAM,IAAIC,oBAAAA,oBAAoB,yBAAyB;CAGzD,MAAM,SAAS,MAAM,KAAK,QAAQ,CAAC,CAAC;CACpC,MAAM,aAAa,IAAI,YAAY,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC;CAEtD,IAAI,SAAS,eACX,MAAM,IAAIA,oBAAAA,oBACR,kCAAkC,cAAc,aAClD;CAGF,IAAI,SAAS,iBAAiB,aAAa,MACzC,MAAM,IAAIA,oBAAAA,oBACR,sCAAsC,cAAc,aACtD;AAEJ;AAEA,MAAM,0BACJ,UACA,kBACS;CACT,IAAI,OAAO,aAAa,YAAY,SAAS,WAAW,GACtD,MAAM,IAAIA,oBAAAA,oBAAoB,yBAAyB;CAGzD,IACE,MAAM,KAAK,QAAQ,CAAC,CAAC,SAAS,iBAC9B,IAAI,YAAY,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,aAAa,MAEhD,MAAM,IAAIA,oBAAAA,oBAAoB,oCAAoC;AAEtE;AAEA,MAAM,qBAAqB,cAA8B;CACvD,IACE,OAAO,cAAc,YACrB,UAAU,KAAK,CAAC,CAAC,WAAW,KAC5B,UAAU,KAAK,CAAC,CAAC,SAAS,KAE1B,MAAM,IAAIA,oBAAAA,oBAAoB,wCAAwC;CAGxE,OAAO,UAAU,KAAK;AACxB;AAEA,IAAa,qBAAb,MAAuD;CACrD;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CAEA,wBAAsD;CACtD,oBAAiD;CAEjD,YACE,cACA,SACA;EACA,KAAK,UAAU,aAAa;EAC5B,KAAK,iBAAiB,aAAa;EACnC,KAAK,oBAAoB,aAAa;EACtC,KAAK,WAAW,aAAa;EAC7B,KAAK,MAAM,aAAa;EACxB,KAAK,YAAY,QAAQ,aAAa,CAAC;EAEvC,IACE,OAAO,QAAQ,uBAAuB,YACtC,QAAQ,mBAAmB,SAAS,IAEpC,MAAM,IAAID,oBAAAA,uBACR,yDACF;EAGF,KAAK,qBAAqB,QAAQ;EAClC,KAAK,uBAAuB,QAAQ,wBAAwB;EAE5D,KAAK,kBAAkB,QAAQ,mBAAA;EAE/B,KAAK,wBACH,QAAQ,yBAAA;EAEV,KAAK,kBAAkB,QAAQ,mBAAA;EAE/B,KAAK,wBACH,QAAQ,yBAAyB;EAEnC,KAAK,wBACH,QAAQ,yBAAyB;EAEnC,IACE,CAAC,OAAO,UAAU,KAAK,qBAAqB,KAC5C,CAAC,OAAO,UAAU,KAAK,qBAAqB,KAC5C,KAAK,wBAAwB,KAC7B,KAAK,wBAAwB,KAAK,uBAElC,MAAM,IAAIA,oBAAAA,uBACR,2CACF;EAGF,KAAK,oBAAoB;GACvB,cAAc;IACZ,GAAG,oBAAoB;IACvB,GAAG,QAAQ,YAAY;GACzB;GACA,QAAQ;IACN,GAAG,oBAAoB;IACvB,GAAG,QAAQ,YAAY;GACzB;EACF;EAEA,sBACE,iCACA,KAAK,kBAAkB,aAAa,KACtC;EAEA,sBACE,8BACA,KAAK,kBAAkB,aAAa,EACtC;EAEA,sBACE,4BACA,KAAK,kBAAkB,OAAO,KAChC;EAEA,sBACE,yBACA,KAAK,kBAAkB,OAAO,EAChC;EAEA,KAAK,QAAQ,QAAQ,gCAAgB,IAAI,KAAK;CAChD;CAEA,MAAM,aAA4B;EAChC,IAAI,CAAC,KAAK,uBAAuB;GAC/B,MAAM,kBAAkB,YAAY;IAClC,MAAME,qBAAAA,sBAAsB,KAAK,SAAS;KACxC,iBAAiB,KAAK;KACtB,uBAAuB,KAAK;KAC5B,iBAAiB,KAAK;IACxB,CAAC;IAID,MAAM,KAAK,KAAK,WAAW;IAE3B,KAAK,oBAAoB,MAAM,KAAK,aAClCC,0BAAAA,eAAe,YAAY,CAC7B;GACF,EAAA,CAAG;GAEH,KAAK,wBAAwB,eAAe,OAAO,UAAU;IAC3D,KAAK,wBAAwB;IAC7B,MAAM;GACR,CAAC;EACH;EAEA,OAAO,KAAK;CACd;CAEA,oBAAkC;EAChC,IAAI,CAAC,KAAK,mBACR,MAAM,IAAIH,oBAAAA,uBACR,wDACF;CAEJ;CAEA,MAAoB;EAClB,MAAM,MAAM,KAAK,MAAM;EAEvB,IAAI,EAAE,eAAe,SAAS,CAAC,OAAO,SAAS,IAAI,QAAQ,CAAC,GAC1D,MAAM,IAAIA,oBAAAA,uBACR,gDACF;EAGF,OAAO;CACT;CAEA,kBACE,QACA,QACA,YAA2B,MAC3B,UAC6B;EAC7B,MAAM,MAAM,KAAK,IAAI;EAErB,OAAO;GACL,IAAIG,0BAAAA,eAAe,YAAY;GAC/B;GACA;GACA;GACA;GACA,WAAW;GACX,WAAW;GACX,UAAU;EACZ;CACF;CAEA,MAAc,YACZ,QACA,QACA,YAA2B,MAC3B,UACe;EACf,IAAI;GACF,MAAM,KAAK,QAAQ,OACjB,KAAK,iBACL,KAAK,kBAAkB,QAAQ,QAAQ,WAAW,QAAQ,CAC5D;EACF,SAAS,OAAO;GACd,MAAM,IAAIC,oBAAAA,kBACR,oDACA,EAAE,OAAO,MAAM,CACjB;EACF;CACF;CAEA,MAAc,aAAa,UAAyC;EAClE,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,eAAe,KAAK,QAAQ;GAEtD,IACE,CAAC,UACD,OAAO,cAAc,cACrB,OAAO,OAAO,SAAS,YACvB,OAAO,KAAK,WAAW,GAEvB,MAAM,IAAI,MAAM,mDAAmD;GAGrE,OAAO;EACT,SAAS,OAAO;GACd,MAAM,IAAIA,oBAAAA,kBAAkB,4BAA4B,EACtD,OAAO,MACT,CAAC;EACH;CACF;CAEA,MAAc,eACZ,UACA,QACkB;EAClB,IAAI;GACF,OAAO,MAAM,KAAK,eAAe,OAAO,UAAU,MAAM;EAC1D,SAAS,OAAO;GACd,MAAM,IAAIA,oBAAAA,kBAAkB,iCAAiC,EAC3D,OAAO,MACT,CAAC;EACH;CACF;CAEA,MAAc,kBACZ,QACA,OACA,WACe;EACf,MAAM,SAAS,KAAK,kBAAkB;EAEtC,MAAM,SAAS,CACb;GACE,OAAO;GACP,YAAY;GACZ,MAAM,OAAO;EACf,GACA;GACE,OAAO;GACP,YAAY;GACZ,MAAM,OAAO;EACf,CACF;EAEA,KAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,MAAM,MAAMC,0BAAAA,mBAChB,KAAK,oBACL,QAAQ,OAAO,GAAG,MAAM,MAAM,GAAG,MAAM,YACzC;GAEA,IAAI;GAEJ,IAAI;IACF,WAAW,MAAM,KAAK,kBAAkB,QAAQ;KAC9C;KACA,OAAO,MAAM,KAAK;KAClB,eAAe,MAAM,KAAK;IAC5B,CAAC;GACH,SAAS,OAAO;IACd,MAAM,IAAID,oBAAAA,kBACR,mDACA,EAAE,OAAO,MAAM,CACjB;GACF;GAEA,IACE,CAAC,YACD,OAAO,SAAS,YAAY,aAC5B,EAAE,SAAS,mBAAmB,SAC9B,CAAC,OAAO,SAAS,SAAS,QAAQ,QAAQ,CAAC,GAE3C,MAAM,IAAIA,oBAAAA,kBACR,gDACF;GAGF,IAAI,CAAC,SAAS,SACZ,MAAM,IAAIE,oBAAAA,mBAAmB,SAAS,OAAO;EAEjD;CACF;CAEA,MAAc,gBACZ,iBACuC;EACvC,OAAO,KAAK,QAAQ,QAA+B,KAAK,iBAAiB,EACvE,OAAO,EACL,iBAAiB;GACf,UAAU;GACV,OAAO;EACT,EACF,EACF,CAAC;CACH;CAEA,sBACE,SACA,MACS;EACT,IAAI,KAAK,iBAAiB,MAAM,OAAO;EAEvC,IACE,KAAK,yBACL,QAAQ,UAAU,QAAQ,KAAK,KAAK,sBAAsB,QAAQ,GAElE,OAAO;EAGT,OAAO;CACT;CAEA,MAAc,cAAc,MAAkC;EAC5D,IAAI,KAAK,UAAU,aAAa,MAC9B,OAAO;EAGT,IAAI,KAAK,UAAU,aACjB,OAAO,QAAQ,MAAM,KAAK,UAAU,YAAY,IAAI,CAAC;EAGvD,OAAO;CACT;CAEA,MAAc,sBAAsB,MAAkC;EAGpE,IAAI,CAAC,MAFkB,KAAK,cAAc,IAAI,GAG5C,OAAO;EAGT,IAAI,CAAC,KAAK,KACR,MAAM,IAAIN,oBAAAA,uBACR,8DACF;EAGF,OAAO,KAAK,IAAI,gBAAgB,KAAK,EAAE;CACzC;CAEA,MAAc,2BACZ,QACA,UAIC;EACD,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,SAAS,OAAO;IACxC;IACA,MAAM;IACN;GACF,CAAC;GAED,IACE,CAAC,OAAO,SACR,OAAO,QAAQ,WAAW,UAC1B,OAAO,QAAQ,SAAS,iBAExB,MAAM,IAAI,MACR,2DACF;GAGF,OAAO;IACL,SAAS,gBAAgB,OAAO,OAAO;IACvC,OAAO,OAAO;GAChB;EACF,SAAS,OAAO;GACd,IAAI,iBAAiBD,oBAAAA,kBACnB,MAAM;GAGR,MAAM,IAAIA,oBAAAA,iBAAiB,8CAA8C,EACvE,OAAO,MACT,CAAC;EACH;CACF;CAEA,MAAM,SAAS,OAAuD;EACpE,KAAK,kBAAkB;EAEvB,MAAM,kBAAkBQ,oBAAAA,eAAe,MAAM,KAAK;EAClD,MAAM,OAAOC,oBAAAA,qBAAqB,MAAM,IAAI;EAC5C,MAAM,YAAY,kBAAkB,MAAM,SAAS;EAEnD,iBACE,MAAM,UACN,KAAK,uBACL,KAAK,qBACP;EAEA,MAAM,KAAK,kBAAkB,gBAAgB,iBAAiB,SAAS;EAEvE,MAAM,eAAe,MAAM,KAAK,aAAa,MAAM,QAAQ;EAC3D,MAAM,MAAM,KAAK,IAAI;EAErB,MAAM,SAASL,0BAAAA,eAAe,MAAM;EACpC,MAAM,eAAeA,0BAAAA,eAAe,YAAY;EAEhD,MAAM,aAAoC;GACxC,IAAI;GACJ,OAAO;GACP;GACA,iBAAiB;GACjB;GACA,OAAO;GACP,eAAe;GACf,uBAAuB;GACvB,WAAW;GACX,WAAW;GACX,UAAU;EACZ;EAEA,MAAM,mBAAoD;GACxD,IAAI;GACJ;GACA,MAAM;GACN;GACA,mBAAmB;GACnB,WAAW;GACX,WAAW;GACX,WAAW;GACX,UAAU;EACZ;EAEA,MAAM,aAAa,aAAa,UAAU;EAC1C,MAAM,cAAc,MAAM,KAAK,cAAc,UAAU;EAEvD,IAAI,eAAe,CAAC,KAAK,KACvB,MAAM,IAAIH,oBAAAA,uBACR,8DACF;EAGF,MAAM,cAAc,KAAK,kBAAkB,sBAAsB,MAAM;EAEvE,MAAM,aAIA;GACJ;IACE,MAAM;IACN,YAAY,KAAK;IACjB,QAAQ;GACV;GACA;IACE,MAAM;IACN,YAAY,KAAK;IACjB,QAAQ;GACV;GACA;IACE,MAAM;IACN,YAAY,KAAK;IACjB,QAAQ;GACV;EACF;EAEA,IAAI;GACF,MAAM,KAAK,QAAQ,YAAY,UAAU;EAC3C,SAAS,OAAO;GACd,IAAI,iBAAiBS,mBAAAA,uBACnB,MAAM,IAAIC,oBAAAA,yBAAyB,EAAE,OAAO,MAAM,CAAC;GAGrD,MAAM;EACR;EAEA,IAAI,aAGF,OAAO,KAAK,IAAK,0BAA0B;GACzC;GACA;EACF,CAAC;EAGH,MAAM,gBAAgB,MAAM,KAAK,2BAC/B,QACA,MAAM,eACR;EAEA,OAAO;GACL,MAAM;GACN,SAAS,cAAc;GACvB,OAAO,cAAc;EACvB;CACF;CAEA,MAAM,OAAO,OAAmD;EAC9D,KAAK,kBAAkB;EAEvB,MAAM,kBAAkBH,oBAAAA,eAAe,MAAM,KAAK;EAClD,MAAM,YAAY,kBAAkB,MAAM,SAAS;EAEnD,uBAAuB,MAAM,UAAU,KAAK,qBAAqB;EAEjE,MAAM,KAAK,kBAAkB,UAAU,iBAAiB,SAAS;EAEjE,MAAM,aAAa,MAAM,KAAK,gBAAgB,eAAe;EAE7D,MAAM,aAAa,aACf,MAAM,KAAK,QAAQ,QACjB,KAAK,uBACL,EACE,OAAO;GACL,QAAQ;IACN,UAAU;IACV,OAAO,WAAW;GACpB;GACA,MAAM;IACJ,UAAU;IACV,OAAO;GACT;EACF,EACF,CACF,IACA;EAEJ,MAAM,mBACJ,cAAc,WAAW,cAAc,OAAO,aAAa;EAE7D,MAAM,eACJ,kBAAkB,gBAAgB,KAAK;EAEzC,MAAM,kBAAkB,MAAM,KAAK,eACjC,MAAM,UACN,YACF;EAEA,IAAI,CAAC,cAAc,CAAC,oBAAoB,CAAC,iBAAiB;GACxD,MAAM,KAAK,YAAY,kBAAkB,YAAY,MAAM,MAAM,MAAM,EACrE,QAAQ,sBACV,CAAC;GAED,MAAM,IAAII,oBAAAA,4BAA4B;EACxC;EAEA,IAAI,WAAW,iBAAiB,MAAM;GACpC,MAAM,KAAK,YAAY,kBAAkB,WAAW,IAAI,MAAM,EAC5D,QAAQ,sBACV,CAAC;GAED,MAAM,IAAIA,oBAAAA,4BAA4B;EACxC;EAEA,IAAI,KAAK,wBAAwB,CAAC,WAAW,iBAAiB;GAC5D,MAAM,KAAK,YAAY,kBAAkB,WAAW,IAAI,MAAM,EAC5D,QAAQ,qBACV,CAAC;GAED,MAAM,IAAIC,oBAAAA,mCAAmC;EAC/C;EAOA,MAAM,aAAa,aAAa,UAAU;EAC1C,MAAM,cAAc,MAAM,KAAK,cAAc,UAAU;EAEvD,MAAM,aAAa,MAAM,KAAK,KAAK,sBAAsB;GACvD,QAAQ,WAAW;GACnB;EACF,CAAC;EAED,IAAI,YACF,OAAO;EAGT,IAAI,aAAa;GACf,IAAI,CAAC,KAAK,KACR,MAAM,IAAIZ,oBAAAA,uBACR,8DACF;GAKF,OAAO,KAAK,IAAI,0BAA0B;IACxC,QAAQ,WAAW;IACnB;GACF,CAAC;EACH;EAEA,MAAM,gBAAgB,MAAM,KAAK,2BAC/B,WAAW,IACX,MAAM,eACR;EAEA,IAAI;GACF,MAAM,KAAK,YACT,WACA,WAAW,IACX,cAAc,QAAQ,EACxB;EACF,SAAS,OAAO;GACd,IAAI;IACF,MAAM,KAAK,SAAS,OAAO,cAAc,QAAQ,EAAE;GACrD,SAAS,aAAa;IACpB,MAAM,IAAID,oBAAAA,iBACR,sDACA,EAAE,OAAO,YAAY,CACvB;GACF;GAEA,MAAM;EACR;EAEA,OAAO;GACL,MAAM,aAAa,UAAU;GAC7B,SAAS,cAAc;GACvB,OAAO,cAAc;EACvB;CACF;CAEA,MAAc,gBAAgB,OAA8C;EAC1E,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAChD,OAAO;EAGT,IAAI;GACF,OAAO,MAAM,KAAK,SAAS,SAAS,KAAK;EAC3C,SAAS,OAAO;GACd,MAAM,IAAIA,oBAAAA,iBACR,kDACA,EAAE,OAAO,MAAM,CACjB;EACF;CACF;CAEA,MAAM,eAAe,OAAyC;EAC5D,KAAK,kBAAkB;EAEvB,MAAM,UAAU,MAAM,KAAK,gBAAgB,KAAK;EAEhD,IAAI,CAAC,WAAW,QAAQ,SAAS,mBAAmB,CAAC,QAAQ,QAC3D,OAAO;EAGT,MAAM,OAAO,MAAM,KAAK,QAAQ,SAC9B,KAAK,iBACL,QAAQ,MACV;EAEA,IAAI,CAAC,MAAM,OAAO;EAElB,IAAI,CAAC,KAAK,sBAAsB,SAAS,IAAI,GAAG,OAAO;EAEvD,IAAI,KAAK,wBAAwB,CAAC,KAAK,iBACrC,OAAO;EAGT,MAAM,aAAa,aAAa,IAAI;EAEpC,IAAI,CAAE,MAAM,KAAK,sBAAsB,UAAU,GAC/C,OAAO;EAGT,OAAO;CACT;CAEA,MAAM,aAAa,OAAmC;EACpD,KAAK,kBAAkB;EAGvB,MAAM,aAAa,MAAM,KAAK,KAAK,oBAAoB,KAAK;EAE5D,IAAI,YACF,OAAO;EAGT,MAAM,UAAU,MAAM,KAAK,gBAAgB,KAAK;EAEhD,IAAI,CAAC,WAAW,QAAQ,SAAS,mBAAmB,CAAC,QAAQ,QAC3D,OAAO,EAAE,QAAQ,kBAAkB;EAGrC,MAAM,OAAO,MAAM,KAAK,QAAQ,SAC9B,KAAK,iBACL,QAAQ,MACV;EAEA,IAAI,CAAC,MACH,OAAO,EAAE,QAAQ,kBAAkB;EAGrC,IAAI,CAAC,KAAK,sBAAsB,SAAS,IAAI,GAC3C,OAAO,EAAE,QAAQ,kBAAkB;EAGrC,IAAI,KAAK,wBAAwB,CAAC,KAAK,iBACrC,OAAO,EAAE,QAAQ,kBAAkB;EAGrC,MAAM,aAAa,aAAa,IAAI;EAEpC,IAAI,CAAE,MAAM,KAAK,sBAAsB,UAAU,GAC/C,OAAO;GACL,QAAQ;GACR,QAAQ,KAAK;GACb,WAAW,QAAQ;EACrB;EAGF,OAAO;GACL,QAAQ;GACR,QAAQ,KAAK;GACb,WAAW,QAAQ;EACrB;CACF;CAEA,MAAM,QAAQ,OAA8B;EAC1C,KAAK,kBAAkB;EAEvB,MAAM,UAAU,MAAM,KAAK,gBAAgB,KAAK;EAEhD,IAAI,CAAC,SAAS;EAEd,IAAI;GACF,MAAM,KAAK,SAAS,OAAO,QAAQ,EAAE;EACvC,SAAS,OAAO;GACd,MAAM,IAAIA,oBAAAA,iBACR,gDACA,EAAE,OAAO,MAAM,CACjB;EACF;EAEA,MAAM,KAAK,YAAY,YAAY,QAAQ,QAAQ,QAAQ,EAAE;CAC/D;CAEA,MAAM,kBAAkB,QAA+B;EACrD,KAAK,kBAAkB;EAEvB,IAAI,OAAO,WAAW,YAAY,OAAO,KAAK,CAAC,CAAC,WAAW,GACzD,MAAM,IAAIE,oBAAAA,oBAAoB,8BAA8B;EAG9D,IAAI;GACF,MAAM,KAAK,SAAS,UAAU,MAAM;EACtC,SAAS,OAAO;GACd,MAAM,IAAIF,oBAAAA,iBAAiB,yCAAyC,EAClE,OAAO,MACT,CAAC;EACH;EAEA,IAAI;GACF,MAAM,KAAK,KAAK,iBAAiB,MAAM;EACzC,SAAS,OAAO;GACd,MAAM,IAAIK,oBAAAA,kBAAkB,4CAA4C,EACtE,OAAO,MACT,CAAC;EACH;EAEA,MAAM,KAAK,YAAY,uBAAuB,MAAM;CACtD;AACF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PasswordHasher, RateLimitProvider } from "./providers/provider.types.cjs";
|
|
2
|
+
import { AuthManager, AuthManagerOptions, AuthRegisterInput, AuthRegisterResult, AuthSignInInput, AuthSignInResult } from "./types/auth-manager.types.cjs";
|
|
3
|
+
import { AuthState } from "./types/state.types.cjs";
|
|
4
|
+
import { AuthUser } from "./types/user.types.cjs";
|
|
5
|
+
import { AuthMfaGateway } from "./types/mfa.types.cjs";
|
|
6
|
+
import { StorageAdapter } from "@puzzle-ui/storage";
|
|
7
|
+
import { SessionManager } from "@puzzle-ui/session";
|
|
8
|
+
export type StorageAuthManagerDependencies = {
|
|
9
|
+
readonly storage: StorageAdapter;
|
|
10
|
+
readonly passwordHasher: PasswordHasher;
|
|
11
|
+
readonly rateLimitProvider: RateLimitProvider;
|
|
12
|
+
readonly sessions: SessionManager;
|
|
13
|
+
readonly mfa?: AuthMfaGateway;
|
|
14
|
+
};
|
|
15
|
+
export declare class StorageAuthManager implements AuthManager {
|
|
16
|
+
private readonly storage;
|
|
17
|
+
private readonly passwordHasher;
|
|
18
|
+
private readonly rateLimitProvider;
|
|
19
|
+
private readonly sessions;
|
|
20
|
+
private readonly mfa?;
|
|
21
|
+
private readonly mfaPolicy;
|
|
22
|
+
private readonly usersCollection;
|
|
23
|
+
private readonly credentialsCollection;
|
|
24
|
+
private readonly auditCollection;
|
|
25
|
+
private readonly minimumPasswordLength;
|
|
26
|
+
private readonly maximumPasswordLength;
|
|
27
|
+
private readonly rateLimitPolicies;
|
|
28
|
+
private readonly rateLimitKeySecret;
|
|
29
|
+
private readonly requireVerifiedEmail;
|
|
30
|
+
private readonly clock;
|
|
31
|
+
private initializationPromise;
|
|
32
|
+
private dummyPasswordHash;
|
|
33
|
+
constructor(dependencies: StorageAuthManagerDependencies, options: AuthManagerOptions);
|
|
34
|
+
initialize(): Promise<void>;
|
|
35
|
+
private assertInitialized;
|
|
36
|
+
private now;
|
|
37
|
+
private createAuditRecord;
|
|
38
|
+
private recordAudit;
|
|
39
|
+
private hashPassword;
|
|
40
|
+
private verifyPassword;
|
|
41
|
+
private enforceRateLimits;
|
|
42
|
+
private findUserByEmail;
|
|
43
|
+
private isSessionValidForUser;
|
|
44
|
+
private isMfaRequired;
|
|
45
|
+
private hasSatisfiedMfaPolicy;
|
|
46
|
+
private createAuthenticatedSession;
|
|
47
|
+
register(input: AuthRegisterInput): Promise<AuthRegisterResult>;
|
|
48
|
+
signIn(input: AuthSignInInput): Promise<AuthSignInResult>;
|
|
49
|
+
private validateSession;
|
|
50
|
+
getCurrentUser(token: string): Promise<AuthUser | null>;
|
|
51
|
+
getAuthState(token: string): Promise<AuthState>;
|
|
52
|
+
signOut(token: string): Promise<void>;
|
|
53
|
+
signOutEverywhere(userId: string): Promise<void>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PasswordHasher, RateLimitProvider } from "./providers/provider.types.js";
|
|
2
|
+
import { AuthManager, AuthManagerOptions, AuthRegisterInput, AuthRegisterResult, AuthSignInInput, AuthSignInResult } from "./types/auth-manager.types.js";
|
|
3
|
+
import { AuthState } from "./types/state.types.js";
|
|
4
|
+
import { AuthUser } from "./types/user.types.js";
|
|
5
|
+
import { AuthMfaGateway } from "./types/mfa.types.js";
|
|
6
|
+
import { StorageAdapter } from "@puzzle-ui/storage";
|
|
7
|
+
import { SessionManager } from "@puzzle-ui/session";
|
|
8
|
+
export type StorageAuthManagerDependencies = {
|
|
9
|
+
readonly storage: StorageAdapter;
|
|
10
|
+
readonly passwordHasher: PasswordHasher;
|
|
11
|
+
readonly rateLimitProvider: RateLimitProvider;
|
|
12
|
+
readonly sessions: SessionManager;
|
|
13
|
+
readonly mfa?: AuthMfaGateway;
|
|
14
|
+
};
|
|
15
|
+
export declare class StorageAuthManager implements AuthManager {
|
|
16
|
+
private readonly storage;
|
|
17
|
+
private readonly passwordHasher;
|
|
18
|
+
private readonly rateLimitProvider;
|
|
19
|
+
private readonly sessions;
|
|
20
|
+
private readonly mfa?;
|
|
21
|
+
private readonly mfaPolicy;
|
|
22
|
+
private readonly usersCollection;
|
|
23
|
+
private readonly credentialsCollection;
|
|
24
|
+
private readonly auditCollection;
|
|
25
|
+
private readonly minimumPasswordLength;
|
|
26
|
+
private readonly maximumPasswordLength;
|
|
27
|
+
private readonly rateLimitPolicies;
|
|
28
|
+
private readonly rateLimitKeySecret;
|
|
29
|
+
private readonly requireVerifiedEmail;
|
|
30
|
+
private readonly clock;
|
|
31
|
+
private initializationPromise;
|
|
32
|
+
private dummyPasswordHash;
|
|
33
|
+
constructor(dependencies: StorageAuthManagerDependencies, options: AuthManagerOptions);
|
|
34
|
+
initialize(): Promise<void>;
|
|
35
|
+
private assertInitialized;
|
|
36
|
+
private now;
|
|
37
|
+
private createAuditRecord;
|
|
38
|
+
private recordAudit;
|
|
39
|
+
private hashPassword;
|
|
40
|
+
private verifyPassword;
|
|
41
|
+
private enforceRateLimits;
|
|
42
|
+
private findUserByEmail;
|
|
43
|
+
private isSessionValidForUser;
|
|
44
|
+
private isMfaRequired;
|
|
45
|
+
private hasSatisfiedMfaPolicy;
|
|
46
|
+
private createAuthenticatedSession;
|
|
47
|
+
register(input: AuthRegisterInput): Promise<AuthRegisterResult>;
|
|
48
|
+
signIn(input: AuthSignInInput): Promise<AuthSignInResult>;
|
|
49
|
+
private validateSession;
|
|
50
|
+
getCurrentUser(token: string): Promise<AuthUser | null>;
|
|
51
|
+
getAuthState(token: string): Promise<AuthState>;
|
|
52
|
+
signOut(token: string): Promise<void>;
|
|
53
|
+
signOutEverywhere(userId: string): Promise<void>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { initializeAuthStorage } from "./auth.storage.js";
|
|
2
|
+
import { AuthAccountConflictError, AuthConfigurationError, AuthEmailVerificationRequiredError, AuthInvalidCredentialsError, AuthProviderError, AuthRateLimitError, AuthSessionError, AuthValidationError } from "./errors/auth.errors.js";
|
|
3
|
+
import { normalizeDisplayName, normalizeEmail } from "./utils/email.utils.js";
|
|
4
|
+
import { createRateLimitKey, generateAuthId } from "./utils/auth-crypto.utils.js";
|
|
5
|
+
import { StorageDuplicateError } from "@puzzle-ui/storage";
|
|
6
|
+
// src/auth.manager.ts
|
|
7
|
+
const DEFAULT_RATE_LIMITS = {
|
|
8
|
+
registration: {
|
|
9
|
+
email: {
|
|
10
|
+
limit: 5,
|
|
11
|
+
windowSeconds: 3600
|
|
12
|
+
},
|
|
13
|
+
ip: {
|
|
14
|
+
limit: 10,
|
|
15
|
+
windowSeconds: 3600
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
signIn: {
|
|
19
|
+
email: {
|
|
20
|
+
limit: 10,
|
|
21
|
+
windowSeconds: 900
|
|
22
|
+
},
|
|
23
|
+
ip: {
|
|
24
|
+
limit: 30,
|
|
25
|
+
windowSeconds: 900
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const DEFAULT_MINIMUM_PASSWORD_LENGTH = 8;
|
|
30
|
+
const DEFAULT_MAXIMUM_PASSWORD_LENGTH = 1024;
|
|
31
|
+
const toPublicUser = (record) => ({
|
|
32
|
+
id: record.id,
|
|
33
|
+
email: record.email,
|
|
34
|
+
emailVerifiedAt: record.emailVerifiedAt,
|
|
35
|
+
name: record.name,
|
|
36
|
+
image: record.image,
|
|
37
|
+
createdAt: record.createdAt,
|
|
38
|
+
updatedAt: record.updatedAt
|
|
39
|
+
});
|
|
40
|
+
const toPublicSession = (record) => {
|
|
41
|
+
if (record.kind !== "authenticated" || !record.userId) throw new AuthSessionError("An authenticated session must contain a user ID.");
|
|
42
|
+
return {
|
|
43
|
+
id: record.id,
|
|
44
|
+
userId: record.userId,
|
|
45
|
+
kind: "authenticated",
|
|
46
|
+
createdAt: record.createdAt,
|
|
47
|
+
lastSeenAt: record.lastSeenAt,
|
|
48
|
+
expiresAt: record.expiresAt,
|
|
49
|
+
revokedAt: record.revokedAt
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
const validateRateLimitRule = (name, rule) => {
|
|
53
|
+
if (!Number.isInteger(rule.limit) || rule.limit < 1 || !Number.isFinite(rule.windowSeconds) || rule.windowSeconds <= 0) throw new AuthConfigurationError(`${name} must have a positive integer limit and a positive window.`);
|
|
54
|
+
};
|
|
55
|
+
const validatePassword = (password, minimumLength, maximumLength) => {
|
|
56
|
+
if (typeof password !== "string") throw new AuthValidationError("A password is required.");
|
|
57
|
+
const length = Array.from(password).length;
|
|
58
|
+
const byteLength = new TextEncoder().encode(password).byteLength;
|
|
59
|
+
if (length < minimumLength) throw new AuthValidationError(`Password must contain at least ${minimumLength} characters.`);
|
|
60
|
+
if (length > maximumLength || byteLength > 4096) throw new AuthValidationError(`Password must contain no more than ${maximumLength} characters.`);
|
|
61
|
+
};
|
|
62
|
+
const validateSignInPassword = (password, maximumLength) => {
|
|
63
|
+
if (typeof password !== "string" || password.length === 0) throw new AuthValidationError("A password is required.");
|
|
64
|
+
if (Array.from(password).length > maximumLength || new TextEncoder().encode(password).byteLength > 4096) throw new AuthValidationError("The supplied password is too long.");
|
|
65
|
+
};
|
|
66
|
+
const validateIpAddress = (ipAddress) => {
|
|
67
|
+
if (typeof ipAddress !== "string" || ipAddress.trim().length === 0 || ipAddress.trim().length > 128) throw new AuthValidationError("A valid client IP address is required.");
|
|
68
|
+
return ipAddress.trim();
|
|
69
|
+
};
|
|
70
|
+
var StorageAuthManager = class {
|
|
71
|
+
storage;
|
|
72
|
+
passwordHasher;
|
|
73
|
+
rateLimitProvider;
|
|
74
|
+
sessions;
|
|
75
|
+
mfa;
|
|
76
|
+
mfaPolicy;
|
|
77
|
+
usersCollection;
|
|
78
|
+
credentialsCollection;
|
|
79
|
+
auditCollection;
|
|
80
|
+
minimumPasswordLength;
|
|
81
|
+
maximumPasswordLength;
|
|
82
|
+
rateLimitPolicies;
|
|
83
|
+
rateLimitKeySecret;
|
|
84
|
+
requireVerifiedEmail;
|
|
85
|
+
clock;
|
|
86
|
+
initializationPromise = null;
|
|
87
|
+
dummyPasswordHash = null;
|
|
88
|
+
constructor(dependencies, options) {
|
|
89
|
+
this.storage = dependencies.storage;
|
|
90
|
+
this.passwordHasher = dependencies.passwordHasher;
|
|
91
|
+
this.rateLimitProvider = dependencies.rateLimitProvider;
|
|
92
|
+
this.sessions = dependencies.sessions;
|
|
93
|
+
this.mfa = dependencies.mfa;
|
|
94
|
+
this.mfaPolicy = options.mfaPolicy ?? {};
|
|
95
|
+
if (typeof options.rateLimitKeySecret !== "string" || options.rateLimitKeySecret.length < 32) throw new AuthConfigurationError("rateLimitKeySecret must contain at least 32 characters.");
|
|
96
|
+
this.rateLimitKeySecret = options.rateLimitKeySecret;
|
|
97
|
+
this.requireVerifiedEmail = options.requireVerifiedEmail ?? false;
|
|
98
|
+
this.usersCollection = options.usersCollection ?? "auth_users";
|
|
99
|
+
this.credentialsCollection = options.credentialsCollection ?? "auth_credentials";
|
|
100
|
+
this.auditCollection = options.auditCollection ?? "auth_audit_events";
|
|
101
|
+
this.minimumPasswordLength = options.minimumPasswordLength ?? DEFAULT_MINIMUM_PASSWORD_LENGTH;
|
|
102
|
+
this.maximumPasswordLength = options.maximumPasswordLength ?? DEFAULT_MAXIMUM_PASSWORD_LENGTH;
|
|
103
|
+
if (!Number.isInteger(this.minimumPasswordLength) || !Number.isInteger(this.maximumPasswordLength) || this.minimumPasswordLength < 1 || this.maximumPasswordLength < this.minimumPasswordLength) throw new AuthConfigurationError("Password length configuration is invalid.");
|
|
104
|
+
this.rateLimitPolicies = {
|
|
105
|
+
registration: {
|
|
106
|
+
...DEFAULT_RATE_LIMITS.registration,
|
|
107
|
+
...options.rateLimits?.registration
|
|
108
|
+
},
|
|
109
|
+
signIn: {
|
|
110
|
+
...DEFAULT_RATE_LIMITS.signIn,
|
|
111
|
+
...options.rateLimits?.signIn
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
validateRateLimitRule("Registration email rate limit", this.rateLimitPolicies.registration.email);
|
|
115
|
+
validateRateLimitRule("Registration IP rate limit", this.rateLimitPolicies.registration.ip);
|
|
116
|
+
validateRateLimitRule("Sign-in email rate limit", this.rateLimitPolicies.signIn.email);
|
|
117
|
+
validateRateLimitRule("Sign-in IP rate limit", this.rateLimitPolicies.signIn.ip);
|
|
118
|
+
this.clock = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
119
|
+
}
|
|
120
|
+
async initialize() {
|
|
121
|
+
if (!this.initializationPromise) {
|
|
122
|
+
const initialization = (async () => {
|
|
123
|
+
await initializeAuthStorage(this.storage, {
|
|
124
|
+
usersCollection: this.usersCollection,
|
|
125
|
+
credentialsCollection: this.credentialsCollection,
|
|
126
|
+
auditCollection: this.auditCollection
|
|
127
|
+
});
|
|
128
|
+
await this.mfa?.initialize();
|
|
129
|
+
this.dummyPasswordHash = await this.hashPassword(generateAuthId("auth-dummy"));
|
|
130
|
+
})();
|
|
131
|
+
this.initializationPromise = initialization.catch((error) => {
|
|
132
|
+
this.initializationPromise = null;
|
|
133
|
+
throw error;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return this.initializationPromise;
|
|
137
|
+
}
|
|
138
|
+
assertInitialized() {
|
|
139
|
+
if (!this.dummyPasswordHash) throw new AuthConfigurationError("AuthManager must be initialized before it can be used.");
|
|
140
|
+
}
|
|
141
|
+
now() {
|
|
142
|
+
const now = this.clock();
|
|
143
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) throw new AuthConfigurationError("The configured clock returned an invalid date.");
|
|
144
|
+
return now;
|
|
145
|
+
}
|
|
146
|
+
createAuditRecord(action, userId, sessionId = null, metadata) {
|
|
147
|
+
const now = this.now();
|
|
148
|
+
return {
|
|
149
|
+
id: generateAuthId("auth-audit"),
|
|
150
|
+
action,
|
|
151
|
+
userId,
|
|
152
|
+
sessionId,
|
|
153
|
+
metadata,
|
|
154
|
+
createdAt: now,
|
|
155
|
+
updatedAt: now,
|
|
156
|
+
revision: 1
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async recordAudit(action, userId, sessionId = null, metadata) {
|
|
160
|
+
try {
|
|
161
|
+
await this.storage.insert(this.auditCollection, this.createAuditRecord(action, userId, sessionId, metadata));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new AuthProviderError("Could not record the authentication audit event.", { cause: error });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async hashPassword(password) {
|
|
167
|
+
try {
|
|
168
|
+
const result = await this.passwordHasher.hash(password);
|
|
169
|
+
if (!result || result.algorithm !== "argon2id" || typeof result.hash !== "string" || result.hash.length === 0) throw new Error("PasswordHasher returned an invalid Argon2id hash.");
|
|
170
|
+
return result;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new AuthProviderError("Password hashing failed.", { cause: error });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async verifyPassword(password, stored) {
|
|
176
|
+
try {
|
|
177
|
+
return await this.passwordHasher.verify(password, stored);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new AuthProviderError("Password verification failed.", { cause: error });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async enforceRateLimits(action, email, ipAddress) {
|
|
183
|
+
const policy = this.rateLimitPolicies[action];
|
|
184
|
+
const checks = [{
|
|
185
|
+
scope: "email",
|
|
186
|
+
identifier: email,
|
|
187
|
+
rule: policy.email
|
|
188
|
+
}, {
|
|
189
|
+
scope: "ip",
|
|
190
|
+
identifier: ipAddress,
|
|
191
|
+
rule: policy.ip
|
|
192
|
+
}];
|
|
193
|
+
for (const check of checks) {
|
|
194
|
+
const key = await createRateLimitKey(this.rateLimitKeySecret, `auth:${action}:${check.scope}:${check.identifier}`);
|
|
195
|
+
let decision;
|
|
196
|
+
try {
|
|
197
|
+
decision = await this.rateLimitProvider.consume({
|
|
198
|
+
key,
|
|
199
|
+
limit: check.rule.limit,
|
|
200
|
+
windowSeconds: check.rule.windowSeconds
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new AuthProviderError("The authentication rate limiter is unavailable.", { cause: error });
|
|
204
|
+
}
|
|
205
|
+
if (!decision || typeof decision.allowed !== "boolean" || !(decision.resetAt instanceof Date) || !Number.isFinite(decision.resetAt.getTime())) throw new AuthProviderError("The rate limiter returned an invalid response.");
|
|
206
|
+
if (!decision.allowed) throw new AuthRateLimitError(decision.resetAt);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async findUserByEmail(normalizedEmail) {
|
|
210
|
+
return this.storage.findOne(this.usersCollection, { where: { normalizedEmail: {
|
|
211
|
+
operator: "eq",
|
|
212
|
+
value: normalizedEmail
|
|
213
|
+
} } });
|
|
214
|
+
}
|
|
215
|
+
isSessionValidForUser(session, user) {
|
|
216
|
+
if (user.deactivatedAt != null) return false;
|
|
217
|
+
if (user.sessionsInvalidatedAt && session.createdAt.getTime() <= user.sessionsInvalidatedAt.getTime()) return false;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
async isMfaRequired(user) {
|
|
221
|
+
if (this.mfaPolicy.required === true) return true;
|
|
222
|
+
if (this.mfaPolicy.requiredFor) return Boolean(await this.mfaPolicy.requiredFor(user));
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
async hasSatisfiedMfaPolicy(user) {
|
|
226
|
+
if (!await this.isMfaRequired(user)) return true;
|
|
227
|
+
if (!this.mfa) throw new AuthConfigurationError("MFA is required by policy, but no MFA manager is configured.");
|
|
228
|
+
return this.mfa.hasActiveFactor(user.id);
|
|
229
|
+
}
|
|
230
|
+
async createAuthenticatedSession(userId, metadata) {
|
|
231
|
+
try {
|
|
232
|
+
const result = await this.sessions.create({
|
|
233
|
+
userId,
|
|
234
|
+
kind: "authenticated",
|
|
235
|
+
metadata
|
|
236
|
+
});
|
|
237
|
+
if (!result.token || result.session.userId !== userId || result.session.kind !== "authenticated") throw new Error("SessionManager returned an invalid authenticated session.");
|
|
238
|
+
return {
|
|
239
|
+
session: toPublicSession(result.session),
|
|
240
|
+
token: result.token
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof AuthSessionError) throw error;
|
|
244
|
+
throw new AuthSessionError("Could not create an authenticated session.", { cause: error });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async register(input) {
|
|
248
|
+
this.assertInitialized();
|
|
249
|
+
const normalizedEmail = normalizeEmail(input.email);
|
|
250
|
+
const name = normalizeDisplayName(input.name);
|
|
251
|
+
const ipAddress = validateIpAddress(input.ipAddress);
|
|
252
|
+
validatePassword(input.password, this.minimumPasswordLength, this.maximumPasswordLength);
|
|
253
|
+
await this.enforceRateLimits("registration", normalizedEmail, ipAddress);
|
|
254
|
+
const passwordHash = await this.hashPassword(input.password);
|
|
255
|
+
const now = this.now();
|
|
256
|
+
const userId = generateAuthId("user");
|
|
257
|
+
const credentialId = generateAuthId("credential");
|
|
258
|
+
const userRecord = {
|
|
259
|
+
id: userId,
|
|
260
|
+
email: normalizedEmail,
|
|
261
|
+
normalizedEmail,
|
|
262
|
+
emailVerifiedAt: null,
|
|
263
|
+
name,
|
|
264
|
+
image: null,
|
|
265
|
+
deactivatedAt: null,
|
|
266
|
+
sessionsInvalidatedAt: null,
|
|
267
|
+
createdAt: now,
|
|
268
|
+
updatedAt: now,
|
|
269
|
+
revision: 1
|
|
270
|
+
};
|
|
271
|
+
const credentialRecord = {
|
|
272
|
+
id: credentialId,
|
|
273
|
+
userId,
|
|
274
|
+
kind: "password",
|
|
275
|
+
passwordHash,
|
|
276
|
+
passwordUpdatedAt: now,
|
|
277
|
+
createdAt: now,
|
|
278
|
+
updatedAt: now,
|
|
279
|
+
revokedAt: null,
|
|
280
|
+
revision: 1
|
|
281
|
+
};
|
|
282
|
+
const publicUser = toPublicUser(userRecord);
|
|
283
|
+
const mfaRequired = await this.isMfaRequired(publicUser);
|
|
284
|
+
if (mfaRequired && !this.mfa) throw new AuthConfigurationError("MFA is required by policy, but no MFA manager is configured.");
|
|
285
|
+
const auditRecord = this.createAuditRecord("account-registered", userId);
|
|
286
|
+
const operations = [
|
|
287
|
+
{
|
|
288
|
+
type: "insert",
|
|
289
|
+
collection: this.usersCollection,
|
|
290
|
+
record: userRecord
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: "insert",
|
|
294
|
+
collection: this.credentialsCollection,
|
|
295
|
+
record: credentialRecord
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
type: "insert",
|
|
299
|
+
collection: this.auditCollection,
|
|
300
|
+
record: auditRecord
|
|
301
|
+
}
|
|
302
|
+
];
|
|
303
|
+
try {
|
|
304
|
+
await this.storage.atomicBatch(operations);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error instanceof StorageDuplicateError) throw new AuthAccountConflictError({ cause: error });
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
if (mfaRequired) return this.mfa.createEnrollmentChallenge({
|
|
310
|
+
userId,
|
|
311
|
+
ipAddress
|
|
312
|
+
});
|
|
313
|
+
const authenticated = await this.createAuthenticatedSession(userId, input.sessionMetadata);
|
|
314
|
+
return {
|
|
315
|
+
user: publicUser,
|
|
316
|
+
session: authenticated.session,
|
|
317
|
+
token: authenticated.token
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async signIn(input) {
|
|
321
|
+
this.assertInitialized();
|
|
322
|
+
const normalizedEmail = normalizeEmail(input.email);
|
|
323
|
+
const ipAddress = validateIpAddress(input.ipAddress);
|
|
324
|
+
validateSignInPassword(input.password, this.maximumPasswordLength);
|
|
325
|
+
await this.enforceRateLimits("signIn", normalizedEmail, ipAddress);
|
|
326
|
+
const userRecord = await this.findUserByEmail(normalizedEmail);
|
|
327
|
+
const credential = userRecord ? await this.storage.findOne(this.credentialsCollection, { where: {
|
|
328
|
+
userId: {
|
|
329
|
+
operator: "eq",
|
|
330
|
+
value: userRecord.id
|
|
331
|
+
},
|
|
332
|
+
kind: {
|
|
333
|
+
operator: "eq",
|
|
334
|
+
value: "password"
|
|
335
|
+
}
|
|
336
|
+
} }) : null;
|
|
337
|
+
const activeCredential = credential && credential.revokedAt === null ? credential : null;
|
|
338
|
+
const passwordHash = activeCredential?.passwordHash ?? this.dummyPasswordHash;
|
|
339
|
+
const passwordMatches = await this.verifyPassword(input.password, passwordHash);
|
|
340
|
+
if (!userRecord || !activeCredential || !passwordMatches) {
|
|
341
|
+
await this.recordAudit("sign-in-failed", userRecord?.id ?? null, null, { reason: "invalid-credentials" });
|
|
342
|
+
throw new AuthInvalidCredentialsError();
|
|
343
|
+
}
|
|
344
|
+
if (userRecord.deactivatedAt != null) {
|
|
345
|
+
await this.recordAudit("sign-in-failed", userRecord.id, null, { reason: "account-deactivated" });
|
|
346
|
+
throw new AuthInvalidCredentialsError();
|
|
347
|
+
}
|
|
348
|
+
if (this.requireVerifiedEmail && !userRecord.emailVerifiedAt) {
|
|
349
|
+
await this.recordAudit("sign-in-failed", userRecord.id, null, { reason: "email-not-verified" });
|
|
350
|
+
throw new AuthEmailVerificationRequiredError();
|
|
351
|
+
}
|
|
352
|
+
const publicUser = toPublicUser(userRecord);
|
|
353
|
+
const mfaRequired = await this.isMfaRequired(publicUser);
|
|
354
|
+
const mfaPending = await this.mfa?.createSignInChallenge({
|
|
355
|
+
userId: userRecord.id,
|
|
356
|
+
ipAddress
|
|
357
|
+
});
|
|
358
|
+
if (mfaPending) return mfaPending;
|
|
359
|
+
if (mfaRequired) {
|
|
360
|
+
if (!this.mfa) throw new AuthConfigurationError("MFA is required by policy, but no MFA manager is configured.");
|
|
361
|
+
return this.mfa.createEnrollmentChallenge({
|
|
362
|
+
userId: userRecord.id,
|
|
363
|
+
ipAddress
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const authenticated = await this.createAuthenticatedSession(userRecord.id, input.sessionMetadata);
|
|
367
|
+
try {
|
|
368
|
+
await this.recordAudit("sign-in", userRecord.id, authenticated.session.id);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
try {
|
|
371
|
+
await this.sessions.revoke(authenticated.session.id);
|
|
372
|
+
} catch (revokeError) {
|
|
373
|
+
throw new AuthSessionError("Could not revoke a session after an audit failure.", { cause: revokeError });
|
|
374
|
+
}
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
user: toPublicUser(userRecord),
|
|
379
|
+
session: authenticated.session,
|
|
380
|
+
token: authenticated.token
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async validateSession(token) {
|
|
384
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
385
|
+
try {
|
|
386
|
+
return await this.sessions.validate(token);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
throw new AuthSessionError("Could not validate the authentication session.", { cause: error });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async getCurrentUser(token) {
|
|
392
|
+
this.assertInitialized();
|
|
393
|
+
const session = await this.validateSession(token);
|
|
394
|
+
if (!session || session.kind !== "authenticated" || !session.userId) return null;
|
|
395
|
+
const user = await this.storage.findById(this.usersCollection, session.userId);
|
|
396
|
+
if (!user) return null;
|
|
397
|
+
if (!this.isSessionValidForUser(session, user)) return null;
|
|
398
|
+
if (this.requireVerifiedEmail && !user.emailVerifiedAt) return null;
|
|
399
|
+
const publicUser = toPublicUser(user);
|
|
400
|
+
if (!await this.hasSatisfiedMfaPolicy(publicUser)) return null;
|
|
401
|
+
return publicUser;
|
|
402
|
+
}
|
|
403
|
+
async getAuthState(token) {
|
|
404
|
+
this.assertInitialized();
|
|
405
|
+
const pendingMfa = await this.mfa?.getPendingAuthState(token);
|
|
406
|
+
if (pendingMfa) return pendingMfa;
|
|
407
|
+
const session = await this.validateSession(token);
|
|
408
|
+
if (!session || session.kind !== "authenticated" || !session.userId) return { status: "unauthenticated" };
|
|
409
|
+
const user = await this.storage.findById(this.usersCollection, session.userId);
|
|
410
|
+
if (!user) return { status: "unauthenticated" };
|
|
411
|
+
if (!this.isSessionValidForUser(session, user)) return { status: "unauthenticated" };
|
|
412
|
+
if (this.requireVerifiedEmail && !user.emailVerifiedAt) return { status: "unauthenticated" };
|
|
413
|
+
const publicUser = toPublicUser(user);
|
|
414
|
+
if (!await this.hasSatisfiedMfaPolicy(publicUser)) return {
|
|
415
|
+
status: "mfa-enrollment-required",
|
|
416
|
+
userId: user.id,
|
|
417
|
+
sessionId: session.id
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
status: "authenticated",
|
|
421
|
+
userId: user.id,
|
|
422
|
+
sessionId: session.id
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async signOut(token) {
|
|
426
|
+
this.assertInitialized();
|
|
427
|
+
const session = await this.validateSession(token);
|
|
428
|
+
if (!session) return;
|
|
429
|
+
try {
|
|
430
|
+
await this.sessions.revoke(session.id);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw new AuthSessionError("Could not revoke the authentication session.", { cause: error });
|
|
433
|
+
}
|
|
434
|
+
await this.recordAudit("sign-out", session.userId, session.id);
|
|
435
|
+
}
|
|
436
|
+
async signOutEverywhere(userId) {
|
|
437
|
+
this.assertInitialized();
|
|
438
|
+
if (typeof userId !== "string" || userId.trim().length === 0) throw new AuthValidationError("A valid user ID is required.");
|
|
439
|
+
try {
|
|
440
|
+
await this.sessions.revokeAll(userId);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
throw new AuthSessionError("Could not revoke the user's sessions.", { cause: error });
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
await this.mfa?.revokeChallenges(userId);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
throw new AuthProviderError("Could not revoke pending MFA challenges.", { cause: error });
|
|
448
|
+
}
|
|
449
|
+
await this.recordAudit("sign-out-everywhere", userId);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
export { StorageAuthManager };
|
|
453
|
+
|
|
454
|
+
//# sourceMappingURL=auth.manager.js.map
|