@authup/server-core 1.0.0-beta.57 → 1.0.0-beta.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/database/logger/module.mjs +2 -4
- package/dist/adapters/database/logger/module.mjs.map +1 -1
- package/dist/adapters/http/controllers/entities/identity-provider/module.d.ts.map +1 -1
- package/dist/adapters/http/controllers/entities/identity-provider/module.mjs +1 -1
- package/dist/adapters/http/controllers/entities/identity-provider/module.mjs.map +1 -1
- package/dist/adapters/http/middleware/built-in/authorization/module.mjs +2 -1
- package/dist/adapters/http/middleware/built-in/authorization/module.mjs.map +1 -1
- package/dist/adapters/http/middleware/built-in/rate-limit.mjs +2 -2
- package/dist/adapters/http/middleware/built-in/rate-limit.mjs.map +1 -1
- package/dist/adapters/http/request/client-certificate.mjs +1 -1
- package/dist/adapters/http/request/client-certificate.mjs.map +1 -1
- package/dist/adapters/http/request/helpers/locations.mjs +1 -3
- package/dist/adapters/http/request/helpers/locations.mjs.map +1 -1
- package/dist/app/modules/authentication/module.mjs +6 -3
- package/dist/app/modules/authentication/module.mjs.map +1 -1
- package/dist/app/modules/config/helpers.mjs +3 -2
- package/dist/app/modules/config/helpers.mjs.map +1 -1
- package/dist/app/modules/config/read/module.mjs +8 -3
- package/dist/app/modules/config/read/module.mjs.map +1 -1
- package/dist/app/modules/database/module.mjs +2 -1
- package/dist/app/modules/database/module.mjs.map +1 -1
- package/dist/app/modules/http/modules/controller.mjs +197 -126
- package/dist/app/modules/http/modules/controller.mjs.map +1 -1
- package/dist/app/modules/http/modules/middleware.mjs +10 -5
- package/dist/app/modules/http/modules/middleware.mjs.map +1 -1
- package/dist/app/modules/identity/module.mjs +6 -3
- package/dist/app/modules/identity/module.mjs.map +1 -1
- package/dist/app/modules/oauth2/module.mjs +37 -17
- package/dist/app/modules/oauth2/module.mjs.map +1 -1
- package/dist/app/modules/oauth2/repositories/authorize/state/repository.mjs +1 -1
- package/dist/app/modules/oauth2/repositories/authorize/state/repository.mjs.map +1 -1
- package/dist/app/modules/oauth2/repositories/token/repository.mjs +1 -1
- package/dist/app/modules/oauth2/repositories/token/repository.mjs.map +1 -1
- package/dist/app/modules/provisioning/module.mjs +9 -8
- package/dist/app/modules/provisioning/module.mjs.map +1 -1
- package/dist/app/modules/provisioning/sources/file/module.d.ts.map +1 -1
- package/dist/app/modules/provisioning/sources/file/module.mjs +11 -2
- package/dist/app/modules/provisioning/sources/file/module.mjs.map +1 -1
- package/dist/core/authentication/session/module.mjs +2 -4
- package/dist/core/authentication/session/module.mjs.map +1 -1
- package/dist/core/entities/client/service.d.ts.map +1 -1
- package/dist/core/entities/client/service.mjs +1 -1
- package/dist/core/entities/client/service.mjs.map +1 -1
- package/dist/core/entities/client-permission/service.mjs +12 -9
- package/dist/core/entities/client-permission/service.mjs.map +1 -1
- package/dist/core/entities/key/service.mjs +4 -2
- package/dist/core/entities/key/service.mjs.map +1 -1
- package/dist/core/entities/permission/service.d.ts.map +1 -1
- package/dist/core/entities/permission/service.mjs +1 -1
- package/dist/core/entities/permission/service.mjs.map +1 -1
- package/dist/core/entities/policy/service.d.ts.map +1 -1
- package/dist/core/entities/policy/service.mjs +3 -2
- package/dist/core/entities/policy/service.mjs.map +1 -1
- package/dist/core/entities/realm/service.d.ts.map +1 -1
- package/dist/core/entities/realm/service.mjs +1 -1
- package/dist/core/entities/realm/service.mjs.map +1 -1
- package/dist/core/entities/role/service.d.ts.map +1 -1
- package/dist/core/entities/role/service.mjs +1 -1
- package/dist/core/entities/role/service.mjs.map +1 -1
- package/dist/core/entities/role-permission/service.mjs +12 -9
- package/dist/core/entities/role-permission/service.mjs.map +1 -1
- package/dist/core/entities/scope/service.d.ts.map +1 -1
- package/dist/core/entities/scope/service.mjs +1 -1
- package/dist/core/entities/scope/service.mjs.map +1 -1
- package/dist/core/entities/user/service.d.ts.map +1 -1
- package/dist/core/entities/user/service.mjs +1 -1
- package/dist/core/entities/user/service.mjs.map +1 -1
- package/dist/core/entities/user-authenticator/helpers.mjs +1 -1
- package/dist/core/entities/user-authenticator/helpers.mjs.map +1 -1
- package/dist/core/entities/user-authenticator/service.mjs +7 -7
- package/dist/core/entities/user-authenticator/service.mjs.map +1 -1
- package/dist/core/entities/user-permission/service.mjs +12 -9
- package/dist/core/entities/user-permission/service.mjs.map +1 -1
- package/dist/core/identity/password-recovery/service.mjs +1 -1
- package/dist/core/identity/password-recovery/service.mjs.map +1 -1
- package/dist/core/provisioning/synchronizer/client/module.mjs +0 -1
- package/dist/core/provisioning/synchronizer/client/module.mjs.map +1 -1
- package/dist/core/provisioning/synchronizer/permission/module.mjs +0 -1
- package/dist/core/provisioning/synchronizer/permission/module.mjs.map +1 -1
- package/dist/core/provisioning/synchronizer/realm/module.mjs +0 -1
- package/dist/core/provisioning/synchronizer/realm/module.mjs.map +1 -1
- package/dist/core/provisioning/synchronizer/role/module.mjs +0 -1
- package/dist/core/provisioning/synchronizer/role/module.mjs.map +1 -1
- package/dist/core/provisioning/synchronizer/scope/module.mjs +0 -1
- package/dist/core/provisioning/synchronizer/scope/module.mjs.map +1 -1
- package/dist/core/provisioning/synchronizer/user/module.mjs +0 -1
- package/dist/core/provisioning/synchronizer/user/module.mjs.map +1 -1
- package/dist/path.mjs.map +1 -1
- package/dist/swagger.json +435 -11
- package/dist/ui/client/.vite/ssr-manifest.json +172 -10
- package/dist/ui/client/assets/index-BTK3GPZZ.js +106 -0
- package/dist/ui/client/assets/{index-Bk6-OZy9.css → index-Cx_pI7BS.css} +1 -1
- package/dist/ui/client/index.html +2 -2
- package/dist/ui/server/server.js +1894 -5045
- package/package.json +25 -24
- package/dist/ui/client/assets/index-DO6_7NTJ.js +0 -106
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":["randomUUID","BuiltInPolicyType","definePolicyData","ValidatorGroup","eq","inArray","or","EventName","EventRefType","EventScope","IdentityType","PermissionName","UserAuthenticatorKind","UserAuthenticatorValidator","AuthupError","EntityCredentialsInvalidError","EntityNotFoundError","ErrorCode","MfaThrottledError","ValidationError","isMfaThrottledError","AbstractEntityService","buildCacheKey","compare","hash","Secret","TOTP","QRCode","isRealmCipherBlobError","MailTemplateName","buildWebauthnAuthenticationOptions","buildWebauthnRegistrationOptions","verifyWebauthnAuthentication","verifyWebauthnRegistration","USER_AUTHENTICATOR_ATTEMPT_CACHE_PREFIX","USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR","USER_AUTHENTICATOR_ATTEMPT_LOCK_MAX","USER_AUTHENTICATOR_ATTEMPT_WINDOW","USER_AUTHENTICATOR_EMAIL_CODE_CACHE_PREFIX","USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES","USER_AUTHENTICATOR_EMAIL_CODE_LENGTH","USER_AUTHENTICATOR_EMAIL_SEND_CACHE_PREFIX","USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN","USER_AUTHENTICATOR_RECOVERY_CODE_COUNT","USER_AUTHENTICATOR_THROTTLE_CACHE_PREFIX","USER_AUTHENTICATOR_TOTP_ALGORITHM","USER_AUTHENTICATOR_TOTP_DIGITS","USER_AUTHENTICATOR_TOTP_PERIOD","USER_AUTHENTICATOR_VERIFY_LOCK_CACHE_PREFIX","USER_AUTHENTICATOR_VERIFY_LOCK_RENEW_INTERVAL","USER_AUTHENTICATOR_VERIFY_LOCK_TTL","USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX","USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW","USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX","generateNumericCode","generateRecoveryCode","appendQueryConditions","decodeQuery","userAuthenticatorSchema","UserAuthenticatorService","repository","userRepository","cache","cipher","eventService","mailClient","mailTemplateRenderer","options","validator","ctx","isOwnedBy","entity","actor","identity","type","USER","userId","data","id","sanitize","secret","codes","getMany","query","isSelf","permissionEvaluator","preEvaluate","name","USER_AUTHENTICATOR_READ","parsed","schema","findManyOptions","owner","compiled","verdict","compile","ownership","scoped","condition","meta","findMany","map","entities","total","push","evaluate","ATTRIBUTES","resourceRealmMatch","getOne","resolveOne","findOneById","enroll","assertEnabled","validated","run","group","CREATE","user","resolveTargetUser","isSelfEnrollment","kind","EMAIL","enrollTotp","RECOVERY","enrollRecovery","enrollEmail","WEBAUTHN","enrollWebauthn","assertWebauthn","existing","findAllWithSecretsByUser","confirmed","excludeCredentials","device","parseWebauthnParameters","filter","parameters","credential_id","transports","challenge","create","realmId","save","set","buildWebauthnCacheKey","expiresAt","Date","now","ttl","webauthn","assertMail","email","findOneByWithEmail","findAllByUser","length","primary","duplicates","duplicate","remove","recordEvent","MFA_ENROLLED","actorUser","undefined","code","IDENTITY_UNAUTHORIZED","message","target","USER_AUTHENTICATOR_CREATE","size","algorithm","digits","period","totp","issuer","label","uri","toString","encrypt","base32","JSON","stringify","qr","toDataURL","stored","i","used_at","confirm","USER_AUTHENTICATOR_UPDATE","confirmWebauthn","assertNotThrottledOrRetry","withSecrets","findOneWithSecretsById","verifyTotp","bumpAttempts","resetAttempts","output","response","cacheKey","state","get","parse","credential","drop","delete","USER_AUTHENTICATOR_DELETE","entityId","MFA_REMOVED","verify","input","assertNotThrottled","e","lockKey","buildVerifyLockCacheKey","lock","acquireVerifyLock","status","lease","maintainVerifyLock","value","devices","matched","verifyRecovery","verifyEmail","newCounter","verifyWebauthnDevice","counter","recordChallengeEvent","MFA_CHALLENGE_FAILED","renew","onVerified","lastUsedAt","toISOString","buildEmailCodeCacheKey","MFA_VERIFIED","stop","releaseVerifyLock","token","seed","decrypt","fromBase32","delta","validate","trim","window","step","Math","floor","entries","normalized","toLowerCase","entry","result","verified","key","sendChallenge","withinCooldown","add","buildEmailSendCacheKey","retryAfter","codeValue","mail","render","template","MFA_EMAIL_OTP","params","expiresInMinutes","locale","send","to","hasConfirmed","hasConfirmedByUser","enabled","required","enrollmentRequired","kinds","Array","from","Set","issueMaterial","includes","credentials","MFA_NOT_CONFIGURABLE","buildAttemptCacheKey","prefix","buildThrottleCacheKey","owned","pending","queueRenewal","renewIfValue","then","renewed","catch","finally","timer","setInterval","clearInterval","dropIfValue","lockedUntil","ceil","count","increment","delay","min","dropMany","record","scope","IDENTITY","refType","USER_AUTHENTICATOR","refId","actorType","actorId","clientId","requestIpAddress","ipAddress","requestUserAgent","userAgent"],"sources":["../../../../src/core/entities/user-authenticator/service.ts"],"sourcesContent":["/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { BuiltInPolicyType, definePolicyData } from '@authup/access';\nimport { ValidatorGroup } from '@authup/kit';\nimport { eq, inArray, or } from '@rapiq/core';\nimport {\n EventName, \n EventRefType, \n EventScope, \n IdentityType, \n PermissionName, \n UserAuthenticatorKind, \n UserAuthenticatorValidator,\n} from '@authup/core-kit';\nimport type { User, UserAuthenticator } from '@authup/core-kit';\nimport {\n AuthupError,\n EntityCredentialsInvalidError,\n EntityNotFoundError,\n ErrorCode,\n MfaThrottledError,\n ValidationError,\n isMfaThrottledError,\n} from '@authup/errors';\nimport { \n AbstractEntityService, \n buildCacheKey, \n compare, \n hash, \n} from '@authup/server-kit';\nimport type {\n ActorContext,\n EntityRepositoryFindManyResult,\n ICache,\n} from '@authup/server-kit';\nimport { Secret, TOTP } from 'otpauth';\nimport QRCode from 'qrcode';\nimport type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/server';\nimport { isRealmCipherBlobError } from '../../key/index.ts';\nimport type { IRealmCipher } from '../../key/index.ts';\nimport type { IEventService } from '../event/index.ts';\nimport type { IMailClient, IMailTemplateRenderer } from '../../mail/index.ts';\nimport { MailTemplateName } from '../../mail/index.ts';\nimport type { IUserRepository } from '../user/index.ts';\nimport {\n buildWebauthnAuthenticationOptions,\n buildWebauthnRegistrationOptions,\n verifyWebauthnAuthentication,\n verifyWebauthnRegistration,\n} from './webauthn.ts';\nimport {\n USER_AUTHENTICATOR_ATTEMPT_CACHE_PREFIX,\n USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR,\n USER_AUTHENTICATOR_ATTEMPT_LOCK_MAX,\n USER_AUTHENTICATOR_ATTEMPT_WINDOW,\n USER_AUTHENTICATOR_EMAIL_CODE_CACHE_PREFIX,\n USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES,\n USER_AUTHENTICATOR_EMAIL_CODE_LENGTH,\n USER_AUTHENTICATOR_EMAIL_SEND_CACHE_PREFIX,\n USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN,\n USER_AUTHENTICATOR_RECOVERY_CODE_COUNT,\n USER_AUTHENTICATOR_THROTTLE_CACHE_PREFIX,\n USER_AUTHENTICATOR_TOTP_ALGORITHM,\n USER_AUTHENTICATOR_TOTP_DIGITS,\n USER_AUTHENTICATOR_TOTP_PERIOD,\n USER_AUTHENTICATOR_VERIFY_LOCK_CACHE_PREFIX,\n USER_AUTHENTICATOR_VERIFY_LOCK_RENEW_INTERVAL,\n USER_AUTHENTICATOR_VERIFY_LOCK_TTL,\n USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX,\n USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW,\n USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX,\n} from './constants.ts';\nimport { generateNumericCode, generateRecoveryCode } from './helpers.ts';\nimport type {\n IUserAuthenticatorRepository,\n IUserAuthenticatorService,\n UserAuthenticatorChallengeStatus,\n UserAuthenticatorEnrollResult,\n UserAuthenticatorRecoveryCode,\n UserAuthenticatorSendContext,\n UserAuthenticatorServiceContext,\n UserAuthenticatorServiceOptions,\n UserAuthenticatorTotpParameters,\n UserAuthenticatorVerifyContext,\n UserAuthenticatorVerifyInput,\n UserAuthenticatorWebauthnParameters,\n} from './types.ts';\nimport type { WebauthnContext } from './webauthn.ts';\nimport { appendQueryConditions, decodeQuery } from '../../query/index.ts';\nimport { userAuthenticatorSchema } from './schema.ts';\n\ntype EmailCodeState = {\n hash: string,\n expiresAt: number,\n};\n\ntype WebauthnChallengeState = {\n challenge: string,\n expiresAt: number,\n};\n\ntype VerifyLock = {\n status: 'acquired',\n value: string,\n} | {\n status: 'busy' | 'unavailable',\n};\n\ntype VerifyLockLease = {\n renew: () => Promise<boolean>,\n stop: () => Promise<void>,\n};\n\nexport class UserAuthenticatorService extends AbstractEntityService implements IUserAuthenticatorService {\n protected repository: IUserAuthenticatorRepository;\n\n protected userRepository: IUserRepository;\n\n protected cache: ICache;\n\n protected cipher: IRealmCipher;\n\n protected eventService?: IEventService;\n\n protected mailClient?: IMailClient;\n\n protected mailTemplateRenderer?: IMailTemplateRenderer;\n\n protected options: UserAuthenticatorServiceOptions;\n\n protected validator: UserAuthenticatorValidator;\n\n constructor(ctx: UserAuthenticatorServiceContext) {\n super();\n\n this.repository = ctx.repository;\n this.userRepository = ctx.userRepository;\n this.cache = ctx.cache;\n this.cipher = ctx.cipher;\n this.eventService = ctx.eventService;\n this.mailClient = ctx.mailClient;\n this.mailTemplateRenderer = ctx.mailTemplateRenderer;\n this.options = ctx.options ?? {};\n this.validator = new UserAuthenticatorValidator();\n }\n\n protected isOwnedBy(entity: UserAuthenticator, actor: ActorContext): boolean {\n return !!actor.identity &&\n actor.identity.type === IdentityType.USER &&\n entity.userId === actor.identity.data.id;\n }\n\n /**\n * The secret/codes columns never leave the service — reads return the\n * row with both nulled (defense in depth on top of select:false).\n */\n protected sanitize(entity: UserAuthenticator): UserAuthenticator {\n return {\n ...entity,\n secret: null,\n codes: null,\n };\n }\n\n // ------------------------------------------------------------------\n\n async getMany(\n query: Record<string, any>,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<EntityRepositoryFindManyResult<UserAuthenticator>> {\n const isSelf = !!options.userId &&\n !!actor.identity &&\n actor.identity.type === IdentityType.USER &&\n actor.identity.data.id === options.userId;\n\n if (!isSelf) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_READ });\n }\n\n const parsed = await decodeQuery(query, { schema: userAuthenticatorSchema, actor });\n const findManyOptions = options.userId ? { owner: { userId: options.userId } } : {};\n\n // Own devices are ungated: the nested self read is already owner-scoped,\n // and on the wider mounts ownership composes as an OR-alternative with\n // the compiled USER_AUTHENTICATOR_READ condition (#3286 phase 3) — the\n // whole gate runs as WHERE and pagination/totals stay exact. A\n // non-expressible policy falls back to the per-row loop below.\n const compiled = isSelf ?\n { verdict: 'allow' as const } :\n await actor.permissionEvaluator.compile({ name: PermissionName.USER_AUTHENTICATOR_READ });\n if (compiled.verdict !== 'post') {\n const ownership = actor.identity && actor.identity.type === IdentityType.USER ?\n eq('userId', actor.identity.data.id) :\n null;\n\n let scoped = parsed;\n if (compiled.verdict === 'deny') {\n scoped = appendQueryConditions(parsed, ownership ?? inArray('id', []));\n } else if (compiled.verdict === 'conditional') {\n scoped = appendQueryConditions(\n parsed,\n ownership ? or(ownership, compiled.condition) : compiled.condition,\n );\n }\n\n const { data, meta } = await this.repository.findMany(scoped, findManyOptions);\n\n return {\n data: data.map((entity) => this.sanitize(entity)),\n meta,\n };\n }\n\n const { data: entities, meta } = await this.repository.findMany(\n parsed,\n findManyOptions,\n );\n\n const data: UserAuthenticator[] = [];\n let { total } = meta;\n\n for (const entity of entities) {\n if (this.isOwnedBy(entity, actor)) {\n data.push(this.sanitize(entity));\n continue;\n }\n\n try {\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_READ,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n data.push(this.sanitize(entity));\n } catch {\n total -= 1;\n }\n }\n\n return {\n data,\n meta: {\n ...meta,\n total,\n },\n };\n }\n\n async getOne(\n id: string,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<UserAuthenticator> {\n const entity = await this.resolveOne(id, options.userId);\n\n if (!this.isOwnedBy(entity, actor)) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_READ });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_READ,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n }\n\n return this.sanitize(entity);\n }\n\n protected async resolveOne(id: string, userId?: string): Promise<UserAuthenticator> {\n const entity = await this.repository.findOneById(id);\n // a device of another user is a 404 on the nested route — never\n // an existence oracle across users.\n if (!entity || (userId && entity.userId !== userId)) {\n throw new EntityNotFoundError();\n }\n\n return entity;\n }\n\n // ------------------------------------------------------------------\n\n async enroll(data: Record<string, any>, actor: ActorContext): Promise<UserAuthenticatorEnrollResult> {\n this.assertEnabled();\n\n const validated = await this.validator.run(data, { group: ValidatorGroup.CREATE });\n\n const user = await this.resolveTargetUser(validated.userId, actor, validated);\n\n // Only EMAIL may be provisioned FOR another user: its code is mailed to\n // the user's own (already-verified) mailbox, so the enroller never\n // obtains a factor it controls. Every other kind would let an admin plant\n // a second factor they hold, defeating MFA — TOTP/recovery return the\n // seed/codes to the caller, and a WebAuthn ceremony can be completed on\n // the enroller's OWN authenticator (the server can't tell whose device\n // signed). Those are self-enrollment only (Keycloak/Okta/Authentik don't\n // hand a user's factor secret to an admin either); an admin resets another\n // user's MFA by DELETING it, and the user re-enrolls.\n const isSelfEnrollment = !!actor.identity &&\n actor.identity.type === IdentityType.USER &&\n actor.identity.data.id === user.id;\n if (!isSelfEnrollment && validated.kind !== UserAuthenticatorKind.EMAIL) {\n throw new ValidationError(\n `A ${validated.kind} authenticator can only be enrolled by the account owner.`,\n );\n }\n\n switch (validated.kind) {\n case UserAuthenticatorKind.TOTP: {\n return this.enrollTotp(user, validated.name ?? null);\n }\n case UserAuthenticatorKind.RECOVERY: {\n return this.enrollRecovery(user, validated.name ?? null);\n }\n case UserAuthenticatorKind.EMAIL: {\n return this.enrollEmail(user, validated.name ?? null);\n }\n case UserAuthenticatorKind.WEBAUTHN: {\n return this.enrollWebauthn(user, validated.name ?? null);\n }\n default: {\n throw new ValidationError(`The authenticator kind ${validated.kind} can not be enrolled yet.`);\n }\n }\n }\n\n protected async enrollWebauthn(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n const ctx = this.assertWebauthn();\n\n // exclude already-registered credentials from a fresh ceremony\n const existing = await this.repository.findAllWithSecretsByUser(user.id, {\n kind: UserAuthenticatorKind.WEBAUTHN,\n confirmed: true,\n });\n const excludeCredentials = existing\n .map((device) => this.parseWebauthnParameters(device))\n .filter((parameters): parameters is UserAuthenticatorWebauthnParameters => !!parameters)\n .map((parameters) => ({ id: parameters.credential_id, transports: parameters.transports }));\n\n const { options, challenge } = await buildWebauthnRegistrationOptions(\n ctx,\n { id: user.id, name: user.name },\n excludeCredentials,\n );\n\n let entity = this.repository.create({\n kind: UserAuthenticatorKind.WEBAUTHN,\n name,\n confirmed: false,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n\n // Key the challenge by the newly created (unconfirmed) row id, NOT the\n // user id — overlapping enrollment ceremonies (a second tab, a retry\n // after a cancelled prompt) would otherwise overwrite each other's\n // challenge and make the first confirmation fail spuriously.\n await this.cache.set(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX, entity.id),\n {\n challenge,\n expiresAt: Date.now() + (USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000),\n } satisfies WebauthnChallengeState,\n { ttl: USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000 },\n );\n\n return {\n data: this.sanitize(entity),\n meta: { webauthn: options },\n };\n }\n\n protected async enrollEmail(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n this.assertMail();\n\n // The actor identity may not carry `email` (the User entity's email\n // column is select:false) — force-load it to confirm the mailbox\n // exists. The email is presumed verified (activation); the row marks\n // the mailbox as an enrolled factor, codes are transient (cache).\n const email = user.email ?? (await this.userRepository.findOneByWithEmail({ id: user.id }))?.email;\n if (!email) {\n throw new ValidationError('The user has no email address to receive codes.');\n }\n\n // one email factor per user — update the existing marker row in place\n // rather than remove-then-create, so a failed save never leaves the user\n // with no email factor (the row is a pure marker: no secret/codes).\n const existing = (await this.repository.findAllByUser(user.id))\n .filter((device) => device.kind === UserAuthenticatorKind.EMAIL);\n\n let entity: UserAuthenticator;\n if (existing.length > 0) {\n const [primary, ...duplicates] = existing;\n primary.name = name;\n primary.confirmed = true;\n entity = await this.repository.save(primary);\n // best-effort cleanup of any stray duplicates (invariant anomaly);\n // a failure here still leaves the primary factor intact.\n for (const duplicate of duplicates) {\n await this.repository.remove(duplicate);\n }\n } else {\n entity = this.repository.create({\n kind: UserAuthenticatorKind.EMAIL,\n name,\n confirmed: true,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n }\n\n await this.recordEvent(EventName.MFA_ENROLLED, entity);\n\n return { data: this.sanitize(entity), meta: {} };\n }\n\n protected async resolveTargetUser(\n userId: string | undefined,\n actor: ActorContext,\n validated: Record<string, any>,\n ): Promise<User> {\n const actorUser : User | undefined = actor.identity && actor.identity.type === IdentityType.USER ?\n actor.identity.data as User :\n undefined;\n\n if (!userId || (actorUser && actorUser.id === userId)) {\n if (!actorUser) {\n throw new AuthupError({ code: ErrorCode.IDENTITY_UNAUTHORIZED, message: 'Authentication required.' });\n }\n\n return actorUser;\n }\n\n const target = await this.userRepository.findOneById(userId);\n if (!target) {\n throw new EntityNotFoundError();\n }\n\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_CREATE });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_CREATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: { ...validated, realmId: target.realmId },\n ...this.resourceRealmMatch(target),\n }),\n });\n\n return target;\n }\n\n protected async enrollTotp(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n const secret = new Secret({ size: 20 });\n const parameters : UserAuthenticatorTotpParameters = {\n algorithm: USER_AUTHENTICATOR_TOTP_ALGORITHM,\n digits: USER_AUTHENTICATOR_TOTP_DIGITS,\n period: USER_AUTHENTICATOR_TOTP_PERIOD,\n };\n\n const totp = new TOTP({\n issuer: this.options.issuer ?? 'authup',\n label: user.name,\n algorithm: parameters.algorithm,\n digits: parameters.digits,\n period: parameters.period,\n secret,\n });\n const uri = totp.toString();\n\n let entity = this.repository.create({\n kind: UserAuthenticatorKind.TOTP,\n name,\n secret: await this.cipher.encrypt(secret.base32, user.realmId),\n parameters: JSON.stringify(parameters),\n confirmed: false,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n\n return {\n data: this.sanitize(entity),\n meta: {\n secret: secret.base32,\n uri,\n qr: await QRCode.toDataURL(uri),\n },\n };\n }\n\n protected async enrollRecovery(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n const codes : string[] = [];\n const stored : UserAuthenticatorRecoveryCode[] = [];\n for (let i = 0; i < USER_AUTHENTICATOR_RECOVERY_CODE_COUNT; i++) {\n const code = generateRecoveryCode();\n codes.push(code);\n stored.push({ hash: await hash(code), used_at: null });\n }\n\n // regenerate semantics: a user holds ONE recovery-code set. Reuse the\n // existing row via a single atomic save instead of delete-then-insert,\n // so a mid-operation failure can never leave the user with no codes.\n const [existing] = await this.repository.findAllWithSecretsByUser(user.id, { kind: UserAuthenticatorKind.RECOVERY });\n\n let entity : UserAuthenticator;\n if (existing) {\n existing.name = name;\n existing.codes = JSON.stringify(stored);\n existing.confirmed = true;\n entity = await this.repository.save(existing);\n } else {\n entity = this.repository.create({\n kind: UserAuthenticatorKind.RECOVERY,\n name,\n codes: JSON.stringify(stored),\n confirmed: true,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n }\n\n await this.recordEvent(EventName.MFA_ENROLLED, entity);\n\n return {\n data: this.sanitize(entity),\n meta: { codes },\n };\n }\n\n // ------------------------------------------------------------------\n\n async confirm(\n id: string,\n code: string,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<UserAuthenticator> {\n const entity = await this.resolveOne(id, options.userId);\n\n if (!this.isOwnedBy(entity, actor)) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_UPDATE });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_UPDATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n }\n\n if (entity.confirmed) {\n return this.sanitize(entity);\n }\n\n if (entity.kind === UserAuthenticatorKind.WEBAUTHN) {\n return this.confirmWebauthn(entity, code);\n }\n\n if (entity.kind !== UserAuthenticatorKind.TOTP) {\n throw new ValidationError(`The authenticator kind ${entity.kind} can not be confirmed with a code.`);\n }\n\n await this.assertNotThrottledOrRetry(entity.userId);\n\n const withSecrets = await this.repository.findOneWithSecretsById(entity.id);\n if (!withSecrets) {\n throw new EntityNotFoundError();\n }\n\n if (!await this.verifyTotp(withSecrets, code)) {\n await this.bumpAttempts(entity.userId);\n throw new EntityCredentialsInvalidError();\n }\n\n await this.resetAttempts(entity.userId);\n\n entity.confirmed = true;\n // The consumed step is tracked only on the login-verify path (below),\n // NOT here — so a legitimate first login in the same window as the\n // confirmation is still accepted; only a repeat of an already-used\n // LOGIN code is rejected.\n const output = await this.repository.save(entity);\n\n await this.recordEvent(EventName.MFA_ENROLLED, output);\n\n return this.sanitize(output);\n }\n\n protected async confirmWebauthn(entity: UserAuthenticator, response: string): Promise<UserAuthenticator> {\n const ctx = this.assertWebauthn();\n\n // brute-force throttle, same lifecycle as the TOTP confirm branch — the\n // attestation verify is a real crypto call and must not be retryable\n // without backoff.\n await this.assertNotThrottledOrRetry(entity.userId);\n\n // challenge is keyed by the enrollment row id (see enrollWebauthn), so\n // overlapping ceremonies do not collide.\n const cacheKey = this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX, entity.id);\n\n const state = await this.cache.get<WebauthnChallengeState>(cacheKey);\n if (!state || state.expiresAt < Date.now()) {\n await this.bumpAttempts(entity.userId);\n throw new EntityCredentialsInvalidError();\n }\n\n let parsed : RegistrationResponseJSON;\n try {\n parsed = JSON.parse(response) as RegistrationResponseJSON;\n } catch {\n throw new ValidationError('The registration response is malformed.');\n }\n\n const credential = await verifyWebauthnRegistration(ctx, parsed, state.challenge);\n if (!credential) {\n await this.bumpAttempts(entity.userId);\n throw new EntityCredentialsInvalidError();\n }\n\n await this.resetAttempts(entity.userId);\n await this.cache.drop(cacheKey);\n\n entity.parameters = JSON.stringify(credential);\n entity.confirmed = true;\n const output = await this.repository.save(entity);\n\n await this.recordEvent(EventName.MFA_ENROLLED, output);\n\n return this.sanitize(output);\n }\n\n async delete(\n id: string,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<UserAuthenticator> {\n const entity = await this.resolveOne(id, options.userId);\n\n if (!this.isOwnedBy(entity, actor)) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_DELETE });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_DELETE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n }\n\n const { id: entityId } = entity;\n await this.repository.remove(entity);\n entity.id = entityId;\n\n await this.recordEvent(EventName.MFA_REMOVED, entity);\n\n return this.sanitize(entity);\n }\n\n // ------------------------------------------------------------------\n\n async verify(\n userId: string,\n input: UserAuthenticatorVerifyInput,\n ctx: UserAuthenticatorVerifyContext = {},\n ): Promise<boolean> {\n try {\n await this.assertNotThrottled(userId);\n } catch (e) {\n if (isMfaThrottledError(e)) {\n throw e;\n }\n\n return false;\n }\n\n // Serialize the read-verify-save critical section per user so a single\n // factor (TOTP step / recovery / email code / webauthn counter) is\n // consumed exactly once even under concurrent verifies. A held lock\n // means another verify is mid-flight — bail without penalty rather than\n // risk a double-consume; that request owns the outcome.\n const lockKey = this.buildVerifyLockCacheKey(userId);\n const lock = await this.acquireVerifyLock(lockKey);\n if (lock.status !== 'acquired') {\n return false;\n }\n\n const lease = this.maintainVerifyLock(lockKey, lock.value);\n\n try {\n const devices = await this.repository.findAllWithSecretsByUser(userId, {\n kind: input.kind,\n confirmed: true,\n });\n\n let matched : UserAuthenticator | undefined;\n\n for (const device of devices) {\n if (input.kind === UserAuthenticatorKind.TOTP) {\n if (await this.verifyTotp(device, input.response)) {\n matched = device;\n }\n }\n\n if (input.kind === UserAuthenticatorKind.RECOVERY) {\n if (await this.verifyRecovery(device, input.response)) {\n matched = device;\n }\n }\n\n if (input.kind === UserAuthenticatorKind.EMAIL) {\n if (await this.verifyEmail(userId, input.response)) {\n matched = device;\n }\n }\n\n if (input.kind === UserAuthenticatorKind.WEBAUTHN) {\n const newCounter = await this.verifyWebauthnDevice(userId, device, input.response);\n if (newCounter !== null) {\n // persist the signature counter (replay defense)\n const parameters = this.parseWebauthnParameters(device);\n if (parameters) {\n device.parameters = JSON.stringify({ ...parameters, counter: newCounter });\n }\n matched = device;\n }\n }\n\n if (matched) {\n break;\n }\n }\n\n if (!matched) {\n await this.bumpAttempts(userId);\n await this.recordChallengeEvent(EventName.MFA_CHALLENGE_FAILED, userId, input.kind, ctx);\n return false;\n }\n\n if (!await lease.renew()) {\n return false;\n }\n\n // Bind the proof to the caller's aggregate (the session mfaAt\n // stamp) BEFORE any consumption persists: a hook failure aborts\n // the verify with nothing consumed — never a burned single-use\n // code without a completed MFA. The residual (a failure AFTER the\n // hook) leaves a stamped session plus a still-valid code — a\n // bounded server-error window, not a lost factor.\n if (ctx.onVerified) {\n await ctx.onVerified();\n }\n\n if (!await lease.renew()) {\n return false;\n }\n\n matched.lastUsedAt = new Date().toISOString();\n await this.repository.save(matched);\n\n // cache-borne single-use artifacts (email code, webauthn challenge\n // nonce) are consumed only once the unit of work committed — the\n // verify lock keeps the deferral race-free.\n if (input.kind === UserAuthenticatorKind.EMAIL) {\n await this.cache.drop(this.buildEmailCodeCacheKey(userId));\n }\n if (input.kind === UserAuthenticatorKind.WEBAUTHN) {\n await this.cache.drop(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX, userId),\n );\n }\n\n await this.resetAttempts(userId);\n\n await this.recordChallengeEvent(EventName.MFA_VERIFIED, userId, input.kind, ctx, matched);\n return true;\n } finally {\n await lease.stop();\n await this.releaseVerifyLock(lockKey, lock.value);\n }\n }\n\n protected async verifyTotp(device: UserAuthenticator, token: string): Promise<boolean> {\n if (!device.secret) {\n return false;\n }\n\n let seed : string;\n try {\n // key-id-addressed blob; one referencing an unknown or foreign\n // key fails closed as a verification failure, never a 500.\n seed = await this.cipher.decrypt(device.secret, device.realmId);\n } catch (e) {\n // ... but ONLY for blob-semantics failures. Infrastructure errors\n // (database outage, KEK misconfiguration) must bubble — mapping\n // them to `false` would burn throttle attempts on a blip.\n if (isRealmCipherBlobError(e)) {\n return false;\n }\n\n throw e;\n }\n\n let parameters : UserAuthenticatorTotpParameters = {\n algorithm: USER_AUTHENTICATOR_TOTP_ALGORITHM,\n digits: USER_AUTHENTICATOR_TOTP_DIGITS,\n period: USER_AUTHENTICATOR_TOTP_PERIOD,\n };\n if (device.parameters) {\n parameters = { ...parameters, ...JSON.parse(device.parameters) };\n }\n\n const totp = new TOTP({\n algorithm: parameters.algorithm,\n digits: parameters.digits,\n period: parameters.period,\n secret: Secret.fromBase32(seed),\n });\n\n const delta = totp.validate({ token: token.trim(), window: 1 });\n if (delta === null) {\n return false;\n }\n\n // Reject replay: the accepted step must strictly advance past the last\n // consumed one (RFC 6238 §5.2), so a captured code can't be reused\n // within its ±1 validation window. The per-user verify lock makes the\n // concurrent case (two verifies advancing the same step) safe.\n const step = Math.floor(Date.now() / 1000 / parameters.period) + delta;\n if (typeof parameters.counter === 'number' && step <= parameters.counter) {\n return false;\n }\n\n // persist the consumed step onto the device — the caller saves the row.\n device.parameters = JSON.stringify({ ...parameters, counter: step });\n return true;\n }\n\n protected async verifyRecovery(device: UserAuthenticator, code: string): Promise<boolean> {\n if (!device.codes) {\n return false;\n }\n\n const entries = JSON.parse(device.codes) as UserAuthenticatorRecoveryCode[];\n const normalized = code.trim().toLowerCase();\n\n for (const entry of entries) {\n if (entry.used_at) {\n continue;\n }\n\n if (await compare(normalized, entry.hash)) {\n entry.used_at = new Date().toISOString();\n device.codes = JSON.stringify(entries);\n return true;\n }\n }\n\n return false;\n }\n\n protected parseWebauthnParameters(device: UserAuthenticator): UserAuthenticatorWebauthnParameters | null {\n if (!device.parameters) {\n return null;\n }\n try {\n return JSON.parse(device.parameters) as UserAuthenticatorWebauthnParameters;\n } catch {\n return null;\n }\n }\n\n protected async verifyWebauthnDevice(\n userId: string,\n device: UserAuthenticator,\n response: string,\n ): Promise<number | null> {\n const ctx = this.assertWebauthn();\n\n const parameters = this.parseWebauthnParameters(device);\n if (!parameters) {\n return null;\n }\n\n let parsed : AuthenticationResponseJSON;\n try {\n parsed = JSON.parse(response) as AuthenticationResponseJSON;\n } catch {\n return null;\n }\n\n // the assertion identifies the credential — only verify against its row\n if (parsed.id !== parameters.credential_id) {\n return null;\n }\n\n const state = await this.cache.get<WebauthnChallengeState>(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX, userId),\n );\n if (!state || state.expiresAt < Date.now()) {\n return null;\n }\n\n try {\n const result = await verifyWebauthnAuthentication(ctx, parsed, state.challenge, parameters);\n return result.verified ? result.newCounter : null;\n } catch {\n return null;\n }\n }\n\n protected async verifyEmail(userId: string, response: string): Promise<boolean> {\n const key = this.buildEmailCodeCacheKey(userId);\n const state = await this.cache.get<EmailCodeState>(key);\n if (!state || state.expiresAt < Date.now()) {\n return false;\n }\n\n // single-use — but the drop is deferred to the verify success block,\n // AFTER the onVerified hook ran, so a hook failure does not burn the\n // code (the verify lock keeps the deferral race-free).\n return compare(response.trim(), state.hash);\n }\n\n // ------------------------------------------------------------------\n\n async sendChallenge(\n userId: string,\n kind: `${UserAuthenticatorKind}`,\n ctx: UserAuthenticatorSendContext = {},\n ): Promise<void> {\n // Only email needs a server-issued challenge; TOTP/recovery are\n // client-derived (no-op keeps the endpoint uniform).\n if (kind !== UserAuthenticatorKind.EMAIL) {\n return;\n }\n\n this.assertMail();\n await this.assertNotThrottledOrRetry(userId);\n\n // require a CONFIRMED email factor — never mail a code to a user who\n // did not enroll email (no code-spray oracle).\n const devices = await this.repository.findAllWithSecretsByUser(userId, {\n kind: UserAuthenticatorKind.EMAIL,\n confirmed: true,\n });\n if (devices.length === 0) {\n return;\n }\n\n // email column is select:false — force-load it for the recipient.\n const user = await this.userRepository.findOneByWithEmail({ id: userId });\n if (!user || !user.email) {\n return;\n }\n\n // per-user send cooldown — an authenticated caller must not be able to\n // spray unlimited OTP mails. `add` is atomic set-if-absent, so the first\n // send stores the key and any resend within the window returns false.\n // Fails open on a cache error (a transient outage must not block a\n // legitimate code).\n let withinCooldown: boolean;\n try {\n withinCooldown = !(await this.cache.add(\n this.buildEmailSendCacheKey(userId),\n 1,\n { ttl: USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN * 1_000 },\n ));\n } catch {\n withinCooldown = false;\n }\n if (withinCooldown) {\n throw new MfaThrottledError({ retryAfter: USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN });\n }\n\n const codeValue = generateNumericCode(USER_AUTHENTICATOR_EMAIL_CODE_LENGTH);\n await this.cache.set(\n this.buildEmailCodeCacheKey(userId),\n {\n hash: await hash(codeValue),\n expiresAt: Date.now() + (USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES * 60 * 1_000),\n } satisfies EmailCodeState,\n { ttl: USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES * 60 * 1_000 },\n );\n\n const mail = await this.mailTemplateRenderer!.render({\n template: MailTemplateName.MFA_EMAIL_OTP,\n params: {\n code: codeValue,\n expiresInMinutes: USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES,\n },\n locale: ctx.locale,\n });\n\n await this.mailClient!.send({\n to: user.email,\n ...mail,\n });\n }\n\n // ------------------------------------------------------------------\n\n async hasConfirmed(userId: string): Promise<boolean> {\n return this.repository.hasConfirmedByUser(userId);\n }\n\n async challenge(\n userId: string,\n options: { issueMaterial?: boolean } = {},\n ): Promise<UserAuthenticatorChallengeStatus> {\n if (!this.options.enabled) {\n return {\n required: false,\n enrollmentRequired: false,\n kinds: [],\n };\n }\n\n const devices = await this.repository.findAllByUser(userId);\n const confirmed = devices.filter((device) => device.confirmed);\n\n const kinds = Array.from(new Set(confirmed.map((device) => device.kind)));\n\n const status : UserAuthenticatorChallengeStatus = {\n required: confirmed.length > 0,\n enrollmentRequired: !!this.options.required && confirmed.length === 0,\n kinds,\n };\n\n // WebAuthn needs server-issued request options as the challenge — build\n // them (and store the nonce) when the subject holds a webauthn factor.\n // Skipped when the caller only needs the requirement flags (issueMaterial\n // false): the enforcement chokepoints must not rotate an in-flight\n // ceremony's nonce nor run this extra query on every request.\n const issueMaterial = options.issueMaterial ?? true;\n if (issueMaterial && kinds.includes(UserAuthenticatorKind.WEBAUTHN) && this.options.webauthn) {\n const credentials = (await this.repository.findAllWithSecretsByUser(userId, {\n kind: UserAuthenticatorKind.WEBAUTHN,\n confirmed: true,\n }))\n .map((device) => this.parseWebauthnParameters(device))\n .filter((parameters): parameters is UserAuthenticatorWebauthnParameters => !!parameters)\n .map((parameters) => ({ id: parameters.credential_id, transports: parameters.transports }));\n\n const { options, challenge } = await buildWebauthnAuthenticationOptions(this.options.webauthn, credentials);\n\n await this.cache.set(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX, userId),\n {\n challenge,\n expiresAt: Date.now() + (USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000),\n } satisfies WebauthnChallengeState,\n { ttl: USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000 },\n );\n\n status.challenge = { webauthn: options };\n }\n\n return status;\n }\n\n // ------------------------------------------------------------------\n\n protected assertEnabled(): void {\n if (!this.options.enabled) {\n throw new AuthupError({\n code: ErrorCode.MFA_NOT_CONFIGURABLE,\n message: 'Multi-factor authentication is not enabled.',\n });\n }\n }\n\n protected assertMail(): void {\n if (!this.mailClient || !this.mailTemplateRenderer) {\n throw new AuthupError({\n code: ErrorCode.MFA_NOT_CONFIGURABLE,\n message: 'Email-based multi-factor authentication requires a configured mail transport.',\n });\n }\n }\n\n protected assertWebauthn(): WebauthnContext {\n if (!this.options.webauthn) {\n throw new AuthupError({\n code: ErrorCode.MFA_NOT_CONFIGURABLE,\n message: 'WebAuthn requires a configured public URL (relying-party origin).',\n });\n }\n\n return this.options.webauthn;\n }\n\n // ------------------------------------------------------------------\n\n protected buildAttemptCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_ATTEMPT_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildThrottleCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_THROTTLE_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildEmailCodeCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_EMAIL_CODE_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildEmailSendCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_EMAIL_SEND_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildWebauthnCacheKey(prefix: string, userId: string): string {\n return buildCacheKey({ prefix, key: userId });\n }\n\n protected buildVerifyLockCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_VERIFY_LOCK_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected async acquireVerifyLock(key: string): Promise<VerifyLock> {\n const value = randomUUID();\n\n try {\n return (await this.cache.add(key, value, { ttl: USER_AUTHENTICATOR_VERIFY_LOCK_TTL })) ?\n { status: 'acquired', value } :\n { status: 'busy' };\n } catch {\n return { status: 'unavailable' };\n }\n }\n\n protected maintainVerifyLock(key: string, value: string): VerifyLockLease {\n let owned = true;\n let pending : Promise<void> | undefined;\n\n const queueRenewal = () => {\n if (!owned || pending) {\n return pending;\n }\n\n pending = this.cache\n .renewIfValue(key, value, USER_AUTHENTICATOR_VERIFY_LOCK_TTL)\n .then((renewed) => {\n owned = renewed;\n })\n .catch(() => {\n owned = false;\n })\n .finally(() => {\n pending = undefined;\n });\n\n return pending;\n };\n\n const timer = setInterval(queueRenewal, USER_AUTHENTICATOR_VERIFY_LOCK_RENEW_INTERVAL);\n\n return {\n renew: async () => {\n await queueRenewal();\n return owned;\n },\n stop: async () => {\n clearInterval(timer);\n await pending;\n },\n };\n }\n\n protected async releaseVerifyLock(key: string, value: string): Promise<void> {\n try {\n await this.cache.dropIfValue(key, value);\n } catch {\n // Best-effort release; the renewable lock TTL bounds a stranded key.\n }\n }\n\n protected async assertNotThrottled(userId: string): Promise<void> {\n const lockedUntil = await this.cache.get<number>(this.buildThrottleCacheKey(userId));\n if (typeof lockedUntil === 'number' && lockedUntil > Date.now()) {\n throw new MfaThrottledError({ retryAfter: Math.ceil((lockedUntil - Date.now()) / 1_000) });\n }\n }\n\n // Throttle gate for the throwing entry points (confirm / confirmWebauthn /\n // sendChallenge). Mirrors verify()'s fail-closed posture: a genuine lockout\n // surfaces unchanged, while an unreadable throttle counter (cache outage) is\n // treated as a lockout — the caller is told to retry (429) rather than the\n // outage bubbling up as an internal 500.\n protected async assertNotThrottledOrRetry(userId: string): Promise<void> {\n try {\n await this.assertNotThrottled(userId);\n } catch (e) {\n if (isMfaThrottledError(e)) {\n throw e;\n }\n\n throw new MfaThrottledError({ retryAfter: USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR });\n }\n }\n\n protected async bumpAttempts(userId: string): Promise<void> {\n const key = this.buildAttemptCacheKey(userId);\n\n // Atomic increment — concurrent failures (verify OR confirm) each get\n // a distinct count, so the exponential backoff never under-counts.\n let count : number;\n try {\n count = await this.cache.increment(key, 1, { ttl: USER_AUTHENTICATOR_ATTEMPT_WINDOW * 1_000 });\n } catch {\n // a non-numeric value at the key (legacy JSON state) — reset and\n // re-count; a genuine cache outage rethrows from the drop.\n await this.cache.drop(key);\n count = await this.cache.increment(key, 1, { ttl: USER_AUTHENTICATOR_ATTEMPT_WINDOW * 1_000 });\n }\n\n const delay = Math.min(\n USER_AUTHENTICATOR_ATTEMPT_LOCK_MAX,\n USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR * (2 ** (count - 1)),\n );\n\n await this.cache.set(\n this.buildThrottleCacheKey(userId),\n Date.now() + (delay * 1_000),\n { ttl: delay * 1_000 },\n );\n }\n\n protected async resetAttempts(userId: string): Promise<void> {\n await this.cache.dropMany([\n this.buildAttemptCacheKey(userId),\n this.buildThrottleCacheKey(userId),\n ]);\n }\n\n // ------------------------------------------------------------------\n\n protected async recordEvent(\n name: `${EventName}`,\n entity: UserAuthenticator,\n ): Promise<void> {\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name,\n refType: EventRefType.USER_AUTHENTICATOR,\n refId: entity.id,\n actorType: IdentityType.USER,\n actorId: entity.userId,\n realmId: entity.realmId,\n data: { kind: entity.kind },\n });\n }\n\n protected async recordChallengeEvent(\n name: `${EventName}`,\n userId: string,\n kind: string,\n ctx: UserAuthenticatorVerifyContext,\n entity?: UserAuthenticator,\n ): Promise<void> {\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name,\n refType: EventRefType.USER,\n refId: userId,\n actorType: IdentityType.USER,\n actorId: userId,\n clientId: ctx.clientId ?? null,\n realmId: entity?.realmId ?? null,\n requestIpAddress: ctx.ipAddress ?? null,\n requestUserAgent: ctx.userAgent ?? null,\n data: { kind },\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":["randomUUID","BuiltInPolicyType","definePolicyData","ValidatorGroup","eq","inArray","or","EventName","EventRefType","EventScope","IdentityType","PermissionName","UserAuthenticatorKind","UserAuthenticatorValidator","AuthupError","EntityCredentialsInvalidError","EntityNotFoundError","ErrorCode","MfaThrottledError","ValidationError","isMfaThrottledError","AbstractEntityService","buildCacheKey","compare","hash","Secret","TOTP","QRCode","isRealmCipherBlobError","MailTemplateName","buildWebauthnAuthenticationOptions","buildWebauthnRegistrationOptions","verifyWebauthnAuthentication","verifyWebauthnRegistration","USER_AUTHENTICATOR_ATTEMPT_CACHE_PREFIX","USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR","USER_AUTHENTICATOR_ATTEMPT_LOCK_MAX","USER_AUTHENTICATOR_ATTEMPT_WINDOW","USER_AUTHENTICATOR_EMAIL_CODE_CACHE_PREFIX","USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES","USER_AUTHENTICATOR_EMAIL_CODE_LENGTH","USER_AUTHENTICATOR_EMAIL_SEND_CACHE_PREFIX","USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN","USER_AUTHENTICATOR_RECOVERY_CODE_COUNT","USER_AUTHENTICATOR_THROTTLE_CACHE_PREFIX","USER_AUTHENTICATOR_TOTP_ALGORITHM","USER_AUTHENTICATOR_TOTP_DIGITS","USER_AUTHENTICATOR_TOTP_PERIOD","USER_AUTHENTICATOR_VERIFY_LOCK_CACHE_PREFIX","USER_AUTHENTICATOR_VERIFY_LOCK_RENEW_INTERVAL","USER_AUTHENTICATOR_VERIFY_LOCK_TTL","USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX","USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW","USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX","generateNumericCode","generateRecoveryCode","appendQueryConditions","decodeQuery","userAuthenticatorSchema","UserAuthenticatorService","repository","userRepository","cache","cipher","eventService","mailClient","mailTemplateRenderer","options","validator","ctx","isOwnedBy","entity","actor","identity","type","USER","userId","data","id","sanitize","secret","codes","getMany","query","isSelf","permissionEvaluator","preEvaluate","name","USER_AUTHENTICATOR_READ","parsed","schema","findManyOptions","owner","compiled","verdict","compile","ownership","scoped","condition","meta","findMany","map","entities","total","push","evaluate","ATTRIBUTES","resourceRealmMatch","getOne","resolveOne","findOneById","enroll","assertEnabled","validated","run","group","CREATE","user","resolveTargetUser","isSelfEnrollment","kind","EMAIL","enrollTotp","RECOVERY","enrollRecovery","enrollEmail","WEBAUTHN","enrollWebauthn","assertWebauthn","existing","findAllWithSecretsByUser","confirmed","excludeCredentials","device","parseWebauthnParameters","filter","parameters","credential_id","transports","challenge","create","realmId","save","set","buildWebauthnCacheKey","expiresAt","Date","now","ttl","webauthn","assertMail","email","findOneByWithEmail","findAllByUser","length","primary","duplicates","duplicate","remove","recordEvent","MFA_ENROLLED","actorUser","undefined","code","IDENTITY_UNAUTHORIZED","message","target","USER_AUTHENTICATOR_CREATE","size","algorithm","digits","period","totp","issuer","label","uri","toString","encrypt","base32","JSON","stringify","qr","toDataURL","stored","i","used_at","confirm","USER_AUTHENTICATOR_UPDATE","confirmWebauthn","assertNotThrottledOrRetry","withSecrets","findOneWithSecretsById","verifyTotp","bumpAttempts","resetAttempts","output","response","cacheKey","state","get","parse","credential","drop","delete","USER_AUTHENTICATOR_DELETE","entityId","MFA_REMOVED","verify","input","assertNotThrottled","e","lockKey","buildVerifyLockCacheKey","lock","acquireVerifyLock","status","lease","maintainVerifyLock","value","devices","matched","verifyRecovery","verifyEmail","newCounter","verifyWebauthnDevice","counter","recordChallengeEvent","MFA_CHALLENGE_FAILED","renew","onVerified","lastUsedAt","toISOString","buildEmailCodeCacheKey","MFA_VERIFIED","stop","releaseVerifyLock","token","seed","decrypt","fromBase32","delta","validate","trim","window","step","Math","floor","entries","normalized","toLowerCase","entry","result","verified","key","sendChallenge","withinCooldown","add","buildEmailSendCacheKey","retryAfter","codeValue","mail","render","template","MFA_EMAIL_OTP","params","expiresInMinutes","locale","send","to","hasConfirmed","hasConfirmedByUser","enabled","required","enrollmentRequired","kinds","Array","from","Set","issueMaterial","includes","credentials","MFA_NOT_CONFIGURABLE","buildAttemptCacheKey","prefix","buildThrottleCacheKey","owned","pending","queueRenewal","renewIfValue","then","renewed","catch","finally","timer","setInterval","clearInterval","dropIfValue","lockedUntil","ceil","count","increment","delay","min","dropMany","record","scope","IDENTITY","refType","USER_AUTHENTICATOR","refId","actorType","actorId","clientId","requestIpAddress","ipAddress","requestUserAgent","userAgent"],"sources":["../../../../src/core/entities/user-authenticator/service.ts"],"sourcesContent":["/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { BuiltInPolicyType, definePolicyData } from '@authup/access';\nimport { ValidatorGroup } from '@authup/kit';\nimport { eq, inArray, or } from '@rapiq/core';\nimport {\n EventName, \n EventRefType, \n EventScope, \n IdentityType, \n PermissionName, \n UserAuthenticatorKind, \n UserAuthenticatorValidator,\n} from '@authup/core-kit';\nimport type { User, UserAuthenticator } from '@authup/core-kit';\nimport {\n AuthupError,\n EntityCredentialsInvalidError,\n EntityNotFoundError,\n ErrorCode,\n MfaThrottledError,\n ValidationError,\n isMfaThrottledError,\n} from '@authup/errors';\nimport { \n AbstractEntityService, \n buildCacheKey, \n compare, \n hash, \n} from '@authup/server-kit';\nimport type {\n ActorContext,\n EntityRepositoryFindManyResult,\n ICache,\n} from '@authup/server-kit';\nimport { Secret, TOTP } from 'otpauth';\nimport QRCode from 'qrcode';\nimport type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/server';\nimport { isRealmCipherBlobError } from '../../key/index.ts';\nimport type { IRealmCipher } from '../../key/index.ts';\nimport type { IEventService } from '../event/index.ts';\nimport type { IMailClient, IMailTemplateRenderer } from '../../mail/index.ts';\nimport { MailTemplateName } from '../../mail/index.ts';\nimport type { IUserRepository } from '../user/index.ts';\nimport {\n buildWebauthnAuthenticationOptions,\n buildWebauthnRegistrationOptions,\n verifyWebauthnAuthentication,\n verifyWebauthnRegistration,\n} from './webauthn.ts';\nimport {\n USER_AUTHENTICATOR_ATTEMPT_CACHE_PREFIX,\n USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR,\n USER_AUTHENTICATOR_ATTEMPT_LOCK_MAX,\n USER_AUTHENTICATOR_ATTEMPT_WINDOW,\n USER_AUTHENTICATOR_EMAIL_CODE_CACHE_PREFIX,\n USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES,\n USER_AUTHENTICATOR_EMAIL_CODE_LENGTH,\n USER_AUTHENTICATOR_EMAIL_SEND_CACHE_PREFIX,\n USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN,\n USER_AUTHENTICATOR_RECOVERY_CODE_COUNT,\n USER_AUTHENTICATOR_THROTTLE_CACHE_PREFIX,\n USER_AUTHENTICATOR_TOTP_ALGORITHM,\n USER_AUTHENTICATOR_TOTP_DIGITS,\n USER_AUTHENTICATOR_TOTP_PERIOD,\n USER_AUTHENTICATOR_VERIFY_LOCK_CACHE_PREFIX,\n USER_AUTHENTICATOR_VERIFY_LOCK_RENEW_INTERVAL,\n USER_AUTHENTICATOR_VERIFY_LOCK_TTL,\n USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX,\n USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW,\n USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX,\n} from './constants.ts';\nimport { generateNumericCode, generateRecoveryCode } from './helpers.ts';\nimport type {\n IUserAuthenticatorRepository,\n IUserAuthenticatorService,\n UserAuthenticatorChallengeStatus,\n UserAuthenticatorEnrollResult,\n UserAuthenticatorRecoveryCode,\n UserAuthenticatorSendContext,\n UserAuthenticatorServiceContext,\n UserAuthenticatorServiceOptions,\n UserAuthenticatorTotpParameters,\n UserAuthenticatorVerifyContext,\n UserAuthenticatorVerifyInput,\n UserAuthenticatorWebauthnParameters,\n} from './types.ts';\nimport type { WebauthnContext } from './webauthn.ts';\nimport { appendQueryConditions, decodeQuery } from '../../query/index.ts';\nimport { userAuthenticatorSchema } from './schema.ts';\n\ntype EmailCodeState = {\n hash: string,\n expiresAt: number,\n};\n\ntype WebauthnChallengeState = {\n challenge: string,\n expiresAt: number,\n};\n\ntype VerifyLock = {\n status: 'acquired',\n value: string,\n} | {\n status: 'busy' | 'unavailable',\n};\n\ntype VerifyLockLease = {\n renew: () => Promise<boolean>,\n stop: () => Promise<void>,\n};\n\nexport class UserAuthenticatorService extends AbstractEntityService implements IUserAuthenticatorService {\n protected repository: IUserAuthenticatorRepository;\n\n protected userRepository: IUserRepository;\n\n protected cache: ICache;\n\n protected cipher: IRealmCipher;\n\n protected eventService?: IEventService;\n\n protected mailClient?: IMailClient;\n\n protected mailTemplateRenderer?: IMailTemplateRenderer;\n\n protected options: UserAuthenticatorServiceOptions;\n\n protected validator: UserAuthenticatorValidator;\n\n constructor(ctx: UserAuthenticatorServiceContext) {\n super();\n\n this.repository = ctx.repository;\n this.userRepository = ctx.userRepository;\n this.cache = ctx.cache;\n this.cipher = ctx.cipher;\n this.eventService = ctx.eventService;\n this.mailClient = ctx.mailClient;\n this.mailTemplateRenderer = ctx.mailTemplateRenderer;\n this.options = ctx.options ?? {};\n this.validator = new UserAuthenticatorValidator();\n }\n\n protected isOwnedBy(entity: UserAuthenticator, actor: ActorContext): boolean {\n return !!actor.identity &&\n actor.identity.type === IdentityType.USER &&\n entity.userId === actor.identity.data.id;\n }\n\n /**\n * The secret/codes columns never leave the service — reads return the\n * row with both nulled (defense in depth on top of select:false).\n */\n protected sanitize(entity: UserAuthenticator): UserAuthenticator {\n return {\n ...entity,\n secret: null,\n codes: null,\n };\n }\n\n // ------------------------------------------------------------------\n\n async getMany(\n query: Record<string, any>,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<EntityRepositoryFindManyResult<UserAuthenticator>> {\n const isSelf = !!options.userId &&\n !!actor.identity &&\n actor.identity.type === IdentityType.USER &&\n actor.identity.data.id === options.userId;\n\n if (!isSelf) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_READ });\n }\n\n const parsed = await decodeQuery(query, { schema: userAuthenticatorSchema, actor });\n const findManyOptions = options.userId ? { owner: { userId: options.userId } } : {};\n\n // Own devices are ungated: the nested self read is already owner-scoped,\n // and on the wider mounts ownership composes as an OR-alternative with\n // the compiled USER_AUTHENTICATOR_READ condition (#3286 phase 3) — the\n // whole gate runs as WHERE and pagination/totals stay exact. A\n // non-expressible policy falls back to the per-row loop below.\n const compiled = isSelf ?\n { verdict: 'allow' as const } :\n await actor.permissionEvaluator.compile({ name: PermissionName.USER_AUTHENTICATOR_READ });\n if (compiled.verdict !== 'post') {\n const ownership = actor.identity && actor.identity.type === IdentityType.USER ?\n eq('userId', actor.identity.data.id) :\n null;\n\n let scoped = parsed;\n if (compiled.verdict === 'deny') {\n scoped = appendQueryConditions(parsed, ownership ?? inArray('id', []));\n } else if (compiled.verdict === 'conditional') {\n scoped = appendQueryConditions(\n parsed,\n ownership ? or(ownership, compiled.condition) : compiled.condition,\n );\n }\n\n const { data, meta } = await this.repository.findMany(scoped, findManyOptions);\n\n return {\n data: data.map((entity) => this.sanitize(entity)),\n meta,\n };\n }\n\n const { data: entities, meta } = await this.repository.findMany(\n parsed,\n findManyOptions,\n );\n\n const data: UserAuthenticator[] = [];\n let { total } = meta;\n\n for (const entity of entities) {\n if (this.isOwnedBy(entity, actor)) {\n data.push(this.sanitize(entity));\n continue;\n }\n\n try {\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_READ,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n data.push(this.sanitize(entity));\n } catch {\n total -= 1;\n }\n }\n\n return {\n data,\n meta: {\n ...meta,\n total,\n },\n };\n }\n\n async getOne(\n id: string,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<UserAuthenticator> {\n const entity = await this.resolveOne(id, options.userId);\n\n if (!this.isOwnedBy(entity, actor)) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_READ });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_READ,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n }\n\n return this.sanitize(entity);\n }\n\n protected async resolveOne(id: string, userId?: string): Promise<UserAuthenticator> {\n const entity = await this.repository.findOneById(id);\n // a device of another user is a 404 on the nested route — never\n // an existence oracle across users.\n if (!entity || (userId && entity.userId !== userId)) {\n throw new EntityNotFoundError();\n }\n\n return entity;\n }\n\n // ------------------------------------------------------------------\n\n async enroll(data: Record<string, any>, actor: ActorContext): Promise<UserAuthenticatorEnrollResult> {\n this.assertEnabled();\n\n const validated = await this.validator.run(data, { group: ValidatorGroup.CREATE });\n\n const user = await this.resolveTargetUser(validated.userId, actor, validated);\n\n // Only EMAIL may be provisioned FOR another user: its code is mailed to\n // the user's own (already-verified) mailbox, so the enroller never\n // obtains a factor it controls. Every other kind would let an admin plant\n // a second factor they hold, defeating MFA — TOTP/recovery return the\n // seed/codes to the caller, and a WebAuthn ceremony can be completed on\n // the enroller's OWN authenticator (the server can't tell whose device\n // signed). Those are self-enrollment only (Keycloak/Okta/Authentik don't\n // hand a user's factor secret to an admin either); an admin resets another\n // user's MFA by DELETING it, and the user re-enrolls.\n const isSelfEnrollment = !!actor.identity &&\n actor.identity.type === IdentityType.USER &&\n actor.identity.data.id === user.id;\n if (!isSelfEnrollment && validated.kind !== UserAuthenticatorKind.EMAIL) {\n throw new ValidationError(\n `A ${validated.kind} authenticator can only be enrolled by the account owner.`,\n );\n }\n\n switch (validated.kind) {\n case UserAuthenticatorKind.TOTP: {\n return this.enrollTotp(user, validated.name ?? null);\n }\n case UserAuthenticatorKind.RECOVERY: {\n return this.enrollRecovery(user, validated.name ?? null);\n }\n case UserAuthenticatorKind.EMAIL: {\n return this.enrollEmail(user, validated.name ?? null);\n }\n case UserAuthenticatorKind.WEBAUTHN: {\n return this.enrollWebauthn(user, validated.name ?? null);\n }\n default: {\n throw new ValidationError(`The authenticator kind ${validated.kind} can not be enrolled yet.`);\n }\n }\n }\n\n protected async enrollWebauthn(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n const ctx = this.assertWebauthn();\n\n // exclude already-registered credentials from a fresh ceremony\n const existing = await this.repository.findAllWithSecretsByUser(user.id, {\n kind: UserAuthenticatorKind.WEBAUTHN,\n confirmed: true,\n });\n const excludeCredentials = existing\n .map((device) => this.parseWebauthnParameters(device))\n .filter((parameters): parameters is UserAuthenticatorWebauthnParameters => !!parameters)\n .map((parameters) => ({ id: parameters.credential_id, transports: parameters.transports }));\n\n const { options, challenge } = await buildWebauthnRegistrationOptions(\n ctx,\n { id: user.id, name: user.name },\n excludeCredentials,\n );\n\n let entity = this.repository.create({\n kind: UserAuthenticatorKind.WEBAUTHN,\n name,\n confirmed: false,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n\n // Key the challenge by the newly created (unconfirmed) row id, NOT the\n // user id — overlapping enrollment ceremonies (a second tab, a retry\n // after a cancelled prompt) would otherwise overwrite each other's\n // challenge and make the first confirmation fail spuriously.\n await this.cache.set(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX, entity.id),\n {\n challenge,\n expiresAt: Date.now() + (USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000),\n } satisfies WebauthnChallengeState,\n { ttl: USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000 },\n );\n\n return {\n data: this.sanitize(entity),\n meta: { webauthn: options },\n };\n }\n\n protected async enrollEmail(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n this.assertMail();\n\n // The actor identity may not carry `email` (the User entity's email\n // column is select:false) — force-load it to confirm the mailbox\n // exists. The email is presumed verified (activation); the row marks\n // the mailbox as an enrolled factor, codes are transient (cache).\n const email = user.email ?? (await this.userRepository.findOneByWithEmail({ id: user.id }))?.email;\n if (!email) {\n throw new ValidationError('The user has no email address to receive codes.');\n }\n\n // one email factor per user — update the existing marker row in place\n // rather than remove-then-create, so a failed save never leaves the user\n // with no email factor (the row is a pure marker: no secret/codes).\n const existing = (await this.repository.findAllByUser(user.id))\n .filter((device) => device.kind === UserAuthenticatorKind.EMAIL);\n\n let entity: UserAuthenticator;\n if (existing.length > 0) {\n const [primary, ...duplicates] = existing;\n primary.name = name;\n primary.confirmed = true;\n entity = await this.repository.save(primary);\n // best-effort cleanup of any stray duplicates (invariant anomaly);\n // a failure here still leaves the primary factor intact.\n for (const duplicate of duplicates) {\n await this.repository.remove(duplicate);\n }\n } else {\n entity = this.repository.create({\n kind: UserAuthenticatorKind.EMAIL,\n name,\n confirmed: true,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n }\n\n await this.recordEvent(EventName.MFA_ENROLLED, entity);\n\n return { data: this.sanitize(entity), meta: {} };\n }\n\n protected async resolveTargetUser(\n userId: string | undefined,\n actor: ActorContext,\n validated: Record<string, any>,\n ): Promise<User> {\n const actorUser : User | undefined = actor.identity && actor.identity.type === IdentityType.USER ?\n actor.identity.data as User :\n undefined;\n\n if (!userId || (actorUser && actorUser.id === userId)) {\n if (!actorUser) {\n throw new AuthupError({ code: ErrorCode.IDENTITY_UNAUTHORIZED, message: 'Authentication required.' });\n }\n\n return actorUser;\n }\n\n const target = await this.userRepository.findOneById(userId);\n if (!target) {\n throw new EntityNotFoundError();\n }\n\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_CREATE });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_CREATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: { ...validated, realmId: target.realmId },\n ...this.resourceRealmMatch(target),\n }),\n });\n\n return target;\n }\n\n protected async enrollTotp(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n const secret = new Secret({ size: 20 });\n const parameters : UserAuthenticatorTotpParameters = {\n algorithm: USER_AUTHENTICATOR_TOTP_ALGORITHM,\n digits: USER_AUTHENTICATOR_TOTP_DIGITS,\n period: USER_AUTHENTICATOR_TOTP_PERIOD,\n };\n\n const totp = new TOTP({\n issuer: this.options.issuer ?? 'authup',\n label: user.name,\n algorithm: parameters.algorithm,\n digits: parameters.digits,\n period: parameters.period,\n secret,\n });\n const uri = totp.toString();\n\n let entity = this.repository.create({\n kind: UserAuthenticatorKind.TOTP,\n name,\n secret: await this.cipher.encrypt(secret.base32, user.realmId),\n parameters: JSON.stringify(parameters),\n confirmed: false,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n\n return {\n data: this.sanitize(entity),\n meta: {\n secret: secret.base32,\n uri,\n qr: await QRCode.toDataURL(uri),\n },\n };\n }\n\n protected async enrollRecovery(user: User, name: string | null): Promise<UserAuthenticatorEnrollResult> {\n const codes : string[] = [];\n const stored : UserAuthenticatorRecoveryCode[] = [];\n for (let i = 0; i < USER_AUTHENTICATOR_RECOVERY_CODE_COUNT; i++) {\n const code = generateRecoveryCode();\n codes.push(code);\n stored.push({ hash: await hash(code), used_at: null });\n }\n\n // regenerate semantics: a user holds ONE recovery-code set. Reuse the\n // existing row via a single atomic save instead of delete-then-insert,\n // so a mid-operation failure can never leave the user with no codes.\n const [existing] = await this.repository.findAllWithSecretsByUser(user.id, { kind: UserAuthenticatorKind.RECOVERY });\n\n let entity : UserAuthenticator;\n if (existing) {\n existing.name = name;\n existing.codes = JSON.stringify(stored);\n existing.confirmed = true;\n entity = await this.repository.save(existing);\n } else {\n entity = this.repository.create({\n kind: UserAuthenticatorKind.RECOVERY,\n name,\n codes: JSON.stringify(stored),\n confirmed: true,\n userId: user.id,\n realmId: user.realmId,\n });\n entity = await this.repository.save(entity);\n }\n\n await this.recordEvent(EventName.MFA_ENROLLED, entity);\n\n return {\n data: this.sanitize(entity),\n meta: { codes },\n };\n }\n\n // ------------------------------------------------------------------\n\n async confirm(\n id: string,\n code: string,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<UserAuthenticator> {\n const entity = await this.resolveOne(id, options.userId);\n\n if (!this.isOwnedBy(entity, actor)) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_UPDATE });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_UPDATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n }\n\n if (entity.confirmed) {\n return this.sanitize(entity);\n }\n\n if (entity.kind === UserAuthenticatorKind.WEBAUTHN) {\n return this.confirmWebauthn(entity, code);\n }\n\n if (entity.kind !== UserAuthenticatorKind.TOTP) {\n throw new ValidationError(`The authenticator kind ${entity.kind} can not be confirmed with a code.`);\n }\n\n await this.assertNotThrottledOrRetry(entity.userId);\n\n const withSecrets = await this.repository.findOneWithSecretsById(entity.id);\n if (!withSecrets) {\n throw new EntityNotFoundError();\n }\n\n if (!await this.verifyTotp(withSecrets, code)) {\n await this.bumpAttempts(entity.userId);\n throw new EntityCredentialsInvalidError();\n }\n\n await this.resetAttempts(entity.userId);\n\n entity.confirmed = true;\n // The consumed step is tracked only on the login-verify path (below),\n // NOT here — so a legitimate first login in the same window as the\n // confirmation is still accepted; only a repeat of an already-used\n // LOGIN code is rejected.\n const output = await this.repository.save(entity);\n\n await this.recordEvent(EventName.MFA_ENROLLED, output);\n\n return this.sanitize(output);\n }\n\n protected async confirmWebauthn(entity: UserAuthenticator, response: string): Promise<UserAuthenticator> {\n const ctx = this.assertWebauthn();\n\n // brute-force throttle, same lifecycle as the TOTP confirm branch — the\n // attestation verify is a real crypto call and must not be retryable\n // without backoff.\n await this.assertNotThrottledOrRetry(entity.userId);\n\n // challenge is keyed by the enrollment row id (see enrollWebauthn), so\n // overlapping ceremonies do not collide.\n const cacheKey = this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_REG_CACHE_PREFIX, entity.id);\n\n const state = await this.cache.get<WebauthnChallengeState>(cacheKey);\n if (!state || state.expiresAt < Date.now()) {\n await this.bumpAttempts(entity.userId);\n throw new EntityCredentialsInvalidError();\n }\n\n let parsed : RegistrationResponseJSON;\n try {\n parsed = JSON.parse(response) as RegistrationResponseJSON;\n } catch {\n throw new ValidationError('The registration response is malformed.');\n }\n\n const credential = await verifyWebauthnRegistration(ctx, parsed, state.challenge);\n if (!credential) {\n await this.bumpAttempts(entity.userId);\n throw new EntityCredentialsInvalidError();\n }\n\n await this.resetAttempts(entity.userId);\n await this.cache.drop(cacheKey);\n\n entity.parameters = JSON.stringify(credential);\n entity.confirmed = true;\n const output = await this.repository.save(entity);\n\n await this.recordEvent(EventName.MFA_ENROLLED, output);\n\n return this.sanitize(output);\n }\n\n async delete(\n id: string,\n actor: ActorContext,\n options: { userId?: string } = {},\n ): Promise<UserAuthenticator> {\n const entity = await this.resolveOne(id, options.userId);\n\n if (!this.isOwnedBy(entity, actor)) {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_AUTHENTICATOR_DELETE });\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_AUTHENTICATOR_DELETE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: entity,\n ...this.resourceRealmMatch(entity),\n }),\n });\n }\n\n const { id: entityId } = entity;\n await this.repository.remove(entity);\n entity.id = entityId;\n\n await this.recordEvent(EventName.MFA_REMOVED, entity);\n\n return this.sanitize(entity);\n }\n\n // ------------------------------------------------------------------\n\n async verify(\n userId: string,\n input: UserAuthenticatorVerifyInput,\n ctx: UserAuthenticatorVerifyContext = {},\n ): Promise<boolean> {\n try {\n await this.assertNotThrottled(userId);\n } catch (e) {\n if (isMfaThrottledError(e)) {\n throw e;\n }\n\n return false;\n }\n\n // Serialize the read-verify-save critical section per user so a single\n // factor (TOTP step / recovery / email code / webauthn counter) is\n // consumed exactly once even under concurrent verifies. A held lock\n // means another verify is mid-flight — bail without penalty rather than\n // risk a double-consume; that request owns the outcome.\n const lockKey = this.buildVerifyLockCacheKey(userId);\n const lock = await this.acquireVerifyLock(lockKey);\n if (lock.status !== 'acquired') {\n return false;\n }\n\n const lease = this.maintainVerifyLock(lockKey, lock.value);\n\n try {\n const devices = await this.repository.findAllWithSecretsByUser(userId, {\n kind: input.kind,\n confirmed: true,\n });\n\n let matched : UserAuthenticator | undefined;\n\n for (const device of devices) {\n if (input.kind === UserAuthenticatorKind.TOTP) {\n if (await this.verifyTotp(device, input.response)) {\n matched = device;\n }\n }\n\n if (input.kind === UserAuthenticatorKind.RECOVERY) {\n if (await this.verifyRecovery(device, input.response)) {\n matched = device;\n }\n }\n\n if (input.kind === UserAuthenticatorKind.EMAIL) {\n if (await this.verifyEmail(userId, input.response)) {\n matched = device;\n }\n }\n\n if (input.kind === UserAuthenticatorKind.WEBAUTHN) {\n const newCounter = await this.verifyWebauthnDevice(userId, device, input.response);\n if (newCounter !== null) {\n // persist the signature counter (replay defense)\n const parameters = this.parseWebauthnParameters(device);\n if (parameters) {\n device.parameters = JSON.stringify({ ...parameters, counter: newCounter });\n }\n matched = device;\n }\n }\n\n if (matched) {\n break;\n }\n }\n\n if (!matched) {\n await this.bumpAttempts(userId);\n await this.recordChallengeEvent(EventName.MFA_CHALLENGE_FAILED, userId, input.kind, ctx);\n return false;\n }\n\n if (!await lease.renew()) {\n return false;\n }\n\n // Bind the proof to the caller's aggregate (the session mfaAt\n // stamp) BEFORE any consumption persists: a hook failure aborts\n // the verify with nothing consumed — never a burned single-use\n // code without a completed MFA. The residual (a failure AFTER the\n // hook) leaves a stamped session plus a still-valid code — a\n // bounded server-error window, not a lost factor.\n if (ctx.onVerified) {\n await ctx.onVerified();\n }\n\n if (!await lease.renew()) {\n return false;\n }\n\n matched.lastUsedAt = new Date().toISOString();\n await this.repository.save(matched);\n\n // cache-borne single-use artifacts (email code, webauthn challenge\n // nonce) are consumed only once the unit of work committed — the\n // verify lock keeps the deferral race-free.\n if (input.kind === UserAuthenticatorKind.EMAIL) {\n await this.cache.drop(this.buildEmailCodeCacheKey(userId));\n }\n if (input.kind === UserAuthenticatorKind.WEBAUTHN) {\n await this.cache.drop(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX, userId),\n );\n }\n\n await this.resetAttempts(userId);\n\n await this.recordChallengeEvent(EventName.MFA_VERIFIED, userId, input.kind, ctx, matched);\n return true;\n } finally {\n await lease.stop();\n await this.releaseVerifyLock(lockKey, lock.value);\n }\n }\n\n protected async verifyTotp(device: UserAuthenticator, token: string): Promise<boolean> {\n if (!device.secret) {\n return false;\n }\n\n let seed : string;\n try {\n // key-id-addressed blob; one referencing an unknown or foreign\n // key fails closed as a verification failure, never a 500.\n seed = await this.cipher.decrypt(device.secret, device.realmId);\n } catch (e) {\n // ... but ONLY for blob-semantics failures. Infrastructure errors\n // (database outage, KEK misconfiguration) must bubble — mapping\n // them to `false` would burn throttle attempts on a blip.\n if (isRealmCipherBlobError(e)) {\n return false;\n }\n\n throw e;\n }\n\n let parameters : UserAuthenticatorTotpParameters = {\n algorithm: USER_AUTHENTICATOR_TOTP_ALGORITHM,\n digits: USER_AUTHENTICATOR_TOTP_DIGITS,\n period: USER_AUTHENTICATOR_TOTP_PERIOD,\n };\n if (device.parameters) {\n parameters = { ...parameters, ...JSON.parse(device.parameters) };\n }\n\n const totp = new TOTP({\n algorithm: parameters.algorithm,\n digits: parameters.digits,\n period: parameters.period,\n secret: Secret.fromBase32(seed),\n });\n\n const delta = totp.validate({ token: token.trim(), window: 1 });\n if (delta === null) {\n return false;\n }\n\n // Reject replay: the accepted step must strictly advance past the last\n // consumed one (RFC 6238 §5.2), so a captured code can't be reused\n // within its ±1 validation window. The per-user verify lock makes the\n // concurrent case (two verifies advancing the same step) safe.\n const step = Math.floor(Date.now() / 1000 / parameters.period) + delta;\n if (typeof parameters.counter === 'number' && step <= parameters.counter) {\n return false;\n }\n\n // persist the consumed step onto the device — the caller saves the row.\n device.parameters = JSON.stringify({ ...parameters, counter: step });\n return true;\n }\n\n protected async verifyRecovery(device: UserAuthenticator, code: string): Promise<boolean> {\n if (!device.codes) {\n return false;\n }\n\n const entries = JSON.parse(device.codes) as UserAuthenticatorRecoveryCode[];\n const normalized = code.trim().toLowerCase();\n\n for (const entry of entries) {\n if (entry.used_at) {\n continue;\n }\n\n if (await compare(normalized, entry.hash)) {\n entry.used_at = new Date().toISOString();\n device.codes = JSON.stringify(entries);\n return true;\n }\n }\n\n return false;\n }\n\n protected parseWebauthnParameters(device: UserAuthenticator): UserAuthenticatorWebauthnParameters | null {\n if (!device.parameters) {\n return null;\n }\n try {\n return JSON.parse(device.parameters) as UserAuthenticatorWebauthnParameters;\n } catch {\n return null;\n }\n }\n\n protected async verifyWebauthnDevice(\n userId: string,\n device: UserAuthenticator,\n response: string,\n ): Promise<number | null> {\n const ctx = this.assertWebauthn();\n\n const parameters = this.parseWebauthnParameters(device);\n if (!parameters) {\n return null;\n }\n\n let parsed : AuthenticationResponseJSON;\n try {\n parsed = JSON.parse(response) as AuthenticationResponseJSON;\n } catch {\n return null;\n }\n\n // the assertion identifies the credential — only verify against its row\n if (parsed.id !== parameters.credential_id) {\n return null;\n }\n\n const state = await this.cache.get<WebauthnChallengeState>(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX, userId),\n );\n if (!state || state.expiresAt < Date.now()) {\n return null;\n }\n\n try {\n const result = await verifyWebauthnAuthentication(ctx, parsed, state.challenge, parameters);\n return result.verified ? result.newCounter : null;\n } catch {\n return null;\n }\n }\n\n protected async verifyEmail(userId: string, response: string): Promise<boolean> {\n const key = this.buildEmailCodeCacheKey(userId);\n const state = await this.cache.get<EmailCodeState>(key);\n if (!state || state.expiresAt < Date.now()) {\n return false;\n }\n\n // single-use — but the drop is deferred to the verify success block,\n // AFTER the onVerified hook ran, so a hook failure does not burn the\n // code (the verify lock keeps the deferral race-free).\n return compare(response.trim(), state.hash);\n }\n\n // ------------------------------------------------------------------\n\n async sendChallenge(\n userId: string,\n kind: `${UserAuthenticatorKind}`,\n ctx: UserAuthenticatorSendContext = {},\n ): Promise<void> {\n // Only email needs a server-issued challenge; TOTP/recovery are\n // client-derived (no-op keeps the endpoint uniform).\n if (kind !== UserAuthenticatorKind.EMAIL) {\n return;\n }\n\n this.assertMail();\n await this.assertNotThrottledOrRetry(userId);\n\n // require a CONFIRMED email factor — never mail a code to a user who\n // did not enroll email (no code-spray oracle).\n const devices = await this.repository.findAllWithSecretsByUser(userId, {\n kind: UserAuthenticatorKind.EMAIL,\n confirmed: true,\n });\n if (devices.length === 0) {\n return;\n }\n\n // email column is select:false — force-load it for the recipient.\n const user = await this.userRepository.findOneByWithEmail({ id: userId });\n if (!user || !user.email) {\n return;\n }\n\n // per-user send cooldown — an authenticated caller must not be able to\n // spray unlimited OTP mails. `add` is atomic set-if-absent, so the first\n // send stores the key and any resend within the window returns false.\n // Fails open on a cache error (a transient outage must not block a\n // legitimate code).\n let withinCooldown: boolean;\n try {\n withinCooldown = !(await this.cache.add(\n this.buildEmailSendCacheKey(userId),\n 1,\n { ttl: USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN * 1_000 },\n ));\n } catch {\n withinCooldown = false;\n }\n if (withinCooldown) {\n throw new MfaThrottledError({ retryAfter: USER_AUTHENTICATOR_EMAIL_SEND_COOLDOWN });\n }\n\n const codeValue = generateNumericCode(USER_AUTHENTICATOR_EMAIL_CODE_LENGTH);\n await this.cache.set(\n this.buildEmailCodeCacheKey(userId),\n {\n hash: await hash(codeValue),\n expiresAt: Date.now() + (USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES * 60 * 1_000),\n } satisfies EmailCodeState,\n { ttl: USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES * 60 * 1_000 },\n );\n\n const mail = await this.mailTemplateRenderer!.render({\n template: MailTemplateName.MFA_EMAIL_OTP,\n params: {\n code: codeValue,\n expiresInMinutes: USER_AUTHENTICATOR_EMAIL_CODE_EXPIRES_IN_MINUTES,\n },\n locale: ctx.locale,\n });\n\n await this.mailClient!.send({\n to: user.email,\n ...mail,\n });\n }\n\n // ------------------------------------------------------------------\n\n async hasConfirmed(userId: string): Promise<boolean> {\n return this.repository.hasConfirmedByUser(userId);\n }\n\n async challenge(\n userId: string,\n options: { issueMaterial?: boolean } = {},\n ): Promise<UserAuthenticatorChallengeStatus> {\n if (!this.options.enabled) {\n return {\n required: false,\n enrollmentRequired: false,\n kinds: [],\n };\n }\n\n const devices = await this.repository.findAllByUser(userId);\n const confirmed = devices.filter((device) => device.confirmed);\n\n const kinds = Array.from(new Set(confirmed.map((device) => device.kind)));\n\n const status : UserAuthenticatorChallengeStatus = {\n required: confirmed.length > 0,\n enrollmentRequired: !!this.options.required && confirmed.length === 0,\n kinds,\n };\n\n // WebAuthn needs server-issued request options as the challenge — build\n // them (and store the nonce) when the subject holds a webauthn factor.\n // Skipped when the caller only needs the requirement flags (issueMaterial\n // false): the enforcement chokepoints must not rotate an in-flight\n // ceremony's nonce nor run this extra query on every request.\n const issueMaterial = options.issueMaterial ?? true;\n if (issueMaterial && kinds.includes(UserAuthenticatorKind.WEBAUTHN) && this.options.webauthn) {\n const credentials = (await this.repository.findAllWithSecretsByUser(userId, {\n kind: UserAuthenticatorKind.WEBAUTHN,\n confirmed: true,\n }))\n .map((device) => this.parseWebauthnParameters(device))\n .filter((parameters): parameters is UserAuthenticatorWebauthnParameters => !!parameters)\n .map((parameters) => ({ id: parameters.credential_id, transports: parameters.transports }));\n\n const { options, challenge } = await buildWebauthnAuthenticationOptions(this.options.webauthn, credentials);\n\n await this.cache.set(\n this.buildWebauthnCacheKey(USER_AUTHENTICATOR_WEBAUTHN_AUTH_CACHE_PREFIX, userId),\n {\n challenge,\n expiresAt: Date.now() + (USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000),\n } satisfies WebauthnChallengeState,\n { ttl: USER_AUTHENTICATOR_WEBAUTHN_CHALLENGE_WINDOW * 1_000 },\n );\n\n status.challenge = { webauthn: options };\n }\n\n return status;\n }\n\n // ------------------------------------------------------------------\n\n protected assertEnabled(): void {\n if (!this.options.enabled) {\n throw new AuthupError({\n code: ErrorCode.MFA_NOT_CONFIGURABLE,\n message: 'Multi-factor authentication is not enabled.',\n });\n }\n }\n\n protected assertMail(): void {\n if (!this.mailClient || !this.mailTemplateRenderer) {\n throw new AuthupError({\n code: ErrorCode.MFA_NOT_CONFIGURABLE,\n message: 'Email-based multi-factor authentication requires a configured mail transport.',\n });\n }\n }\n\n protected assertWebauthn(): WebauthnContext {\n if (!this.options.webauthn) {\n throw new AuthupError({\n code: ErrorCode.MFA_NOT_CONFIGURABLE,\n message: 'WebAuthn requires a configured public URL (relying-party origin).',\n });\n }\n\n return this.options.webauthn;\n }\n\n // ------------------------------------------------------------------\n\n protected buildAttemptCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_ATTEMPT_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildThrottleCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_THROTTLE_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildEmailCodeCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_EMAIL_CODE_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildEmailSendCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_EMAIL_SEND_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected buildWebauthnCacheKey(prefix: string, userId: string): string {\n return buildCacheKey({ prefix, key: userId });\n }\n\n protected buildVerifyLockCacheKey(userId: string): string {\n return buildCacheKey({\n prefix: USER_AUTHENTICATOR_VERIFY_LOCK_CACHE_PREFIX,\n key: userId,\n });\n }\n\n protected async acquireVerifyLock(key: string): Promise<VerifyLock> {\n const value = randomUUID();\n\n try {\n return (await this.cache.add(key, value, { ttl: USER_AUTHENTICATOR_VERIFY_LOCK_TTL })) ?\n { status: 'acquired', value } :\n { status: 'busy' };\n } catch {\n return { status: 'unavailable' };\n }\n }\n\n protected maintainVerifyLock(key: string, value: string): VerifyLockLease {\n let owned = true;\n let pending : Promise<void> | undefined;\n\n const queueRenewal = () => {\n if (!owned || pending) {\n return pending;\n }\n\n pending = this.cache\n .renewIfValue(key, value, USER_AUTHENTICATOR_VERIFY_LOCK_TTL)\n .then((renewed) => {\n owned = renewed;\n })\n .catch(() => {\n owned = false;\n })\n .finally(() => {\n pending = undefined;\n });\n\n return pending;\n };\n\n const timer = setInterval(queueRenewal, USER_AUTHENTICATOR_VERIFY_LOCK_RENEW_INTERVAL);\n\n return {\n renew: async () => {\n await queueRenewal();\n return owned;\n },\n stop: async () => {\n clearInterval(timer);\n await pending;\n },\n };\n }\n\n protected async releaseVerifyLock(key: string, value: string): Promise<void> {\n try {\n await this.cache.dropIfValue(key, value);\n } catch {\n // Best-effort release; the renewable lock TTL bounds a stranded key.\n }\n }\n\n protected async assertNotThrottled(userId: string): Promise<void> {\n const lockedUntil = await this.cache.get<number>(this.buildThrottleCacheKey(userId));\n if (typeof lockedUntil === 'number' && lockedUntil > Date.now()) {\n throw new MfaThrottledError({ retryAfter: Math.ceil((lockedUntil - Date.now()) / 1_000) });\n }\n }\n\n // Throttle gate for the throwing entry points (confirm / confirmWebauthn /\n // sendChallenge). Mirrors verify()'s fail-closed posture: a genuine lockout\n // surfaces unchanged, while an unreadable throttle counter (cache outage) is\n // treated as a lockout — the caller is told to retry (429) rather than the\n // outage bubbling up as an internal 500.\n protected async assertNotThrottledOrRetry(userId: string): Promise<void> {\n try {\n await this.assertNotThrottled(userId);\n } catch (e) {\n if (isMfaThrottledError(e)) {\n throw e;\n }\n\n throw new MfaThrottledError({ retryAfter: USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR });\n }\n }\n\n protected async bumpAttempts(userId: string): Promise<void> {\n const key = this.buildAttemptCacheKey(userId);\n\n // Atomic increment — concurrent failures (verify OR confirm) each get\n // a distinct count, so the exponential backoff never under-counts.\n let count : number;\n try {\n count = await this.cache.increment(key, 1, { ttl: USER_AUTHENTICATOR_ATTEMPT_WINDOW * 1_000 });\n } catch {\n // a non-numeric value at the key (legacy JSON state) — reset and\n // re-count; a genuine cache outage rethrows from the drop.\n await this.cache.drop(key);\n count = await this.cache.increment(key, 1, { ttl: USER_AUTHENTICATOR_ATTEMPT_WINDOW * 1_000 });\n }\n\n const delay = Math.min(\n USER_AUTHENTICATOR_ATTEMPT_LOCK_MAX,\n USER_AUTHENTICATOR_ATTEMPT_LOCK_FACTOR * (2 ** (count - 1)),\n );\n\n await this.cache.set(\n this.buildThrottleCacheKey(userId),\n Date.now() + (delay * 1_000),\n { ttl: delay * 1_000 },\n );\n }\n\n protected async resetAttempts(userId: string): Promise<void> {\n await this.cache.dropMany([\n this.buildAttemptCacheKey(userId),\n this.buildThrottleCacheKey(userId),\n ]);\n }\n\n // ------------------------------------------------------------------\n\n protected async recordEvent(\n name: `${EventName}`,\n entity: UserAuthenticator,\n ): Promise<void> {\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name,\n refType: EventRefType.USER_AUTHENTICATOR,\n refId: entity.id,\n actorType: IdentityType.USER,\n actorId: entity.userId,\n realmId: entity.realmId,\n data: { kind: entity.kind },\n });\n }\n\n protected async recordChallengeEvent(\n name: `${EventName}`,\n userId: string,\n kind: string,\n ctx: UserAuthenticatorVerifyContext,\n entity?: UserAuthenticator,\n ): Promise<void> {\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name,\n refType: EventRefType.USER,\n refId: userId,\n actorType: IdentityType.USER,\n actorId: userId,\n clientId: ctx.clientId ?? null,\n realmId: entity?.realmId ?? null,\n requestIpAddress: ctx.ipAddress ?? null,\n requestUserAgent: ctx.userAgent ?? null,\n data: { kind },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -62,15 +62,18 @@ var UserPermissionService = class extends JunctionEntityService {
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
if (validated.user) validated.userRealmId = validated.user.realmId;
|
|
65
|
-
if (validated.permission && actor.identity)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
if (validated.permission && actor.identity) {
|
|
66
|
+
const grant = await this.identityPermissionProvider.resolveJunctionGrant({
|
|
67
|
+
type: actor.identity.type,
|
|
68
|
+
id: actor.identity.data.id
|
|
69
|
+
}, {
|
|
70
|
+
name: validated.permission.name,
|
|
71
|
+
realmId: validated.permission.realmId,
|
|
72
|
+
clientId: validated.permission.clientId,
|
|
73
|
+
realmScope: validated.realmScope ?? RealmScope.OWN
|
|
74
|
+
});
|
|
75
|
+
applyJunctionCreateGrant(validated, grant);
|
|
76
|
+
}
|
|
74
77
|
await actor.permissionEvaluator.evaluate({
|
|
75
78
|
name: PermissionName.USER_PERMISSION_CREATE,
|
|
76
79
|
data: definePolicyData({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":["BuiltInPolicyType","RealmScope","definePolicyData","EntityConflictError","EntityNotFoundError","ValidatorGroup","PermissionName","UserPermissionValidator","applyJunctionCreateGrant","buildJunctionUpdateData","JunctionEntityService","decodeQuery","userPermissionSchema","UserPermissionService","ownerRealmKey","repository","permissionRepository","identityPermissionProvider","validator","ctx","getMany","query","actor","permissionEvaluator","preEvaluateOneOf","name","USER_PERMISSION_CREATE","USER_PERMISSION_DELETE","USER_PERMISSION_READ","findMany","schema","getOne","id","entity","findOneBy","create","data","preEvaluate","validated","run","group","CREATE","validateJoinColumns","existing","permissionId","userId","permission","permissionRealmId","realmId","clientId","user","userRealmId","identity","grant","resolveJunctionGrant","type","realmScope","OWN","evaluate","ATTRIBUTES","junctionAttributes","REALM_MATCH","junctionResourceRealm","save","update","USER_PERMISSION_UPDATE","UPDATE","findOneById","actorScope","ANY","actorPolicyFree","actorPolicyId","policy","updateData","existingScope","merged","merge","delete","entityId","remove"],"sources":["../../../../src/core/entities/user-permission/service.ts"],"sourcesContent":["/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n BuiltInPolicyType,\n RealmScope,\n definePolicyData,\n} from '@authup/access';\nimport { EntityConflictError, EntityNotFoundError } from '@authup/errors';\nimport { ValidatorGroup } from '@authup/kit';\nimport { PermissionName, UserPermissionValidator } from '@authup/core-kit';\nimport type { Permission, UserPermission } from '@authup/core-kit';\nimport type { IIdentityPermissionProvider } from '../../identity/permission/types.ts';\nimport { applyJunctionCreateGrant, buildJunctionUpdateData } from '../../identity/permission/junction-grant.ts';\nimport type { ActorContext, EntityRepositoryFindManyResult, IEntityRepository } from '@authup/server-kit';\nimport { JunctionEntityService } from '@authup/server-kit';\nimport type { IUserPermissionRepository, IUserPermissionService } from './types.ts';\nimport { decodeQuery } from '../../query/index.ts';\nimport { userPermissionSchema } from './schema.ts';\n\nexport type UserPermissionServiceContext = {\n repository: IUserPermissionRepository;\n permissionRepository: IEntityRepository<Permission>;\n identityPermissionProvider: IIdentityPermissionProvider;\n};\n\nexport class UserPermissionService extends JunctionEntityService implements IUserPermissionService {\n protected readonly ownerRealmKey = 'userRealmId';\n\n protected repository: IUserPermissionRepository;\n\n protected permissionRepository: IEntityRepository<Permission>;\n\n protected identityPermissionProvider: IIdentityPermissionProvider;\n\n protected validator: UserPermissionValidator;\n\n constructor(ctx: UserPermissionServiceContext) {\n super();\n this.repository = ctx.repository;\n this.permissionRepository = ctx.permissionRepository;\n this.identityPermissionProvider = ctx.identityPermissionProvider;\n this.validator = new UserPermissionValidator();\n }\n\n async getMany(\n query: Record<string, any>,\n actor: ActorContext,\n ): Promise<EntityRepositoryFindManyResult<UserPermission>> {\n await actor.permissionEvaluator.preEvaluateOneOf({\n name: [\n PermissionName.USER_PERMISSION_CREATE,\n PermissionName.USER_PERMISSION_DELETE,\n PermissionName.USER_PERMISSION_READ,\n ],\n });\n\n return this.repository.findMany(await decodeQuery(query, { schema: userPermissionSchema, actor }));\n }\n\n async getOne(\n id: string,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluateOneOf({\n name: [\n PermissionName.USER_PERMISSION_CREATE,\n PermissionName.USER_PERMISSION_DELETE,\n PermissionName.USER_PERMISSION_READ,\n ],\n });\n\n const entity = await this.repository.findOneBy({ id });\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n return entity;\n }\n\n async create(\n data: Record<string, any>,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_PERMISSION_CREATE });\n\n const validated = await this.validator.run(data, { group: ValidatorGroup.CREATE });\n\n await this.repository.validateJoinColumns(validated);\n\n const existing = await this.repository.findOneBy({\n permissionId: validated.permissionId,\n userId: validated.userId,\n });\n if (existing) {\n throw new EntityConflictError({ entity: 'user-permission' });\n }\n\n if (validated.permission) {\n validated.permissionRealmId = validated.permission.realmId;\n\n await actor.permissionEvaluator.preEvaluate({\n name: validated.permission.name,\n realmId: validated.permission.realmId,\n clientId: validated.permission.clientId,\n });\n }\n\n if (validated.user) {\n validated.userRealmId = validated.user.realmId;\n }\n\n if (validated.permission && actor.identity) {\n const grant = await this.identityPermissionProvider.resolveJunctionGrant(\n {\n type: actor.identity.type,\n id: actor.identity.data.id,\n },\n {\n name: validated.permission.name,\n realmId: validated.permission.realmId,\n clientId: validated.permission.clientId,\n realmScope: validated.realmScope ?? RealmScope.OWN,\n },\n );\n\n applyJunctionCreateGrant(validated, grant);\n }\n\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_PERMISSION_CREATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: this.junctionAttributes(validated),\n [BuiltInPolicyType.REALM_MATCH]: this.junctionResourceRealm(validated),\n }),\n });\n\n let entity = this.repository.create(validated);\n entity = await this.repository.save(entity);\n\n return entity;\n }\n\n async update(\n id: string,\n data: Record<string, any>,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_PERMISSION_UPDATE });\n\n const entity = await this.repository.findOneBy({ id });\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n const validated = await this.validator.run(data, { group: ValidatorGroup.UPDATE });\n\n const permission = await this.permissionRepository.findOneById(entity.permissionId);\n if (permission) {\n // Member-permission gate: an actor may only modify a binding for a permission it\n // holds (mirrors create()).\n await actor.permissionEvaluator.preEvaluate({\n name: permission.name,\n realmId: permission.realmId,\n clientId: permission.clientId,\n });\n }\n\n let actorScope: `${RealmScope}` = RealmScope.ANY;\n let actorPolicyFree = true;\n let actorPolicyId: string | null = null;\n if (actor.identity && permission) {\n const grant = await this.identityPermissionProvider.resolveJunctionGrant(\n { type: actor.identity.type, id: actor.identity.data.id },\n {\n name: permission.name,\n realmId: permission.realmId,\n clientId: permission.clientId,\n realmScope: validated.realmScope ?? entity.realmScope,\n },\n );\n actorScope = grant.realmScope;\n actorPolicyFree = !grant.policy;\n actorPolicyId = grant.policy ? grant.policy.id : null;\n } else if (actor.identity) {\n actorScope = RealmScope.OWN;\n }\n\n const updateData = buildJunctionUpdateData({\n data: validated,\n existingScope: entity.realmScope,\n actorScope,\n actorPolicyFree,\n actorPolicyId,\n });\n\n await this.repository.validateJoinColumns(updateData);\n\n const merged = this.repository.merge(entity, updateData);\n\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_PERMISSION_UPDATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: this.junctionAttributes(merged),\n [BuiltInPolicyType.REALM_MATCH]: this.junctionResourceRealm(merged),\n }),\n });\n\n return this.repository.save(merged);\n }\n\n async delete(\n id: string,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_PERMISSION_DELETE });\n\n const entity = await this.repository.findOneBy({ id });\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_PERMISSION_DELETE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: this.junctionAttributes(entity),\n [BuiltInPolicyType.REALM_MATCH]: this.junctionResourceRealm(entity),\n }),\n });\n\n const { id: entityId } = entity;\n await this.repository.remove(entity);\n entity.id = entityId;\n\n return entity;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":["BuiltInPolicyType","RealmScope","definePolicyData","EntityConflictError","EntityNotFoundError","ValidatorGroup","PermissionName","UserPermissionValidator","applyJunctionCreateGrant","buildJunctionUpdateData","JunctionEntityService","decodeQuery","userPermissionSchema","UserPermissionService","ownerRealmKey","repository","permissionRepository","identityPermissionProvider","validator","ctx","getMany","query","actor","permissionEvaluator","preEvaluateOneOf","name","USER_PERMISSION_CREATE","USER_PERMISSION_DELETE","USER_PERMISSION_READ","findMany","schema","getOne","id","entity","findOneBy","create","data","preEvaluate","validated","run","group","CREATE","validateJoinColumns","existing","permissionId","userId","permission","permissionRealmId","realmId","clientId","user","userRealmId","identity","grant","resolveJunctionGrant","type","realmScope","OWN","evaluate","ATTRIBUTES","junctionAttributes","REALM_MATCH","junctionResourceRealm","save","update","USER_PERMISSION_UPDATE","UPDATE","findOneById","actorScope","ANY","actorPolicyFree","actorPolicyId","policy","updateData","existingScope","merged","merge","delete","entityId","remove"],"sources":["../../../../src/core/entities/user-permission/service.ts"],"sourcesContent":["/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n BuiltInPolicyType,\n RealmScope,\n definePolicyData,\n} from '@authup/access';\nimport { EntityConflictError, EntityNotFoundError } from '@authup/errors';\nimport { ValidatorGroup } from '@authup/kit';\nimport { PermissionName, UserPermissionValidator } from '@authup/core-kit';\nimport type { Permission, UserPermission } from '@authup/core-kit';\nimport type { IIdentityPermissionProvider } from '../../identity/permission/types.ts';\nimport { applyJunctionCreateGrant, buildJunctionUpdateData } from '../../identity/permission/junction-grant.ts';\nimport type { ActorContext, EntityRepositoryFindManyResult, IEntityRepository } from '@authup/server-kit';\nimport { JunctionEntityService } from '@authup/server-kit';\nimport type { IUserPermissionRepository, IUserPermissionService } from './types.ts';\nimport { decodeQuery } from '../../query/index.ts';\nimport { userPermissionSchema } from './schema.ts';\n\nexport type UserPermissionServiceContext = {\n repository: IUserPermissionRepository;\n permissionRepository: IEntityRepository<Permission>;\n identityPermissionProvider: IIdentityPermissionProvider;\n};\n\nexport class UserPermissionService extends JunctionEntityService implements IUserPermissionService {\n protected readonly ownerRealmKey = 'userRealmId';\n\n protected repository: IUserPermissionRepository;\n\n protected permissionRepository: IEntityRepository<Permission>;\n\n protected identityPermissionProvider: IIdentityPermissionProvider;\n\n protected validator: UserPermissionValidator;\n\n constructor(ctx: UserPermissionServiceContext) {\n super();\n this.repository = ctx.repository;\n this.permissionRepository = ctx.permissionRepository;\n this.identityPermissionProvider = ctx.identityPermissionProvider;\n this.validator = new UserPermissionValidator();\n }\n\n async getMany(\n query: Record<string, any>,\n actor: ActorContext,\n ): Promise<EntityRepositoryFindManyResult<UserPermission>> {\n await actor.permissionEvaluator.preEvaluateOneOf({\n name: [\n PermissionName.USER_PERMISSION_CREATE,\n PermissionName.USER_PERMISSION_DELETE,\n PermissionName.USER_PERMISSION_READ,\n ],\n });\n\n return this.repository.findMany(await decodeQuery(query, { schema: userPermissionSchema, actor }));\n }\n\n async getOne(\n id: string,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluateOneOf({\n name: [\n PermissionName.USER_PERMISSION_CREATE,\n PermissionName.USER_PERMISSION_DELETE,\n PermissionName.USER_PERMISSION_READ,\n ],\n });\n\n const entity = await this.repository.findOneBy({ id });\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n return entity;\n }\n\n async create(\n data: Record<string, any>,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_PERMISSION_CREATE });\n\n const validated = await this.validator.run(data, { group: ValidatorGroup.CREATE });\n\n await this.repository.validateJoinColumns(validated);\n\n const existing = await this.repository.findOneBy({\n permissionId: validated.permissionId,\n userId: validated.userId,\n });\n if (existing) {\n throw new EntityConflictError({ entity: 'user-permission' });\n }\n\n if (validated.permission) {\n validated.permissionRealmId = validated.permission.realmId;\n\n await actor.permissionEvaluator.preEvaluate({\n name: validated.permission.name,\n realmId: validated.permission.realmId,\n clientId: validated.permission.clientId,\n });\n }\n\n if (validated.user) {\n validated.userRealmId = validated.user.realmId;\n }\n\n if (validated.permission && actor.identity) {\n const grant = await this.identityPermissionProvider.resolveJunctionGrant(\n {\n type: actor.identity.type,\n id: actor.identity.data.id,\n },\n {\n name: validated.permission.name,\n realmId: validated.permission.realmId,\n clientId: validated.permission.clientId,\n realmScope: validated.realmScope ?? RealmScope.OWN,\n },\n );\n\n applyJunctionCreateGrant(validated, grant);\n }\n\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_PERMISSION_CREATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: this.junctionAttributes(validated),\n [BuiltInPolicyType.REALM_MATCH]: this.junctionResourceRealm(validated),\n }),\n });\n\n let entity = this.repository.create(validated);\n entity = await this.repository.save(entity);\n\n return entity;\n }\n\n async update(\n id: string,\n data: Record<string, any>,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_PERMISSION_UPDATE });\n\n const entity = await this.repository.findOneBy({ id });\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n const validated = await this.validator.run(data, { group: ValidatorGroup.UPDATE });\n\n const permission = await this.permissionRepository.findOneById(entity.permissionId);\n if (permission) {\n // Member-permission gate: an actor may only modify a binding for a permission it\n // holds (mirrors create()).\n await actor.permissionEvaluator.preEvaluate({\n name: permission.name,\n realmId: permission.realmId,\n clientId: permission.clientId,\n });\n }\n\n let actorScope: `${RealmScope}` = RealmScope.ANY;\n let actorPolicyFree = true;\n let actorPolicyId: string | null = null;\n if (actor.identity && permission) {\n const grant = await this.identityPermissionProvider.resolveJunctionGrant(\n { type: actor.identity.type, id: actor.identity.data.id },\n {\n name: permission.name,\n realmId: permission.realmId,\n clientId: permission.clientId,\n realmScope: validated.realmScope ?? entity.realmScope,\n },\n );\n actorScope = grant.realmScope;\n actorPolicyFree = !grant.policy;\n actorPolicyId = grant.policy ? grant.policy.id : null;\n } else if (actor.identity) {\n actorScope = RealmScope.OWN;\n }\n\n const updateData = buildJunctionUpdateData({\n data: validated,\n existingScope: entity.realmScope,\n actorScope,\n actorPolicyFree,\n actorPolicyId,\n });\n\n await this.repository.validateJoinColumns(updateData);\n\n const merged = this.repository.merge(entity, updateData);\n\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_PERMISSION_UPDATE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: this.junctionAttributes(merged),\n [BuiltInPolicyType.REALM_MATCH]: this.junctionResourceRealm(merged),\n }),\n });\n\n return this.repository.save(merged);\n }\n\n async delete(\n id: string,\n actor: ActorContext,\n ): Promise<UserPermission> {\n await actor.permissionEvaluator.preEvaluate({ name: PermissionName.USER_PERMISSION_DELETE });\n\n const entity = await this.repository.findOneBy({ id });\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n await actor.permissionEvaluator.evaluate({\n name: PermissionName.USER_PERMISSION_DELETE,\n data: definePolicyData({\n [BuiltInPolicyType.ATTRIBUTES]: this.junctionAttributes(entity),\n [BuiltInPolicyType.REALM_MATCH]: this.junctionResourceRealm(entity),\n }),\n });\n\n const { id: entityId } = entity;\n await this.repository.remove(entity);\n entity.id = entityId;\n\n return entity;\n }\n}\n"],"mappings":"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"}
|
|
@@ -43,7 +43,7 @@ var PasswordRecoveryService = class {
|
|
|
43
43
|
const entity = await this.repository.findOneByWithEmail(where);
|
|
44
44
|
if (!entity) throw new EntityNotFoundError();
|
|
45
45
|
const merged = this.repository.merge(entity, {
|
|
46
|
-
resetExpires: new Date(Date.now() +
|
|
46
|
+
resetExpires: new Date(Date.now() + 18e5).toISOString(),
|
|
47
47
|
resetHash: randomBytes(32).toString("hex")
|
|
48
48
|
});
|
|
49
49
|
await this.repository.save(merged);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":["EventName","EventRefType","EventScope","IdentityType","USER_PASSWORD_MAX_LENGTH","USER_PASSWORD_MIN_LENGTH","EntityNotFoundError","ValidationError","PasswordRecoveryDisabledError","EmailVerificationRequiredError","ResetTokenExpiredError","createValidator","randomBytes","Container","z","UserCredentialsService","MailTemplateName","PASSWORD_RESET_EXPIRES_IN_MINUTES","PasswordRecoveryService","options","repository","realmRepository","mailClient","mailTemplateRenderer","eventService","ctx","forgotPassword","data","context","passwordRecoveryEnabled","emailVerificationEnabled","validated","runForgotPasswordValidator","realm","resolve","realmId","where","name","email","id","entity","findOneByWithEmail","merged","merge","resetExpires","Date","now","toISOString","resetHash","toString","save","resetUrl","publicUrl","replace","encodeURIComponent","undefined","mail","render","template","PASSWORD_RESET","params","code","url","expiresInMinutes","locale","send","to","record","scope","IDENTITY","PASSWORD_RESET_REQUESTED","refType","USER","refId","actorType","actorId","actorName","resetPassword","runResetPasswordValidator","validateJoinColumns","token","findOneBy","credentialsService","hashedPassword","protect","password","resetAt","PASSWORD_RESET_COMPLETED","validator","oneOfContainer","oneOf","mount","string","trim","toLowerCase","regex","message","min","optional","uuid","nullable","run","max","passwordMinLength"],"sources":["../../../../src/core/identity/password-recovery/service.ts"],"sourcesContent":["/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n EventName,\n EventRefType,\n EventScope,\n IdentityType,\n USER_PASSWORD_MAX_LENGTH,\n USER_PASSWORD_MIN_LENGTH,\n} from '@authup/core-kit';\nimport { EntityNotFoundError, ValidationError } from '@authup/errors';\nimport { PasswordRecoveryDisabledError } from './disabled.ts';\nimport { EmailVerificationRequiredError } from './email-verification-required.ts';\nimport { ResetTokenExpiredError } from './token-expired.ts';\nimport { createValidator } from '@validup/zod';\nimport { randomBytes } from 'node:crypto';\nimport { Container } from 'validup';\nimport { z } from 'zod';\nimport { UserCredentialsService } from '../../authentication/credential/entities/user/module.ts';\nimport type {\n IPasswordRecoveryService,\n PasswordForgotResult,\n PasswordRecoveryServiceContext,\n PasswordRecoveryServiceOptions,\n PasswordResetResult,\n} from './types.ts';\nimport type { IMailClient, IMailTemplateRenderer } from '../../mail/types.ts';\nimport { MailTemplateName } from '../../mail/index.ts';\nimport type { IEventService, IRealmRepository, IUserRepository } from '../../entities/index.ts';\nimport type { IdentityWorkflowContext } from '../types.ts';\nimport { PASSWORD_RESET_EXPIRES_IN_MINUTES } from './constants.ts';\n\nexport class PasswordRecoveryService implements IPasswordRecoveryService {\n protected options: PasswordRecoveryServiceOptions;\n\n protected repository: IUserRepository;\n\n protected realmRepository: IRealmRepository;\n\n protected mailClient: IMailClient;\n\n protected mailTemplateRenderer: IMailTemplateRenderer;\n\n protected eventService?: IEventService;\n\n constructor(ctx: PasswordRecoveryServiceContext) {\n this.options = ctx.options;\n this.repository = ctx.repository;\n this.realmRepository = ctx.realmRepository;\n this.mailClient = ctx.mailClient;\n this.mailTemplateRenderer = ctx.mailTemplateRenderer;\n this.eventService = ctx.eventService;\n }\n\n async forgotPassword(data: Record<string, any>, context?: IdentityWorkflowContext): Promise<PasswordForgotResult> {\n if (!this.options.passwordRecoveryEnabled) {\n throw new PasswordRecoveryDisabledError();\n }\n\n if (!this.options.emailVerificationEnabled) {\n throw new EmailVerificationRequiredError('Email verification is not enabled, but required to reset a password.');\n }\n\n const validated = await this.runForgotPasswordValidator(data);\n\n const realm = await this.realmRepository.resolve(validated.realmId, true);\n\n const where: Record<string, any> = {\n ...(validated.name ? { name: validated.name } : {}),\n ...(validated.email ? { email: validated.email } : {}),\n realmId: realm.id,\n };\n\n const entity = await this.repository.findOneByWithEmail(where);\n\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n const merged = this.repository.merge(entity, {\n resetExpires: new Date(\n Date.now() + (1000 * 60 * PASSWORD_RESET_EXPIRES_IN_MINUTES),\n ).toISOString(),\n resetHash: randomBytes(32).toString('hex'),\n });\n\n await this.repository.save(merged);\n\n try {\n // realmId MUST ride the link: resetPassword resolves the realm\n // from the request and filters the lookup by it, so a realm-less\n // link resolves to the master realm and never matches a\n // non-master user.\n const resetUrl = this.options.publicUrl ?\n `${this.options.publicUrl.replace(/\\/+$/, '')}/password-reset` +\n `?token=${encodeURIComponent(merged.resetHash!)}` +\n `&realmId=${encodeURIComponent(entity.realmId)}` :\n undefined;\n\n const mail = await this.mailTemplateRenderer.render({\n template: MailTemplateName.PASSWORD_RESET,\n params: {\n code: merged.resetHash!,\n url: resetUrl,\n expiresInMinutes: PASSWORD_RESET_EXPIRES_IN_MINUTES,\n },\n locale: context?.locale,\n });\n\n await this.mailClient.send({\n to: entity.email,\n ...mail,\n });\n } catch {\n this.repository.merge(merged, {\n resetHash: null,\n resetExpires: null,\n });\n await this.repository.save(merged);\n\n throw new ValidationError('Password recovery failed. Could not send reset email.');\n }\n\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name: EventName.PASSWORD_RESET_REQUESTED,\n refType: EventRefType.USER,\n refId: merged.id,\n actorType: IdentityType.USER,\n actorId: merged.id,\n actorName: merged.name,\n realmId: merged.realmId,\n });\n\n return { resetExpires: merged.resetExpires! };\n }\n\n async resetPassword(data: Record<string, any>): Promise<PasswordResetResult> {\n if (!this.options.passwordRecoveryEnabled) {\n throw new PasswordRecoveryDisabledError();\n }\n\n const validated = await this.runResetPasswordValidator(data);\n\n await this.repository.validateJoinColumns(validated);\n\n const realm = await this.realmRepository.resolve(validated.realmId, true);\n\n const where: Record<string, any> = {\n ...(validated.name ? { name: validated.name } : {}),\n ...(validated.email ? { email: validated.email } : {}),\n resetHash: validated.token,\n realmId: realm.id,\n };\n\n const entity = await this.repository.findOneBy(where);\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n if (!entity.resetExpires || new Date(entity.resetExpires) < new Date()) {\n throw new ResetTokenExpiredError();\n }\n\n const credentialsService = new UserCredentialsService();\n const hashedPassword = await credentialsService.protect(validated.password);\n\n const merged = this.repository.merge(entity, {\n resetAt: new Date().toISOString(),\n resetHash: null,\n resetExpires: null,\n password: hashedPassword,\n });\n\n await this.repository.save(merged);\n\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name: EventName.PASSWORD_RESET_COMPLETED,\n refType: EventRefType.USER,\n refId: merged.id,\n actorType: IdentityType.USER,\n actorId: merged.id,\n actorName: merged.name,\n realmId: merged.realmId,\n });\n\n return { resetAt: merged.resetAt! };\n }\n\n private async runForgotPasswordValidator(data: Record<string, any>) {\n const validator = new Container<any>({});\n\n const oneOfContainer = new Container({ oneOf: true });\n\n oneOfContainer.mount(\n 'email',\n createValidator(\n z.string()\n .trim()\n .toLowerCase()\n .email()\n .regex(/^[^A-Z]+$/, { message: 'Email must be lowercase.' }),\n ),\n );\n\n oneOfContainer.mount(\n 'name',\n createValidator(z.string().trim().toLowerCase().min(1)),\n );\n\n validator.mount(oneOfContainer);\n\n validator.mount(\n 'realmId',\n { optional: true },\n createValidator(z.uuid().nullable()),\n );\n\n return validator.run(data);\n }\n\n private async runResetPasswordValidator(data: Record<string, any>) {\n const validator = new Container<any>({});\n\n const oneOfContainer = new Container({ oneOf: true });\n oneOfContainer.mount(\n 'email',\n createValidator(\n z.string()\n .trim()\n .toLowerCase()\n .email()\n .regex(/^[^A-Z]+$/, { message: 'Email must be lowercase.' }),\n ),\n );\n oneOfContainer.mount(\n 'name',\n createValidator(z.string().trim().toLowerCase().min(1)),\n );\n\n validator.mount(oneOfContainer);\n\n validator.mount(\n 'realmId',\n { optional: true },\n createValidator(z.uuid().nullable()),\n );\n\n validator.mount(\n 'token',\n createValidator(z.string().min(3).max(256)),\n );\n\n validator.mount(\n 'password',\n createValidator(\n z.string()\n .min(this.options.passwordMinLength ?? USER_PASSWORD_MIN_LENGTH)\n .max(USER_PASSWORD_MAX_LENGTH),\n ),\n );\n\n return validator.run(data);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":["EventName","EventRefType","EventScope","IdentityType","USER_PASSWORD_MAX_LENGTH","USER_PASSWORD_MIN_LENGTH","EntityNotFoundError","ValidationError","PasswordRecoveryDisabledError","EmailVerificationRequiredError","ResetTokenExpiredError","createValidator","randomBytes","Container","z","UserCredentialsService","MailTemplateName","PASSWORD_RESET_EXPIRES_IN_MINUTES","PasswordRecoveryService","options","repository","realmRepository","mailClient","mailTemplateRenderer","eventService","ctx","forgotPassword","data","context","passwordRecoveryEnabled","emailVerificationEnabled","validated","runForgotPasswordValidator","realm","resolve","realmId","where","name","email","id","entity","findOneByWithEmail","merged","merge","resetExpires","Date","now","toISOString","resetHash","toString","save","resetUrl","publicUrl","replace","encodeURIComponent","undefined","mail","render","template","PASSWORD_RESET","params","code","url","expiresInMinutes","locale","send","to","record","scope","IDENTITY","PASSWORD_RESET_REQUESTED","refType","USER","refId","actorType","actorId","actorName","resetPassword","runResetPasswordValidator","validateJoinColumns","token","findOneBy","credentialsService","hashedPassword","protect","password","resetAt","PASSWORD_RESET_COMPLETED","validator","oneOfContainer","oneOf","mount","string","trim","toLowerCase","regex","message","min","optional","uuid","nullable","run","max","passwordMinLength"],"sources":["../../../../src/core/identity/password-recovery/service.ts"],"sourcesContent":["/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n EventName,\n EventRefType,\n EventScope,\n IdentityType,\n USER_PASSWORD_MAX_LENGTH,\n USER_PASSWORD_MIN_LENGTH,\n} from '@authup/core-kit';\nimport { EntityNotFoundError, ValidationError } from '@authup/errors';\nimport { PasswordRecoveryDisabledError } from './disabled.ts';\nimport { EmailVerificationRequiredError } from './email-verification-required.ts';\nimport { ResetTokenExpiredError } from './token-expired.ts';\nimport { createValidator } from '@validup/zod';\nimport { randomBytes } from 'node:crypto';\nimport { Container } from 'validup';\nimport { z } from 'zod';\nimport { UserCredentialsService } from '../../authentication/credential/entities/user/module.ts';\nimport type {\n IPasswordRecoveryService,\n PasswordForgotResult,\n PasswordRecoveryServiceContext,\n PasswordRecoveryServiceOptions,\n PasswordResetResult,\n} from './types.ts';\nimport type { IMailClient, IMailTemplateRenderer } from '../../mail/types.ts';\nimport { MailTemplateName } from '../../mail/index.ts';\nimport type { IEventService, IRealmRepository, IUserRepository } from '../../entities/index.ts';\nimport type { IdentityWorkflowContext } from '../types.ts';\nimport { PASSWORD_RESET_EXPIRES_IN_MINUTES } from './constants.ts';\n\nexport class PasswordRecoveryService implements IPasswordRecoveryService {\n protected options: PasswordRecoveryServiceOptions;\n\n protected repository: IUserRepository;\n\n protected realmRepository: IRealmRepository;\n\n protected mailClient: IMailClient;\n\n protected mailTemplateRenderer: IMailTemplateRenderer;\n\n protected eventService?: IEventService;\n\n constructor(ctx: PasswordRecoveryServiceContext) {\n this.options = ctx.options;\n this.repository = ctx.repository;\n this.realmRepository = ctx.realmRepository;\n this.mailClient = ctx.mailClient;\n this.mailTemplateRenderer = ctx.mailTemplateRenderer;\n this.eventService = ctx.eventService;\n }\n\n async forgotPassword(data: Record<string, any>, context?: IdentityWorkflowContext): Promise<PasswordForgotResult> {\n if (!this.options.passwordRecoveryEnabled) {\n throw new PasswordRecoveryDisabledError();\n }\n\n if (!this.options.emailVerificationEnabled) {\n throw new EmailVerificationRequiredError('Email verification is not enabled, but required to reset a password.');\n }\n\n const validated = await this.runForgotPasswordValidator(data);\n\n const realm = await this.realmRepository.resolve(validated.realmId, true);\n\n const where: Record<string, any> = {\n ...(validated.name ? { name: validated.name } : {}),\n ...(validated.email ? { email: validated.email } : {}),\n realmId: realm.id,\n };\n\n const entity = await this.repository.findOneByWithEmail(where);\n\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n const merged = this.repository.merge(entity, {\n resetExpires: new Date(\n Date.now() + (1000 * 60 * PASSWORD_RESET_EXPIRES_IN_MINUTES),\n ).toISOString(),\n resetHash: randomBytes(32).toString('hex'),\n });\n\n await this.repository.save(merged);\n\n try {\n // realmId MUST ride the link: resetPassword resolves the realm\n // from the request and filters the lookup by it, so a realm-less\n // link resolves to the master realm and never matches a\n // non-master user.\n const resetUrl = this.options.publicUrl ?\n `${this.options.publicUrl.replace(/\\/+$/, '')}/password-reset` +\n `?token=${encodeURIComponent(merged.resetHash!)}` +\n `&realmId=${encodeURIComponent(entity.realmId)}` :\n undefined;\n\n const mail = await this.mailTemplateRenderer.render({\n template: MailTemplateName.PASSWORD_RESET,\n params: {\n code: merged.resetHash!,\n url: resetUrl,\n expiresInMinutes: PASSWORD_RESET_EXPIRES_IN_MINUTES,\n },\n locale: context?.locale,\n });\n\n await this.mailClient.send({\n to: entity.email,\n ...mail,\n });\n } catch {\n this.repository.merge(merged, {\n resetHash: null,\n resetExpires: null,\n });\n await this.repository.save(merged);\n\n throw new ValidationError('Password recovery failed. Could not send reset email.');\n }\n\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name: EventName.PASSWORD_RESET_REQUESTED,\n refType: EventRefType.USER,\n refId: merged.id,\n actorType: IdentityType.USER,\n actorId: merged.id,\n actorName: merged.name,\n realmId: merged.realmId,\n });\n\n return { resetExpires: merged.resetExpires! };\n }\n\n async resetPassword(data: Record<string, any>): Promise<PasswordResetResult> {\n if (!this.options.passwordRecoveryEnabled) {\n throw new PasswordRecoveryDisabledError();\n }\n\n const validated = await this.runResetPasswordValidator(data);\n\n await this.repository.validateJoinColumns(validated);\n\n const realm = await this.realmRepository.resolve(validated.realmId, true);\n\n const where: Record<string, any> = {\n ...(validated.name ? { name: validated.name } : {}),\n ...(validated.email ? { email: validated.email } : {}),\n resetHash: validated.token,\n realmId: realm.id,\n };\n\n const entity = await this.repository.findOneBy(where);\n if (!entity) {\n throw new EntityNotFoundError();\n }\n\n if (!entity.resetExpires || new Date(entity.resetExpires) < new Date()) {\n throw new ResetTokenExpiredError();\n }\n\n const credentialsService = new UserCredentialsService();\n const hashedPassword = await credentialsService.protect(validated.password);\n\n const merged = this.repository.merge(entity, {\n resetAt: new Date().toISOString(),\n resetHash: null,\n resetExpires: null,\n password: hashedPassword,\n });\n\n await this.repository.save(merged);\n\n await this.eventService?.record({\n scope: EventScope.IDENTITY,\n name: EventName.PASSWORD_RESET_COMPLETED,\n refType: EventRefType.USER,\n refId: merged.id,\n actorType: IdentityType.USER,\n actorId: merged.id,\n actorName: merged.name,\n realmId: merged.realmId,\n });\n\n return { resetAt: merged.resetAt! };\n }\n\n private async runForgotPasswordValidator(data: Record<string, any>) {\n const validator = new Container<any>({});\n\n const oneOfContainer = new Container({ oneOf: true });\n\n oneOfContainer.mount(\n 'email',\n createValidator(\n z.string()\n .trim()\n .toLowerCase()\n .email()\n .regex(/^[^A-Z]+$/, { message: 'Email must be lowercase.' }),\n ),\n );\n\n oneOfContainer.mount(\n 'name',\n createValidator(z.string().trim().toLowerCase().min(1)),\n );\n\n validator.mount(oneOfContainer);\n\n validator.mount(\n 'realmId',\n { optional: true },\n createValidator(z.uuid().nullable()),\n );\n\n return validator.run(data);\n }\n\n private async runResetPasswordValidator(data: Record<string, any>) {\n const validator = new Container<any>({});\n\n const oneOfContainer = new Container({ oneOf: true });\n oneOfContainer.mount(\n 'email',\n createValidator(\n z.string()\n .trim()\n .toLowerCase()\n .email()\n .regex(/^[^A-Z]+$/, { message: 'Email must be lowercase.' }),\n ),\n );\n oneOfContainer.mount(\n 'name',\n createValidator(z.string().trim().toLowerCase().min(1)),\n );\n\n validator.mount(oneOfContainer);\n\n validator.mount(\n 'realmId',\n { optional: true },\n createValidator(z.uuid().nullable()),\n );\n\n validator.mount(\n 'token',\n createValidator(z.string().min(3).max(256)),\n );\n\n validator.mount(\n 'password',\n createValidator(\n z.string()\n .min(this.options.passwordMinLength ?? USER_PASSWORD_MIN_LENGTH)\n .max(USER_PASSWORD_MAX_LENGTH),\n ),\n );\n\n return validator.run(data);\n }\n}\n"],"mappings":"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"}
|
|
@@ -65,7 +65,6 @@ var ClientProvisioningSynchronizer = class extends BaseProvisioningSynchronizer
|
|
|
65
65
|
case ProvisioningEntityStrategyType.REPLACE:
|
|
66
66
|
input.attributes.id = attributes.id;
|
|
67
67
|
attributes = await this.clientRepository.save(this.clientRepository.create(input.attributes));
|
|
68
|
-
break;
|
|
69
68
|
}
|
|
70
69
|
else attributes = await this.clientRepository.save(this.clientRepository.create(input.attributes));
|
|
71
70
|
const permissions = [...await this.permissionResolver.resolveGlobal(input.relations && input.relations.globalPermissions), ...attributes.realmId ? await this.permissionResolver.resolveRealm(input.relations && input.relations.realmPermissions, attributes.realmId) : []];
|