@contember/engine-tenant-api 2.2.0-alpha.5 → 2.2.0-alpha.7
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/CLAUDE.md +1 -0
- package/dist/development/index.cjs +2 -0
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +2 -0
- package/dist/development/index.js.map +1 -1
- package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs +18 -0
- package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs.map +1 -0
- package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js +19 -0
- package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js.map +1 -0
- package/dist/development/src/migrations/runner.cjs +3 -1
- package/dist/development/src/migrations/runner.cjs.map +1 -1
- package/dist/development/src/migrations/runner.js +3 -1
- package/dist/development/src/migrations/runner.js.map +1 -1
- package/dist/development/src/migrations/snapshot.cjs +2 -1
- package/dist/development/src/migrations/snapshot.cjs.map +1 -1
- package/dist/development/src/migrations/snapshot.js +2 -1
- package/dist/development/src/migrations/snapshot.js.map +1 -1
- package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs +3 -7
- package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs.map +1 -1
- package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.js +3 -7
- package/dist/development/src/model/commands/idp/ClaimIdpRevalidationCommand.js.map +1 -1
- package/dist/development/src/model/commands/idp/CreateIdpCommand.cjs +8 -2
- package/dist/development/src/model/commands/idp/CreateIdpCommand.cjs.map +1 -1
- package/dist/development/src/model/commands/idp/CreateIdpCommand.js +8 -2
- package/dist/development/src/model/commands/idp/CreateIdpCommand.js.map +1 -1
- package/dist/development/src/model/commands/idp/UpdateIdpCommand.cjs +6 -2
- package/dist/development/src/model/commands/idp/UpdateIdpCommand.cjs.map +1 -1
- package/dist/development/src/model/commands/idp/UpdateIdpCommand.js +6 -2
- package/dist/development/src/model/commands/idp/UpdateIdpCommand.js.map +1 -1
- package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs +21 -0
- package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs.map +1 -0
- package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js +21 -0
- package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js.map +1 -0
- package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.cjs +1 -1
- package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.cjs.map +1 -1
- package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.js +1 -1
- package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.js.map +1 -1
- package/dist/development/src/model/queries/idp/helpers.cjs +3 -3
- package/dist/development/src/model/queries/idp/helpers.cjs.map +1 -1
- package/dist/development/src/model/queries/idp/helpers.js +3 -3
- package/dist/development/src/model/queries/idp/helpers.js.map +1 -1
- package/dist/development/src/model/service/PasswordResetManager.cjs +5 -0
- package/dist/development/src/model/service/PasswordResetManager.cjs.map +1 -1
- package/dist/development/src/model/service/PasswordResetManager.js +5 -0
- package/dist/development/src/model/service/PasswordResetManager.js.map +1 -1
- package/dist/development/src/model/service/PasswordlessSignInManager.cjs +19 -0
- package/dist/development/src/model/service/PasswordlessSignInManager.cjs.map +1 -1
- package/dist/development/src/model/service/PasswordlessSignInManager.js +19 -0
- package/dist/development/src/model/service/PasswordlessSignInManager.js.map +1 -1
- package/dist/development/src/model/service/SignInManager.cjs +5 -0
- package/dist/development/src/model/service/SignInManager.cjs.map +1 -1
- package/dist/development/src/model/service/SignInManager.js +5 -0
- package/dist/development/src/model/service/SignInManager.js.map +1 -1
- package/dist/development/src/model/service/idp/PersonIdentityProviderManager.cjs +6 -0
- package/dist/development/src/model/service/idp/PersonIdentityProviderManager.cjs.map +1 -1
- package/dist/development/src/model/service/idp/PersonIdentityProviderManager.js +6 -0
- package/dist/development/src/model/service/idp/PersonIdentityProviderManager.js.map +1 -1
- package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs +2 -1
- package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs.map +1 -1
- package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.js +2 -1
- package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.js.map +1 -1
- package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs +2 -1
- package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs.map +1 -1
- package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js +2 -1
- package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js.map +1 -1
- package/dist/development/src/resolvers/mutation/idp/audit.cjs +1 -0
- package/dist/development/src/resolvers/mutation/idp/audit.cjs.map +1 -1
- package/dist/development/src/resolvers/mutation/idp/audit.js +1 -0
- package/dist/development/src/resolvers/mutation/idp/audit.js.map +1 -1
- package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs +4 -0
- package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs.map +1 -1
- package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.js +4 -0
- package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.js.map +1 -1
- package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.cjs +2 -2
- package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.cjs.map +1 -1
- package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.js +2 -2
- package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.js.map +1 -1
- package/dist/development/src/schema/tenant.graphql.cjs +28 -0
- package/dist/development/src/schema/tenant.graphql.cjs.map +1 -1
- package/dist/development/src/schema/tenant.graphql.js +28 -0
- package/dist/development/src/schema/tenant.graphql.js.map +1 -1
- package/dist/production/index.cjs +2 -0
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +2 -0
- package/dist/production/index.js.map +1 -1
- package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs +18 -0
- package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs.map +1 -0
- package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js +19 -0
- package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js.map +1 -0
- package/dist/production/src/migrations/runner.cjs +3 -1
- package/dist/production/src/migrations/runner.cjs.map +1 -1
- package/dist/production/src/migrations/runner.js +3 -1
- package/dist/production/src/migrations/runner.js.map +1 -1
- package/dist/production/src/migrations/snapshot.cjs +2 -1
- package/dist/production/src/migrations/snapshot.cjs.map +1 -1
- package/dist/production/src/migrations/snapshot.js +2 -1
- package/dist/production/src/migrations/snapshot.js.map +1 -1
- package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs +3 -7
- package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.cjs.map +1 -1
- package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.js +3 -7
- package/dist/production/src/model/commands/idp/ClaimIdpRevalidationCommand.js.map +1 -1
- package/dist/production/src/model/commands/idp/CreateIdpCommand.cjs +8 -2
- package/dist/production/src/model/commands/idp/CreateIdpCommand.cjs.map +1 -1
- package/dist/production/src/model/commands/idp/CreateIdpCommand.js +8 -2
- package/dist/production/src/model/commands/idp/CreateIdpCommand.js.map +1 -1
- package/dist/production/src/model/commands/idp/UpdateIdpCommand.cjs +6 -2
- package/dist/production/src/model/commands/idp/UpdateIdpCommand.cjs.map +1 -1
- package/dist/production/src/model/commands/idp/UpdateIdpCommand.js +6 -2
- package/dist/production/src/model/commands/idp/UpdateIdpCommand.js.map +1 -1
- package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs +21 -0
- package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs.map +1 -0
- package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js +21 -0
- package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js.map +1 -0
- package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.cjs +1 -1
- package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.cjs.map +1 -1
- package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.js +1 -1
- package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.js.map +1 -1
- package/dist/production/src/model/queries/idp/helpers.cjs +3 -3
- package/dist/production/src/model/queries/idp/helpers.cjs.map +1 -1
- package/dist/production/src/model/queries/idp/helpers.js +3 -3
- package/dist/production/src/model/queries/idp/helpers.js.map +1 -1
- package/dist/production/src/model/service/PasswordResetManager.cjs +5 -0
- package/dist/production/src/model/service/PasswordResetManager.cjs.map +1 -1
- package/dist/production/src/model/service/PasswordResetManager.js +5 -0
- package/dist/production/src/model/service/PasswordResetManager.js.map +1 -1
- package/dist/production/src/model/service/PasswordlessSignInManager.cjs +19 -0
- package/dist/production/src/model/service/PasswordlessSignInManager.cjs.map +1 -1
- package/dist/production/src/model/service/PasswordlessSignInManager.js +19 -0
- package/dist/production/src/model/service/PasswordlessSignInManager.js.map +1 -1
- package/dist/production/src/model/service/SignInManager.cjs +5 -0
- package/dist/production/src/model/service/SignInManager.cjs.map +1 -1
- package/dist/production/src/model/service/SignInManager.js +5 -0
- package/dist/production/src/model/service/SignInManager.js.map +1 -1
- package/dist/production/src/model/service/idp/PersonIdentityProviderManager.cjs +6 -0
- package/dist/production/src/model/service/idp/PersonIdentityProviderManager.cjs.map +1 -1
- package/dist/production/src/model/service/idp/PersonIdentityProviderManager.js +6 -0
- package/dist/production/src/model/service/idp/PersonIdentityProviderManager.js.map +1 -1
- package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs +2 -1
- package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs.map +1 -1
- package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.js +2 -1
- package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.js.map +1 -1
- package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs +2 -1
- package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs.map +1 -1
- package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js +2 -1
- package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js.map +1 -1
- package/dist/production/src/resolvers/mutation/idp/audit.cjs +1 -0
- package/dist/production/src/resolvers/mutation/idp/audit.cjs.map +1 -1
- package/dist/production/src/resolvers/mutation/idp/audit.js +1 -0
- package/dist/production/src/resolvers/mutation/idp/audit.js.map +1 -1
- package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs +4 -0
- package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs.map +1 -1
- package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.js +4 -0
- package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.js.map +1 -1
- package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.cjs +2 -2
- package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.cjs.map +1 -1
- package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.js +2 -2
- package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.js.map +1 -1
- package/dist/production/src/schema/tenant.graphql.cjs +28 -0
- package/dist/production/src/schema/tenant.graphql.cjs.map +1 -1
- package/dist/production/src/schema/tenant.graphql.js +28 -0
- package/dist/production/src/schema/tenant.graphql.js.map +1 -1
- package/dist/tests/cases/integration/mocked/gql/passwordless.d.ts +3 -0
- package/dist/tests/cases/integration/mocked/gql/passwordless.d.ts.map +1 -1
- package/dist/tests/cases/integration/mocked/localAuthenticationDisabled.test.d.ts +2 -0
- package/dist/tests/cases/integration/mocked/localAuthenticationDisabled.test.d.ts.map +1 -0
- package/dist/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.d.ts +10 -0
- package/dist/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.d.ts.map +1 -0
- package/dist/tests/cases/integration/mocked/sql/personIdentityProvidersSql.d.ts +1 -0
- package/dist/tests/cases/integration/mocked/sql/personIdentityProvidersSql.d.ts.map +1 -1
- package/dist/tests/cases/unit/idp/claimIdpRevalidation.test.d.ts +2 -0
- package/dist/tests/cases/unit/idp/claimIdpRevalidation.test.d.ts.map +1 -0
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/migrations/2026-09-04-120000-idp-disable-local-authentication.d.ts +3 -0
- package/dist/types/migrations/2026-09-04-120000-idp-disable-local-authentication.d.ts.map +1 -0
- package/dist/types/migrations/runner.d.ts.map +1 -1
- package/dist/types/migrations/snapshot.d.ts.map +1 -1
- package/dist/types/model/commands/idp/ClaimIdpRevalidationCommand.d.ts.map +1 -1
- package/dist/types/model/commands/idp/CreateIdpCommand.d.ts.map +1 -1
- package/dist/types/model/commands/idp/UpdateIdpCommand.d.ts.map +1 -1
- package/dist/types/model/queries/idp/LocalAuthDisablingIdpsQuery.d.ts +18 -0
- package/dist/types/model/queries/idp/LocalAuthDisablingIdpsQuery.d.ts.map +1 -0
- package/dist/types/model/queries/idp/PersonIdentityProvidersQuery.d.ts +1 -0
- package/dist/types/model/queries/idp/PersonIdentityProvidersQuery.d.ts.map +1 -1
- package/dist/types/model/queries/idp/helpers.d.ts +1 -1
- package/dist/types/model/queries/idp/helpers.d.ts.map +1 -1
- package/dist/types/model/queries/idp/index.d.ts +1 -0
- package/dist/types/model/queries/idp/index.d.ts.map +1 -1
- package/dist/types/model/queries/idp/types.d.ts +1 -0
- package/dist/types/model/queries/idp/types.d.ts.map +1 -1
- package/dist/types/model/service/PasswordResetManager.d.ts.map +1 -1
- package/dist/types/model/service/PasswordlessSignInManager.d.ts.map +1 -1
- package/dist/types/model/service/SignInManager.d.ts.map +1 -1
- package/dist/types/model/service/idp/PersonIdentityProviderManager.d.ts.map +1 -1
- package/dist/types/model/type/IdentityProvider.d.ts +2 -0
- package/dist/types/model/type/IdentityProvider.d.ts.map +1 -1
- package/dist/types/resolvers/mutation/idp/AddIDPMutationResolver.d.ts.map +1 -1
- package/dist/types/resolvers/mutation/idp/UpdateIDPMutationResolver.d.ts.map +1 -1
- package/dist/types/resolvers/mutation/idp/audit.d.ts.map +1 -1
- package/dist/types/resolvers/mutation/person/PasswordlessMutationResolver.d.ts.map +1 -1
- package/dist/types/schema/index.d.ts +33 -4
- package/dist/types/schema/index.d.ts.map +1 -1
- package/dist/types/schema/tenant.graphql.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/migrations/2026-09-04-120000-idp-disable-local-authentication.ts +18 -0
- package/src/migrations/runner.ts +2 -0
- package/src/migrations/snapshot.ts +2 -1
- package/src/model/commands/idp/ClaimIdpRevalidationCommand.ts +8 -7
- package/src/model/commands/idp/CreateIdpCommand.ts +7 -1
- package/src/model/commands/idp/UpdateIdpCommand.ts +5 -1
- package/src/model/queries/idp/LocalAuthDisablingIdpsQuery.ts +41 -0
- package/src/model/queries/idp/PersonIdentityProvidersQuery.ts +2 -0
- package/src/model/queries/idp/helpers.ts +3 -2
- package/src/model/queries/idp/index.ts +1 -0
- package/src/model/queries/idp/types.ts +1 -0
- package/src/model/service/PasswordResetManager.ts +9 -1
- package/src/model/service/PasswordlessSignInManager.ts +23 -1
- package/src/model/service/SignInManager.ts +9 -1
- package/src/model/service/idp/PersonIdentityProviderManager.ts +8 -0
- package/src/model/type/IdentityProvider.ts +2 -0
- package/src/resolvers/mutation/idp/AddIDPMutationResolver.ts +1 -0
- package/src/resolvers/mutation/idp/UpdateIDPMutationResolver.ts +1 -0
- package/src/resolvers/mutation/idp/audit.ts +1 -0
- package/src/resolvers/mutation/person/PasswordlessMutationResolver.ts +7 -0
- package/src/resolvers/mutation/person/SignInMutationResolver.ts +2 -2
- package/src/schema/index.ts +29 -0
- package/src/schema/tenant.graphql.ts +28 -0
- package/tests/cases/integration/mocked/gql/passwordless.ts +10 -0
- package/tests/cases/integration/mocked/idpClaimMappingConfig.test.ts +4 -4
- package/tests/cases/integration/mocked/localAuthenticationDisabled.test.ts +182 -0
- package/tests/cases/integration/mocked/personIdentityProviders.test.ts +67 -0
- package/tests/cases/integration/mocked/signInAnomalyDetection.test.ts +11 -0
- package/tests/cases/integration/mocked/signInIdpClaimMapping.test.ts +14 -0
- package/tests/cases/integration/mocked/signInIdpClaimMappingFixes.test.ts +4 -0
- package/tests/cases/integration/mocked/signInIdpClaimMappingSafety.test.ts +6 -0
- package/tests/cases/integration/mocked/signInIdpMutation.test.ts +5 -0
- package/tests/cases/integration/mocked/signInMfaEnforcement.test.ts +7 -0
- package/tests/cases/integration/mocked/signInMutation.test.ts +14 -0
- package/tests/cases/integration/mocked/signInPasswordlessMutation.test.ts +36 -0
- package/tests/cases/integration/mocked/sql/getIdpBySlugSql.ts +1 -1
- package/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.ts +21 -0
- package/tests/cases/integration/mocked/sql/personIdentityProvidersSql.ts +3 -1
- package/tests/cases/integration/mocked/trustForwardedClientInfo.test.ts +3 -0
- package/tests/cases/unit/idp/claimIdpRevalidation.test.ts +50 -0
- package/tests/cases/unit/idp/idpManagerUnknownConfigKeys.test.ts +8 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignInManager.js","sources":["../../../../../../packages/engine-tenant-api/src/model/service/SignInManager.ts"],"sourcesContent":["import { CreateSessionTokenErrorCode, MfaEnrollment, SignInErrorCode } from '../../schema/index.js'\nimport { ApiKeyManager, AuthPolicyResolver, BackupCodeManager, EmailOtpManager, LoginRiskAnalyzer, OtpManager } from '../service/index.js'\nimport { ConfigurationQuery, PersonQuery, PersonRow, PersonUniqueIdentifier } from '../queries/index.js'\nimport { Config } from '../type/Config.js'\nimport { Providers } from '../providers.js'\nimport { DatabaseContext } from '../utils/index.js'\nimport { Response, ResponseError, ResponseOk } from '../utils/Response.js'\nimport { ImplementationException } from '../../exceptions.js'\nimport { AuthLogService } from './AuthLogService.js'\nimport { UserMailer } from '../mailing/index.js'\nimport { ApiKeyRequestInfo, SetMfaGraceUntilCommand } from '../commands/index.js'\nimport { intervalToSeconds } from '../utils/interval.js'\n\nclass SignInManager {\n\tconstructor(\n\t\tprivate readonly apiKeyManager: ApiKeyManager,\n\t\tprivate readonly providers: Providers,\n\t\tprivate readonly otpManager: OtpManager,\n\t\tprivate readonly backupCodeManager: BackupCodeManager,\n\t\tprivate readonly emailOtpManager: EmailOtpManager,\n\t\tprivate readonly authPolicyResolver: AuthPolicyResolver,\n\t\tprivate readonly loginRiskAnalyzer: LoginRiskAnalyzer,\n\t\tprivate readonly userMailer: UserMailer,\n\t) {}\n\n\tasync signIn(\n\t\tdbContext: DatabaseContext,\n\t\temail: string,\n\t\tpassword: string,\n\t\texpiration?: number,\n\t\totpCode?: string,\n\t\trequestInfo?: ApiKeyRequestInfo,\n\t\ttrustForwardedInfo?: boolean,\n\t\tbackupCode?: string,\n\t\tconfig?: Config,\n\t\tgeoCountry?: string,\n\t): Promise<SignInResponse> {\n\t\tconst person = await dbContext.queryHandler.fetch(PersonQuery.byEmail(email))\n\n\t\tif (person === null) {\n\t\t\treturn new ResponseError('UNKNOWN_EMAIL', `Person with email ${email} not found`, {\n\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\tpersonInput: email,\n\t\t\t\t}),\n\t\t\t})\n\t\t}\n\n\t\tconst authLogData = {\n\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\tpersonInput: email,\n\t\t\t\tpersonId: person.id,\n\t\t\t}),\n\t\t}\n\t\tif (!person.password_hash) {\n\t\t\treturn new ResponseError('NO_PASSWORD_SET', `No password set`, authLogData)\n\t\t}\n\n\t\tif (person.disabled_at !== null) {\n\t\t\treturn new ResponseError('PERSON_DISABLED', `Person is disabled`, authLogData)\n\t\t}\n\n\t\tconst passwordValid = await this.providers.bcryptCompare(password, person.password_hash)\n\n\t\tif (!passwordValid) {\n\t\t\treturn new ResponseError('INVALID_PASSWORD', `Password does not match`, authLogData)\n\t\t}\n\n\t\t// Block sign-in for accounts that were created while email verification\n\t\t// was required but never proved ownership. Accounts predating the\n\t\t// requirement carry email_verification_required = false and pass through.\n\t\tif (person.email_verification_required && person.email_verified_at === null) {\n\t\t\treturn new ResponseError('EMAIL_NOT_VERIFIED', `E-mail address has not been verified`, authLogData)\n\t\t}\n\n\t\tlet usedBackupCode = false\n\t\t// Whether a second factor was actively proven in THIS request. Anomaly\n\t\t// step-up (A03) reuses this: a login that already cleared a second factor\n\t\t// is not asked to step up again.\n\t\tlet mfaSatisfiedThisRequest = false\n\t\tconst hasActiveTotp = Boolean(person.otp_secret && person.otp_activated_at)\n\t\tif (hasActiveTotp) {\n\t\t\t// Active TOTP takes precedence (A07 path, unchanged).\n\t\t\tif (otpCode) {\n\t\t\t\tif (!await this.otpManager.verifyOtp(dbContext, person, otpCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else if (backupCode) {\n\t\t\t\tif (!await this.backupCodeManager.verifyAndConsume(dbContext, person, backupCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tusedBackupCode = true\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else {\n\t\t\t\treturn new ResponseError('OTP_REQUIRED', `2FA is enabled. OTP token is required`, authLogData)\n\t\t\t}\n\t\t} else if (person.email_otp_enabled) {\n\t\t\t// No active TOTP, but email OTP is enabled (A05).\n\t\t\tif (otpCode) {\n\t\t\t\tif (!await this.emailOtpManager.verifyAndConsume(dbContext, person, otpCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else if (backupCode) {\n\t\t\t\tif (!await this.backupCodeManager.verifyAndConsume(dbContext, person, backupCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tusedBackupCode = true\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else {\n\t\t\t\t// Dispatch a fresh code and ask the client to retry with otpToken. Reuses\n\t\t\t\t// the existing OTP_REQUIRED contract so clients that already handle it work.\n\t\t\t\t// The send is rate-limited per person; when throttled nothing is emailed\n\t\t\t\t// (emailOtpSent: false) but a previously emailed code stays valid.\n\t\t\t\t// Independent fetch (not the passed-in `config`): the email-OTP send is\n\t\t\t\t// rate-limited by the live config and this branch never reaches the\n\t\t\t\t// anomaly analyzer, so query shape stays as before A03.\n\t\t\t\tconst emailOtpConfig = await dbContext.queryHandler.fetch(new ConfigurationQuery(dbContext.providers))\n\t\t\t\tconst decision = await this.emailOtpManager.sendCode(dbContext, person, emailOtpConfig)\n\t\t\t\treturn new ResponseError('OTP_REQUIRED', `2FA is enabled. OTP token is required`, {\n\t\t\t\t\temailOtpSent: decision.ok,\n\t\t\t\t\t...authLogData,\n\t\t\t\t})\n\t\t\t}\n\t\t} else {\n\t\t\t// No active factor at all (A06). Enforce MFA if a matching auth_policy\n\t\t\t// requires it. With no policy rows configured this resolves to\n\t\t\t// mfaRequired=false and sign-in proceeds exactly as before.\n\t\t\tconst enforcement = await this.enforceMfaEnrollment(dbContext, person, otpCode, authLogData)\n\t\t\tif (enforcement !== null) {\n\t\t\t\tif ('error' in enforcement) {\n\t\t\t\t\treturn enforcement\n\t\t\t\t}\n\t\t\t\t// Enrollment completed in this request (pending TOTP promoted to active),\n\t\t\t\t// so MFA is definitively satisfied — anomaly step-up cannot apply.\n\t\t\t\tconst riskHandled = await this.applyRiskPolicy(dbContext, person, {\n\t\t\t\t\tconfig,\n\t\t\t\t\tgeoCountry,\n\t\t\t\t\trequestInfo,\n\t\t\t\t\totpCode,\n\t\t\t\t\tmfaSatisfiedThisRequest: true,\n\t\t\t\t\tauthLogData,\n\t\t\t\t})\n\t\t\t\tif (riskHandled !== null && 'error' in riskHandled) {\n\t\t\t\t\treturn riskHandled\n\t\t\t\t}\n\t\t\t\tconst sessionToken = await this.apiKeyManager.createSessionApiKey(\n\t\t\t\t\tdbContext,\n\t\t\t\t\tperson.identity_id,\n\t\t\t\t\texpiration,\n\t\t\t\t\trequestInfo,\n\t\t\t\t\ttrustForwardedInfo,\n\t\t\t\t)\n\t\t\t\treturn new ResponseOk({\n\t\t\t\t\tperson,\n\t\t\t\t\ttoken: sessionToken,\n\t\t\t\t\tbackupCodes: enforcement.backupCodes,\n\t\t\t\t\tunusualLoginDetected: riskHandled?.unusualLoginDetected || undefined,\n\t\t\t\t\trisk: riskHandled?.risk,\n\t\t\t\t\t...authLogData,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tconst riskHandled = await this.applyRiskPolicy(dbContext, person, {\n\t\t\tconfig,\n\t\t\tgeoCountry,\n\t\t\trequestInfo,\n\t\t\totpCode,\n\t\t\tmfaSatisfiedThisRequest,\n\t\t\tauthLogData,\n\t\t})\n\t\tif (riskHandled !== null && 'error' in riskHandled) {\n\t\t\treturn riskHandled\n\t\t}\n\n\t\tconst sessionToken = await this.apiKeyManager.createSessionApiKey(dbContext, person.identity_id, expiration, requestInfo, trustForwardedInfo)\n\n\t\treturn new ResponseOk({\n\t\t\tperson,\n\t\t\ttoken: sessionToken,\n\t\t\tusedBackupCode,\n\t\t\tunusualLoginDetected: riskHandled?.unusualLoginDetected || undefined,\n\t\t\trisk: riskHandled?.risk,\n\t\t\t...authLogData,\n\t\t})\n\t}\n\n\t/**\n\t * A03 — evaluate the sign-in risk score and act per policy. Returns:\n\t * - `null` when anomaly detection is off or no `config` was supplied (the\n\t * default): no DB work, sign-in proceeds unchanged.\n\t * - `{ unusualLoginDetected: true }` when an informational UNUSUAL_LOGIN email\n\t * was sent (best-effort) but the session may still be issued.\n\t * - a `ResponseError('OTP_REQUIRED')` when step-up is required and no code was\n\t * supplied — reuses the email-OTP step-up channel (dispatches a code).\n\t * - a `ResponseError('INVALID_OTP_TOKEN')` when a step-up code was supplied but\n\t * did not verify. A valid code consumes the token and the session is issued.\n\t * - `{ unusualLoginDetected: false }` when the score was below every threshold.\n\t *\n\t * The audit entries (`unusual_login_detected`, `step_up_required`) are emitted\n\t * by the resolver from the response metadata, matching the project convention.\n\t */\n\tprivate async applyRiskPolicy(\n\t\tdbContext: DatabaseContext,\n\t\tperson: PersonRow,\n\t\targs: {\n\t\t\tconfig?: Config\n\t\t\tgeoCountry?: string\n\t\t\trequestInfo?: ApiKeyRequestInfo\n\t\t\totpCode?: string\n\t\t\tmfaSatisfiedThisRequest: boolean\n\t\t\tauthLogData: { [AuthLogService.Key]: AuthLogService.Bag }\n\t\t},\n\t): Promise<RiskPolicyOutcome> {\n\t\tconst { config, geoCountry, requestInfo, otpCode, mfaSatisfiedThisRequest, authLogData } = args\n\t\tconst policy = config?.login.anomalyDetection\n\t\tif (!config || !policy?.enabled) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst assessment = await this.loginRiskAnalyzer.analyze(dbContext, person.id, {\n\t\t\tgeoCountry: geoCountry ?? null,\n\t\t\tdeviceFingerprint: this.loginRiskAnalyzer.fingerprint(requestInfo?.userAgent),\n\t\t\tip: requestInfo?.ip ?? null,\n\t\t}, policy)\n\n\t\tif (assessment.action === 'allow') {\n\t\t\treturn { unusualLoginDetected: false }\n\t\t}\n\n\t\tconst riskMeta = { score: assessment.score, reasons: assessment.reasons }\n\n\t\tif (assessment.action === 'stepUp' && !mfaSatisfiedThisRequest) {\n\t\t\t// Reuse the existing email-OTP step-up channel. This is independent of\n\t\t\t// `person.email_otp_enabled`, which gates email OTP only as the *primary*\n\t\t\t// second factor (A05); the step-up challenge stands on its own, so a person\n\t\t\t// with no standing MFA can still complete it. Without consuming the code\n\t\t\t// here, such a person would never satisfy the step-up and would loop on\n\t\t\t// OTP_REQUIRED forever (the score is unchanged on every retry).\n\t\t\tif (otpCode) {\n\t\t\t\t// Retry leg: the client resent the emailed code as otpToken. Mirror the\n\t\t\t\t// A05 contract — an invalid code is rejected (not re-sent); the per-code\n\t\t\t\t// 3-attempt cap still applies, and a fresh code is issued only when the\n\t\t\t\t// client retries without a code.\n\t\t\t\tif (!await this.emailOtpManager.verifyAndConsume(dbContext, person, otpCode)) {\n\t\t\t\t\t// A failed *forced* second factor is itself an anomaly signal — carry the\n\t\t\t\t\t// step-up/risk metadata so the resolver still emits unusual_login_detected\n\t\t\t\t\t// + step_up_required with the score/reasons, rather than logging it as an\n\t\t\t\t\t// ordinary mistyped-OTP login indistinguishable from a normal 2FA typo.\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', {\n\t\t\t\t\t\tstepUpRequired: true,\n\t\t\t\t\t\tunusualLoginDetected: true,\n\t\t\t\t\t\trisk: riskMeta,\n\t\t\t\t\t\t...authLogData,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\t// Step-up satisfied this request — fall through to notify + allow below.\n\t\t\t} else {\n\t\t\t\t// First leg: dispatch a code and ask the client to retry with otpToken.\n\t\t\t\t// Rate-limited per person; a previously emailed code stays valid when throttled.\n\t\t\t\tconst decision = await this.emailOtpManager.sendCode(dbContext, person, config)\n\t\t\t\treturn new ResponseError('OTP_REQUIRED', 'Additional verification is required for this sign-in', {\n\t\t\t\t\temailOtpSent: decision.ok,\n\t\t\t\t\tstepUpRequired: true,\n\t\t\t\t\tunusualLoginDetected: true,\n\t\t\t\t\trisk: riskMeta,\n\t\t\t\t\t...authLogData,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// action === 'email', or step-up satisfied (already this request, or just now\n\t\t// via the emailed code): notify the person of the unusual sign-in. A failed\n\t\t// email must never block sign-in.\n\t\tawait this.sendUnusualLoginEmail(dbContext, person, geoCountry ?? null, requestInfo?.ip ?? null)\n\t\treturn { unusualLoginDetected: true, risk: riskMeta }\n\t}\n\n\tprivate async sendUnusualLoginEmail(dbContext: DatabaseContext, person: PersonRow, geoCountry: string | null, ip: string | null): Promise<void> {\n\t\tif (!person.email) {\n\t\t\t// No address to notify (e.g. an IDP-only person) — nothing to send.\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait this.userMailer.sendUnusualLoginEmail(dbContext, {\n\t\t\t\temail: person.email,\n\t\t\t\tgeoCountry,\n\t\t\t\tipAddress: ip,\n\t\t\t}, { projectId: null, variant: '' })\n\t\t} catch {\n\t\t\t// ignore — informational only\n\t\t}\n\t}\n\n\t/**\n\t * A06 enforcement for the no-active-factor case. Returns:\n\t * - `null` when MFA is not required, or grace currently allows sign-in\n\t * (caller proceeds to issue a session as normal). With zero auth_policy\n\t * rows this is always the outcome — no behavior change.\n\t * - a `ResponseError` (MFA_ENROLLMENT_REQUIRED with the pending secret to\n\t * enroll, or INVALID_OTP_TOKEN) when sign-in must be blocked.\n\t * - `{ ok: true, backupCodes }` when enrollment just completed (pending TOTP\n\t * promoted to active); caller issues the session.\n\t */\n\tprivate async enforceMfaEnrollment(\n\t\tdbContext: DatabaseContext,\n\t\tperson: PersonRow,\n\t\totpCode: string | undefined,\n\t\tauthLogData: { [AuthLogService.Key]: AuthLogService.Bag },\n\t): Promise<\n\t\t| null\n\t\t| ResponseError<SignInErrorCode, {\n\t\t\temailOtpSent?: boolean\n\t\t\tmfaEnrollmentRequired?: boolean\n\t\t\tmfaEnrollment?: MfaEnrollment\n\t\t\t[AuthLogService.Key]: AuthLogService.Bag\n\t\t}>\n\t\t| { backupCodes: string[] }\n\t> {\n\t\tconst policy = await this.authPolicyResolver.resolveForIdentity(dbContext, person.identity_id, person.roles)\n\t\tif (!policy.mfaRequired) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst now = this.providers.now()\n\n\t\t// Resolve the effective grace duration (seconds). A per-policy override\n\t\t// wins; otherwise we fall back to the global config default. Only fetch the\n\t\t// config when the override is absent, to keep query shapes minimal.\n\t\tlet graceSeconds: number\n\t\tif (policy.graceDuration !== null) {\n\t\t\tgraceSeconds = intervalToSeconds(policy.graceDuration)\n\t\t} else {\n\t\t\tconst config = await dbContext.queryHandler.fetch(new ConfigurationQuery(dbContext.providers))\n\t\t\tgraceSeconds = intervalToSeconds(config.login.mfaGraceDuration)\n\t\t}\n\n\t\t// Grace handling. Default grace duration is 0 (immediate enforcement), so\n\t\t// by default neither branch below opens a window.\n\t\tif (person.mfa_grace_until !== null) {\n\t\t\tif (person.mfa_grace_until.getTime() > now.getTime()) {\n\t\t\t\t// Still inside an open grace window — allow sign-in.\n\t\t\t\treturn null\n\t\t\t}\n\t\t\t// Grace expired → fall through to enforcement.\n\t\t} else if (graceSeconds > 0) {\n\t\t\t// Open the grace window now and allow this sign-in.\n\t\t\tconst graceUntil = new Date(now.getTime() + graceSeconds * 1000)\n\t\t\tawait dbContext.commandBus.execute(new SetMfaGraceUntilCommand(person.id, graceUntil))\n\t\t\treturn null\n\t\t}\n\n\t\t// Enforce.\n\t\tif (otpCode && person.otp_pending_secret) {\n\t\t\t// Enrollment completion: verify against the pending secret, then promote.\n\t\t\tif (!await this.otpManager.verifyPendingOtp(person, otpCode)) {\n\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t}\n\t\t\tconst promoted = await this.otpManager.confirmOtp(dbContext, person)\n\t\t\tif (!promoted) {\n\t\t\t\t// Lost a concurrent enrollment-completion race: the pending secret was\n\t\t\t\t// already promoted by the other request (which issued the backup codes).\n\t\t\t\t// Don't regenerate here — that would invalidate the codes already\n\t\t\t\t// returned. TOTP is now active, so a retry signs in via the normal path.\n\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t}\n\t\t\tconst backupCodes = await this.backupCodeManager.generate(dbContext, person.id)\n\t\t\treturn { backupCodes }\n\t\t}\n\n\t\t// Provision a fresh pending TOTP secret and ask the client to enroll.\n\t\tconst otp = await this.otpManager.prepareOtp(dbContext, person, 'Contember')\n\t\tconst mfaEnrollment: MfaEnrollment = { otpUri: otp.uri, otpSecret: otp.secret }\n\t\treturn new ResponseError('MFA_ENROLLMENT_REQUIRED', 'MFA enrollment is required', {\n\t\t\tmfaEnrollmentRequired: true,\n\t\t\tmfaEnrollment,\n\t\t\t...authLogData,\n\t\t})\n\t}\n\n\tasync createSessionToken(\n\t\tdbContext: DatabaseContext,\n\t\tpersonIdentifier: PersonUniqueIdentifier,\n\t\texpiration?: number,\n\t\tverifier?: (person: PersonRow) => Promise<void>,\n\t\trequestInfo?: ApiKeyRequestInfo,\n\t\ttrustForwardedInfo?: boolean,\n\t): Promise<CreateSessionTokenResponse> {\n\t\tconst person = await dbContext.queryHandler.fetch(PersonQuery.byUniqueIdentifier(personIdentifier))\n\n\t\tif (person === null) {\n\t\t\tif (personIdentifier.type === 'email') {\n\t\t\t\treturn new ResponseError('UNKNOWN_EMAIL', `Person with email ${personIdentifier.email} not found`, {\n\t\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\t\tpersonInput: personIdentifier.email,\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t} else if (personIdentifier.type === 'id') {\n\t\t\t\treturn new ResponseError('UNKNOWN_PERSON_ID', `Person with id ${personIdentifier.id} not found`, {\n\t\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\t\tpersonInput: personIdentifier.id,\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tthrow new ImplementationException()\n\t\t}\n\n\t\tconst inputValue = personIdentifier.type === 'email' ? personIdentifier.email : personIdentifier.id\n\t\tif (person.disabled_at !== null) {\n\t\t\treturn new ResponseError('PERSON_DISABLED', `Person with id ${person.id} is disabled`, {\n\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\tpersonId: person.id,\n\t\t\t\t\tpersonInput: inputValue,\n\t\t\t\t}),\n\t\t\t})\n\t\t}\n\n\t\tawait verifier?.(person)\n\n\t\tconst sessionToken = await this.apiKeyManager.createSessionApiKey(dbContext, person.identity_id, expiration, requestInfo, trustForwardedInfo)\n\t\treturn new ResponseOk({\n\t\t\tperson,\n\t\t\ttoken: sessionToken,\n\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\tpersonId: person.id,\n\t\t\t\tpersonInput: inputValue,\n\t\t\t}),\n\t\t})\n\t}\n}\n\n/** A03: greppable payload describing why a sign-in scored as unusual. */\nexport interface RiskMeta {\n\treadonly score: number\n\treadonly reasons: readonly string[]\n}\n\n/**\n * Result of {@link SignInManager.applyRiskPolicy}: `null` = feature off / no\n * config; a metadata object when the session may still be issued; a\n * `ResponseError` when step-up blocks the session.\n */\ntype RiskPolicyOutcome =\n\t| null\n\t| { unusualLoginDetected: boolean; risk?: RiskMeta }\n\t| ResponseError<SignInErrorCode, {\n\t\temailOtpSent?: boolean\n\t\tstepUpRequired?: boolean\n\t\tunusualLoginDetected?: boolean\n\t\trisk?: RiskMeta\n\t\t[AuthLogService.Key]: AuthLogService.Bag\n\t}>\n\nexport interface SignInResult {\n\treadonly person: PersonRow\n\treadonly token: string\n\t/** True when MFA was satisfied by consuming a backup code (instead of a TOTP token). */\n\treadonly usedBackupCode?: boolean\n\t/** Set when sign-in completed an MFA enrollment (A06): the freshly issued backup codes, shown once. */\n\treadonly backupCodes?: string[]\n\t/** A03: true when this sign-in scored as unusual and an UNUSUAL_LOGIN email was sent. */\n\treadonly unusualLoginDetected?: boolean\n\t/** A03: the risk score + reasons, recorded in the `unusual_login_detected` audit `event_data`. */\n\treadonly risk?: RiskMeta\n\t[AuthLogService.Key]: AuthLogService.Bag\n}\n\nexport type SignInResponse = Response<SignInResult, SignInErrorCode, {\n\t/** Set on an OTP_REQUIRED error when an email-OTP code was just dispatched (A05). */\n\temailOtpSent?: boolean\n\t/** Set on a MFA_ENROLLMENT_REQUIRED error (A06): triggers the `mfa_enrollment_required` audit. */\n\tmfaEnrollmentRequired?: boolean\n\t/** Set on a MFA_ENROLLMENT_REQUIRED error (A06): the pending secret the client must enroll. */\n\tmfaEnrollment?: MfaEnrollment\n\t/** Set on an OTP_REQUIRED error raised by A03 anomaly step-up: triggers the `step_up_required` audit. */\n\tstepUpRequired?: boolean\n\t/** A03: true when the sign-in scored as unusual (triggers the `unusual_login_detected` audit). */\n\tunusualLoginDetected?: boolean\n\t/** A03: the risk score + reasons, recorded in the audit `event_data`. */\n\trisk?: RiskMeta\n\t[AuthLogService.Key]: AuthLogService.Bag\n}>\nexport type CreateSessionTokenResponse = Response<SignInResult, CreateSessionTokenErrorCode, {\n\t[AuthLogService.Key]: AuthLogService.Bag\n}>\n\nexport { SignInManager }\n"],"names":["riskHandled","sessionToken"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SignInManager.js","sources":["../../../../../../packages/engine-tenant-api/src/model/service/SignInManager.ts"],"sourcesContent":["import { CreateSessionTokenErrorCode, MfaEnrollment, SignInErrorCode } from '../../schema/index.js'\nimport { ApiKeyManager, AuthPolicyResolver, BackupCodeManager, EmailOtpManager, LoginRiskAnalyzer, OtpManager } from '../service/index.js'\nimport { ConfigurationQuery, LocalAuthDisablingIdpsQuery, PersonQuery, PersonRow, PersonUniqueIdentifier } from '../queries/index.js'\nimport { Config } from '../type/Config.js'\nimport { Providers } from '../providers.js'\nimport { DatabaseContext } from '../utils/index.js'\nimport { Response, ResponseError, ResponseOk } from '../utils/Response.js'\nimport { ImplementationException } from '../../exceptions.js'\nimport { AuthLogService } from './AuthLogService.js'\nimport { UserMailer } from '../mailing/index.js'\nimport { ApiKeyRequestInfo, SetMfaGraceUntilCommand } from '../commands/index.js'\nimport { intervalToSeconds } from '../utils/interval.js'\n\nclass SignInManager {\n\tconstructor(\n\t\tprivate readonly apiKeyManager: ApiKeyManager,\n\t\tprivate readonly providers: Providers,\n\t\tprivate readonly otpManager: OtpManager,\n\t\tprivate readonly backupCodeManager: BackupCodeManager,\n\t\tprivate readonly emailOtpManager: EmailOtpManager,\n\t\tprivate readonly authPolicyResolver: AuthPolicyResolver,\n\t\tprivate readonly loginRiskAnalyzer: LoginRiskAnalyzer,\n\t\tprivate readonly userMailer: UserMailer,\n\t) {}\n\n\tasync signIn(\n\t\tdbContext: DatabaseContext,\n\t\temail: string,\n\t\tpassword: string,\n\t\texpiration?: number,\n\t\totpCode?: string,\n\t\trequestInfo?: ApiKeyRequestInfo,\n\t\ttrustForwardedInfo?: boolean,\n\t\tbackupCode?: string,\n\t\tconfig?: Config,\n\t\tgeoCountry?: string,\n\t): Promise<SignInResponse> {\n\t\tconst person = await dbContext.queryHandler.fetch(PersonQuery.byEmail(email))\n\n\t\tif (person === null) {\n\t\t\treturn new ResponseError('UNKNOWN_EMAIL', `Person with email ${email} not found`, {\n\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\tpersonInput: email,\n\t\t\t\t}),\n\t\t\t})\n\t\t}\n\n\t\tconst authLogData = {\n\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\tpersonInput: email,\n\t\t\t\tpersonId: person.id,\n\t\t\t}),\n\t\t}\n\t\t// Refused before the password is even looked at: for a person federated\n\t\t// through a provider with `disableLocalAuthentication`, a password is not a\n\t\t// credential this tenant accepts, however it got set.\n\t\tconst exclusiveIdps = await dbContext.queryHandler.fetch(new LocalAuthDisablingIdpsQuery(person.id))\n\t\tif (exclusiveIdps.length > 0) {\n\t\t\treturn new ResponseError('IDP_REQUIRED', `Sign in using ${exclusiveIdps.join(' or ')}`, authLogData)\n\t\t}\n\n\t\tif (!person.password_hash) {\n\t\t\treturn new ResponseError('NO_PASSWORD_SET', `No password set`, authLogData)\n\t\t}\n\n\t\tif (person.disabled_at !== null) {\n\t\t\treturn new ResponseError('PERSON_DISABLED', `Person is disabled`, authLogData)\n\t\t}\n\n\t\tconst passwordValid = await this.providers.bcryptCompare(password, person.password_hash)\n\n\t\tif (!passwordValid) {\n\t\t\treturn new ResponseError('INVALID_PASSWORD', `Password does not match`, authLogData)\n\t\t}\n\n\t\t// Block sign-in for accounts that were created while email verification\n\t\t// was required but never proved ownership. Accounts predating the\n\t\t// requirement carry email_verification_required = false and pass through.\n\t\tif (person.email_verification_required && person.email_verified_at === null) {\n\t\t\treturn new ResponseError('EMAIL_NOT_VERIFIED', `E-mail address has not been verified`, authLogData)\n\t\t}\n\n\t\tlet usedBackupCode = false\n\t\t// Whether a second factor was actively proven in THIS request. Anomaly\n\t\t// step-up (A03) reuses this: a login that already cleared a second factor\n\t\t// is not asked to step up again.\n\t\tlet mfaSatisfiedThisRequest = false\n\t\tconst hasActiveTotp = Boolean(person.otp_secret && person.otp_activated_at)\n\t\tif (hasActiveTotp) {\n\t\t\t// Active TOTP takes precedence (A07 path, unchanged).\n\t\t\tif (otpCode) {\n\t\t\t\tif (!await this.otpManager.verifyOtp(dbContext, person, otpCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else if (backupCode) {\n\t\t\t\tif (!await this.backupCodeManager.verifyAndConsume(dbContext, person, backupCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tusedBackupCode = true\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else {\n\t\t\t\treturn new ResponseError('OTP_REQUIRED', `2FA is enabled. OTP token is required`, authLogData)\n\t\t\t}\n\t\t} else if (person.email_otp_enabled) {\n\t\t\t// No active TOTP, but email OTP is enabled (A05).\n\t\t\tif (otpCode) {\n\t\t\t\tif (!await this.emailOtpManager.verifyAndConsume(dbContext, person, otpCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else if (backupCode) {\n\t\t\t\tif (!await this.backupCodeManager.verifyAndConsume(dbContext, person, backupCode)) {\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t\t}\n\t\t\t\tusedBackupCode = true\n\t\t\t\tmfaSatisfiedThisRequest = true\n\t\t\t} else {\n\t\t\t\t// Dispatch a fresh code and ask the client to retry with otpToken. Reuses\n\t\t\t\t// the existing OTP_REQUIRED contract so clients that already handle it work.\n\t\t\t\t// The send is rate-limited per person; when throttled nothing is emailed\n\t\t\t\t// (emailOtpSent: false) but a previously emailed code stays valid.\n\t\t\t\t// Independent fetch (not the passed-in `config`): the email-OTP send is\n\t\t\t\t// rate-limited by the live config and this branch never reaches the\n\t\t\t\t// anomaly analyzer, so query shape stays as before A03.\n\t\t\t\tconst emailOtpConfig = await dbContext.queryHandler.fetch(new ConfigurationQuery(dbContext.providers))\n\t\t\t\tconst decision = await this.emailOtpManager.sendCode(dbContext, person, emailOtpConfig)\n\t\t\t\treturn new ResponseError('OTP_REQUIRED', `2FA is enabled. OTP token is required`, {\n\t\t\t\t\temailOtpSent: decision.ok,\n\t\t\t\t\t...authLogData,\n\t\t\t\t})\n\t\t\t}\n\t\t} else {\n\t\t\t// No active factor at all (A06). Enforce MFA if a matching auth_policy\n\t\t\t// requires it. With no policy rows configured this resolves to\n\t\t\t// mfaRequired=false and sign-in proceeds exactly as before.\n\t\t\tconst enforcement = await this.enforceMfaEnrollment(dbContext, person, otpCode, authLogData)\n\t\t\tif (enforcement !== null) {\n\t\t\t\tif ('error' in enforcement) {\n\t\t\t\t\treturn enforcement\n\t\t\t\t}\n\t\t\t\t// Enrollment completed in this request (pending TOTP promoted to active),\n\t\t\t\t// so MFA is definitively satisfied — anomaly step-up cannot apply.\n\t\t\t\tconst riskHandled = await this.applyRiskPolicy(dbContext, person, {\n\t\t\t\t\tconfig,\n\t\t\t\t\tgeoCountry,\n\t\t\t\t\trequestInfo,\n\t\t\t\t\totpCode,\n\t\t\t\t\tmfaSatisfiedThisRequest: true,\n\t\t\t\t\tauthLogData,\n\t\t\t\t})\n\t\t\t\tif (riskHandled !== null && 'error' in riskHandled) {\n\t\t\t\t\treturn riskHandled\n\t\t\t\t}\n\t\t\t\tconst sessionToken = await this.apiKeyManager.createSessionApiKey(\n\t\t\t\t\tdbContext,\n\t\t\t\t\tperson.identity_id,\n\t\t\t\t\texpiration,\n\t\t\t\t\trequestInfo,\n\t\t\t\t\ttrustForwardedInfo,\n\t\t\t\t)\n\t\t\t\treturn new ResponseOk({\n\t\t\t\t\tperson,\n\t\t\t\t\ttoken: sessionToken,\n\t\t\t\t\tbackupCodes: enforcement.backupCodes,\n\t\t\t\t\tunusualLoginDetected: riskHandled?.unusualLoginDetected || undefined,\n\t\t\t\t\trisk: riskHandled?.risk,\n\t\t\t\t\t...authLogData,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tconst riskHandled = await this.applyRiskPolicy(dbContext, person, {\n\t\t\tconfig,\n\t\t\tgeoCountry,\n\t\t\trequestInfo,\n\t\t\totpCode,\n\t\t\tmfaSatisfiedThisRequest,\n\t\t\tauthLogData,\n\t\t})\n\t\tif (riskHandled !== null && 'error' in riskHandled) {\n\t\t\treturn riskHandled\n\t\t}\n\n\t\tconst sessionToken = await this.apiKeyManager.createSessionApiKey(dbContext, person.identity_id, expiration, requestInfo, trustForwardedInfo)\n\n\t\treturn new ResponseOk({\n\t\t\tperson,\n\t\t\ttoken: sessionToken,\n\t\t\tusedBackupCode,\n\t\t\tunusualLoginDetected: riskHandled?.unusualLoginDetected || undefined,\n\t\t\trisk: riskHandled?.risk,\n\t\t\t...authLogData,\n\t\t})\n\t}\n\n\t/**\n\t * A03 — evaluate the sign-in risk score and act per policy. Returns:\n\t * - `null` when anomaly detection is off or no `config` was supplied (the\n\t * default): no DB work, sign-in proceeds unchanged.\n\t * - `{ unusualLoginDetected: true }` when an informational UNUSUAL_LOGIN email\n\t * was sent (best-effort) but the session may still be issued.\n\t * - a `ResponseError('OTP_REQUIRED')` when step-up is required and no code was\n\t * supplied — reuses the email-OTP step-up channel (dispatches a code).\n\t * - a `ResponseError('INVALID_OTP_TOKEN')` when a step-up code was supplied but\n\t * did not verify. A valid code consumes the token and the session is issued.\n\t * - `{ unusualLoginDetected: false }` when the score was below every threshold.\n\t *\n\t * The audit entries (`unusual_login_detected`, `step_up_required`) are emitted\n\t * by the resolver from the response metadata, matching the project convention.\n\t */\n\tprivate async applyRiskPolicy(\n\t\tdbContext: DatabaseContext,\n\t\tperson: PersonRow,\n\t\targs: {\n\t\t\tconfig?: Config\n\t\t\tgeoCountry?: string\n\t\t\trequestInfo?: ApiKeyRequestInfo\n\t\t\totpCode?: string\n\t\t\tmfaSatisfiedThisRequest: boolean\n\t\t\tauthLogData: { [AuthLogService.Key]: AuthLogService.Bag }\n\t\t},\n\t): Promise<RiskPolicyOutcome> {\n\t\tconst { config, geoCountry, requestInfo, otpCode, mfaSatisfiedThisRequest, authLogData } = args\n\t\tconst policy = config?.login.anomalyDetection\n\t\tif (!config || !policy?.enabled) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst assessment = await this.loginRiskAnalyzer.analyze(dbContext, person.id, {\n\t\t\tgeoCountry: geoCountry ?? null,\n\t\t\tdeviceFingerprint: this.loginRiskAnalyzer.fingerprint(requestInfo?.userAgent),\n\t\t\tip: requestInfo?.ip ?? null,\n\t\t}, policy)\n\n\t\tif (assessment.action === 'allow') {\n\t\t\treturn { unusualLoginDetected: false }\n\t\t}\n\n\t\tconst riskMeta = { score: assessment.score, reasons: assessment.reasons }\n\n\t\tif (assessment.action === 'stepUp' && !mfaSatisfiedThisRequest) {\n\t\t\t// Reuse the existing email-OTP step-up channel. This is independent of\n\t\t\t// `person.email_otp_enabled`, which gates email OTP only as the *primary*\n\t\t\t// second factor (A05); the step-up challenge stands on its own, so a person\n\t\t\t// with no standing MFA can still complete it. Without consuming the code\n\t\t\t// here, such a person would never satisfy the step-up and would loop on\n\t\t\t// OTP_REQUIRED forever (the score is unchanged on every retry).\n\t\t\tif (otpCode) {\n\t\t\t\t// Retry leg: the client resent the emailed code as otpToken. Mirror the\n\t\t\t\t// A05 contract — an invalid code is rejected (not re-sent); the per-code\n\t\t\t\t// 3-attempt cap still applies, and a fresh code is issued only when the\n\t\t\t\t// client retries without a code.\n\t\t\t\tif (!await this.emailOtpManager.verifyAndConsume(dbContext, person, otpCode)) {\n\t\t\t\t\t// A failed *forced* second factor is itself an anomaly signal — carry the\n\t\t\t\t\t// step-up/risk metadata so the resolver still emits unusual_login_detected\n\t\t\t\t\t// + step_up_required with the score/reasons, rather than logging it as an\n\t\t\t\t\t// ordinary mistyped-OTP login indistinguishable from a normal 2FA typo.\n\t\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', {\n\t\t\t\t\t\tstepUpRequired: true,\n\t\t\t\t\t\tunusualLoginDetected: true,\n\t\t\t\t\t\trisk: riskMeta,\n\t\t\t\t\t\t...authLogData,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\t// Step-up satisfied this request — fall through to notify + allow below.\n\t\t\t} else {\n\t\t\t\t// First leg: dispatch a code and ask the client to retry with otpToken.\n\t\t\t\t// Rate-limited per person; a previously emailed code stays valid when throttled.\n\t\t\t\tconst decision = await this.emailOtpManager.sendCode(dbContext, person, config)\n\t\t\t\treturn new ResponseError('OTP_REQUIRED', 'Additional verification is required for this sign-in', {\n\t\t\t\t\temailOtpSent: decision.ok,\n\t\t\t\t\tstepUpRequired: true,\n\t\t\t\t\tunusualLoginDetected: true,\n\t\t\t\t\trisk: riskMeta,\n\t\t\t\t\t...authLogData,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// action === 'email', or step-up satisfied (already this request, or just now\n\t\t// via the emailed code): notify the person of the unusual sign-in. A failed\n\t\t// email must never block sign-in.\n\t\tawait this.sendUnusualLoginEmail(dbContext, person, geoCountry ?? null, requestInfo?.ip ?? null)\n\t\treturn { unusualLoginDetected: true, risk: riskMeta }\n\t}\n\n\tprivate async sendUnusualLoginEmail(dbContext: DatabaseContext, person: PersonRow, geoCountry: string | null, ip: string | null): Promise<void> {\n\t\tif (!person.email) {\n\t\t\t// No address to notify (e.g. an IDP-only person) — nothing to send.\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait this.userMailer.sendUnusualLoginEmail(dbContext, {\n\t\t\t\temail: person.email,\n\t\t\t\tgeoCountry,\n\t\t\t\tipAddress: ip,\n\t\t\t}, { projectId: null, variant: '' })\n\t\t} catch {\n\t\t\t// ignore — informational only\n\t\t}\n\t}\n\n\t/**\n\t * A06 enforcement for the no-active-factor case. Returns:\n\t * - `null` when MFA is not required, or grace currently allows sign-in\n\t * (caller proceeds to issue a session as normal). With zero auth_policy\n\t * rows this is always the outcome — no behavior change.\n\t * - a `ResponseError` (MFA_ENROLLMENT_REQUIRED with the pending secret to\n\t * enroll, or INVALID_OTP_TOKEN) when sign-in must be blocked.\n\t * - `{ ok: true, backupCodes }` when enrollment just completed (pending TOTP\n\t * promoted to active); caller issues the session.\n\t */\n\tprivate async enforceMfaEnrollment(\n\t\tdbContext: DatabaseContext,\n\t\tperson: PersonRow,\n\t\totpCode: string | undefined,\n\t\tauthLogData: { [AuthLogService.Key]: AuthLogService.Bag },\n\t): Promise<\n\t\t| null\n\t\t| ResponseError<SignInErrorCode, {\n\t\t\temailOtpSent?: boolean\n\t\t\tmfaEnrollmentRequired?: boolean\n\t\t\tmfaEnrollment?: MfaEnrollment\n\t\t\t[AuthLogService.Key]: AuthLogService.Bag\n\t\t}>\n\t\t| { backupCodes: string[] }\n\t> {\n\t\tconst policy = await this.authPolicyResolver.resolveForIdentity(dbContext, person.identity_id, person.roles)\n\t\tif (!policy.mfaRequired) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst now = this.providers.now()\n\n\t\t// Resolve the effective grace duration (seconds). A per-policy override\n\t\t// wins; otherwise we fall back to the global config default. Only fetch the\n\t\t// config when the override is absent, to keep query shapes minimal.\n\t\tlet graceSeconds: number\n\t\tif (policy.graceDuration !== null) {\n\t\t\tgraceSeconds = intervalToSeconds(policy.graceDuration)\n\t\t} else {\n\t\t\tconst config = await dbContext.queryHandler.fetch(new ConfigurationQuery(dbContext.providers))\n\t\t\tgraceSeconds = intervalToSeconds(config.login.mfaGraceDuration)\n\t\t}\n\n\t\t// Grace handling. Default grace duration is 0 (immediate enforcement), so\n\t\t// by default neither branch below opens a window.\n\t\tif (person.mfa_grace_until !== null) {\n\t\t\tif (person.mfa_grace_until.getTime() > now.getTime()) {\n\t\t\t\t// Still inside an open grace window — allow sign-in.\n\t\t\t\treturn null\n\t\t\t}\n\t\t\t// Grace expired → fall through to enforcement.\n\t\t} else if (graceSeconds > 0) {\n\t\t\t// Open the grace window now and allow this sign-in.\n\t\t\tconst graceUntil = new Date(now.getTime() + graceSeconds * 1000)\n\t\t\tawait dbContext.commandBus.execute(new SetMfaGraceUntilCommand(person.id, graceUntil))\n\t\t\treturn null\n\t\t}\n\n\t\t// Enforce.\n\t\tif (otpCode && person.otp_pending_secret) {\n\t\t\t// Enrollment completion: verify against the pending secret, then promote.\n\t\t\tif (!await this.otpManager.verifyPendingOtp(person, otpCode)) {\n\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t}\n\t\t\tconst promoted = await this.otpManager.confirmOtp(dbContext, person)\n\t\t\tif (!promoted) {\n\t\t\t\t// Lost a concurrent enrollment-completion race: the pending secret was\n\t\t\t\t// already promoted by the other request (which issued the backup codes).\n\t\t\t\t// Don't regenerate here — that would invalidate the codes already\n\t\t\t\t// returned. TOTP is now active, so a retry signs in via the normal path.\n\t\t\t\treturn new ResponseError('INVALID_OTP_TOKEN', 'OTP token validation has failed', authLogData)\n\t\t\t}\n\t\t\tconst backupCodes = await this.backupCodeManager.generate(dbContext, person.id)\n\t\t\treturn { backupCodes }\n\t\t}\n\n\t\t// Provision a fresh pending TOTP secret and ask the client to enroll.\n\t\tconst otp = await this.otpManager.prepareOtp(dbContext, person, 'Contember')\n\t\tconst mfaEnrollment: MfaEnrollment = { otpUri: otp.uri, otpSecret: otp.secret }\n\t\treturn new ResponseError('MFA_ENROLLMENT_REQUIRED', 'MFA enrollment is required', {\n\t\t\tmfaEnrollmentRequired: true,\n\t\t\tmfaEnrollment,\n\t\t\t...authLogData,\n\t\t})\n\t}\n\n\tasync createSessionToken(\n\t\tdbContext: DatabaseContext,\n\t\tpersonIdentifier: PersonUniqueIdentifier,\n\t\texpiration?: number,\n\t\tverifier?: (person: PersonRow) => Promise<void>,\n\t\trequestInfo?: ApiKeyRequestInfo,\n\t\ttrustForwardedInfo?: boolean,\n\t): Promise<CreateSessionTokenResponse> {\n\t\tconst person = await dbContext.queryHandler.fetch(PersonQuery.byUniqueIdentifier(personIdentifier))\n\n\t\tif (person === null) {\n\t\t\tif (personIdentifier.type === 'email') {\n\t\t\t\treturn new ResponseError('UNKNOWN_EMAIL', `Person with email ${personIdentifier.email} not found`, {\n\t\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\t\tpersonInput: personIdentifier.email,\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t} else if (personIdentifier.type === 'id') {\n\t\t\t\treturn new ResponseError('UNKNOWN_PERSON_ID', `Person with id ${personIdentifier.id} not found`, {\n\t\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\t\tpersonInput: personIdentifier.id,\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tthrow new ImplementationException()\n\t\t}\n\n\t\tconst inputValue = personIdentifier.type === 'email' ? personIdentifier.email : personIdentifier.id\n\t\tif (person.disabled_at !== null) {\n\t\t\treturn new ResponseError('PERSON_DISABLED', `Person with id ${person.id} is disabled`, {\n\t\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\t\tpersonId: person.id,\n\t\t\t\t\tpersonInput: inputValue,\n\t\t\t\t}),\n\t\t\t})\n\t\t}\n\n\t\tawait verifier?.(person)\n\n\t\tconst sessionToken = await this.apiKeyManager.createSessionApiKey(dbContext, person.identity_id, expiration, requestInfo, trustForwardedInfo)\n\t\treturn new ResponseOk({\n\t\t\tperson,\n\t\t\ttoken: sessionToken,\n\t\t\t[AuthLogService.Key]: new AuthLogService.Bag({\n\t\t\t\tpersonId: person.id,\n\t\t\t\tpersonInput: inputValue,\n\t\t\t}),\n\t\t})\n\t}\n}\n\n/** A03: greppable payload describing why a sign-in scored as unusual. */\nexport interface RiskMeta {\n\treadonly score: number\n\treadonly reasons: readonly string[]\n}\n\n/**\n * Result of {@link SignInManager.applyRiskPolicy}: `null` = feature off / no\n * config; a metadata object when the session may still be issued; a\n * `ResponseError` when step-up blocks the session.\n */\ntype RiskPolicyOutcome =\n\t| null\n\t| { unusualLoginDetected: boolean; risk?: RiskMeta }\n\t| ResponseError<SignInErrorCode, {\n\t\temailOtpSent?: boolean\n\t\tstepUpRequired?: boolean\n\t\tunusualLoginDetected?: boolean\n\t\trisk?: RiskMeta\n\t\t[AuthLogService.Key]: AuthLogService.Bag\n\t}>\n\nexport interface SignInResult {\n\treadonly person: PersonRow\n\treadonly token: string\n\t/** True when MFA was satisfied by consuming a backup code (instead of a TOTP token). */\n\treadonly usedBackupCode?: boolean\n\t/** Set when sign-in completed an MFA enrollment (A06): the freshly issued backup codes, shown once. */\n\treadonly backupCodes?: string[]\n\t/** A03: true when this sign-in scored as unusual and an UNUSUAL_LOGIN email was sent. */\n\treadonly unusualLoginDetected?: boolean\n\t/** A03: the risk score + reasons, recorded in the `unusual_login_detected` audit `event_data`. */\n\treadonly risk?: RiskMeta\n\t[AuthLogService.Key]: AuthLogService.Bag\n}\n\nexport type SignInResponse = Response<SignInResult, SignInErrorCode, {\n\t/** Set on an OTP_REQUIRED error when an email-OTP code was just dispatched (A05). */\n\temailOtpSent?: boolean\n\t/** Set on a MFA_ENROLLMENT_REQUIRED error (A06): triggers the `mfa_enrollment_required` audit. */\n\tmfaEnrollmentRequired?: boolean\n\t/** Set on a MFA_ENROLLMENT_REQUIRED error (A06): the pending secret the client must enroll. */\n\tmfaEnrollment?: MfaEnrollment\n\t/** Set on an OTP_REQUIRED error raised by A03 anomaly step-up: triggers the `step_up_required` audit. */\n\tstepUpRequired?: boolean\n\t/** A03: true when the sign-in scored as unusual (triggers the `unusual_login_detected` audit). */\n\tunusualLoginDetected?: boolean\n\t/** A03: the risk score + reasons, recorded in the audit `event_data`. */\n\trisk?: RiskMeta\n\t[AuthLogService.Key]: AuthLogService.Bag\n}>\nexport type CreateSessionTokenResponse = Response<SignInResult, CreateSessionTokenErrorCode, {\n\t[AuthLogService.Key]: AuthLogService.Bag\n}>\n\nexport { SignInManager }\n"],"names":["riskHandled","sessionToken"],"mappings":"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"}
|
|
@@ -24,6 +24,12 @@ class PersonIdentityProviderManager {
|
|
|
24
24
|
if (!connection) {
|
|
25
25
|
return new Response.ResponseError("NOT_FOUND", `You are not connected to the identity provider connection "${connectionId}".`);
|
|
26
26
|
}
|
|
27
|
+
if (connection.identityProviderDisableLocalAuthentication) {
|
|
28
|
+
return new Response.ResponseError(
|
|
29
|
+
"IDP_REQUIRED",
|
|
30
|
+
`The identity provider "${connection.identityProviderSlug}" is the only way you may sign in; its connection cannot be removed.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
27
33
|
const configuration = await db2.queryHandler.fetch(new ConfigurationQuery.ConfigurationQuery(db2.providers));
|
|
28
34
|
const hasPassword = person.password_hash !== null;
|
|
29
35
|
const hasPasswordless = isPasswordlessEnabled.isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonIdentityProviderManager.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/model/service/idp/PersonIdentityProviderManager.ts"],"sourcesContent":["import { Response, ResponseError, ResponseOk } from '../../utils/Response.js'\nimport { DisconnectIdpErrorCode } from '../../../schema/index.js'\nimport { DatabaseContext } from '../../utils/index.js'\nimport { ConfigurationQuery, PersonIdentityProviderRow, PersonIdentityProvidersQuery, PersonRow } from '../../queries/index.js'\nimport { DisconnectPersonIdentityProviderCommand } from '../../commands/idp/DisconnectPersonIdentityProviderCommand.js'\nimport { isPasswordlessEnabled } from '../helpers/isPasswordlessEnabled.js'\n\n/**\n * Manages the external IdP connections of the *currently authenticated* person.\n *\n * Lock-out policy: a person may always disconnect a provider as long as it does\n * not remove their last remaining sign-in method. A connection is refused only\n * when, after removal, the person would have no password, no passwordless\n * sign-in enabled and no other *usable* IdP connection left — which would lock\n * them out of their account entirely. Whether passwordless counts is decided by\n * the same tenant policy the sign-in flow applies, and connections to disabled\n * providers are not counted as a usable fallback (they cannot sign in).\n */\nexport class PersonIdentityProviderManager {\n\tpublic async listPersonIdentityProviders(db: DatabaseContext, personId: string): Promise<PersonIdentityProviderRow[]> {\n\t\treturn await db.queryHandler.fetch(new PersonIdentityProvidersQuery(personId))\n\t}\n\n\tpublic async disconnectIdentityProvider(db: DatabaseContext, person: PersonRow, connectionId: string): Promise<DisconnectIDPResponse> {\n\t\treturn await db.transaction(async db => {\n\t\t\tconst connections = await db.queryHandler.fetch(new PersonIdentityProvidersQuery(person.id))\n\t\t\tconst connection = connections.find(it => it.id === connectionId)\n\t\t\tif (!connection) {\n\t\t\t\treturn new ResponseError('NOT_FOUND', `You are not connected to the identity provider connection \"${connectionId}\".`)\n\t\t\t}\n\n\t\t\tconst configuration = await db.queryHandler.fetch(new ConfigurationQuery(db.providers))\n\t\t\tconst hasPassword = person.password_hash !== null\n\t\t\tconst hasPasswordless = isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled)\n\t\t\tconst otherUsableConnections = connections.filter(it => it.id !== connection.id && it.identityProviderDisabledAt === null)\n\t\t\tconst wouldLockOut = !hasPassword && !hasPasswordless && otherUsableConnections.length === 0\n\t\t\tif (wouldLockOut) {\n\t\t\t\treturn new ResponseError(\n\t\t\t\t\t'LAST_AUTH_METHOD',\n\t\t\t\t\t'Cannot disconnect the only remaining sign-in method. Set up a password or another sign-in method first.',\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tawait db.commandBus.execute(new DisconnectPersonIdentityProviderCommand(person.id, connection.id))\n\t\t\treturn new ResponseOk({ slug: connection.identityProviderSlug })\n\t\t})\n\t}\n}\n\nexport interface DisconnectIDPResult {\n\treadonly slug: string\n}\n\nexport type DisconnectIDPResponse = Response<DisconnectIDPResult, DisconnectIdpErrorCode>\n"],"names":["PersonIdentityProvidersQuery","db","ResponseError","ConfigurationQuery","isPasswordlessEnabled","DisconnectPersonIdentityProviderCommand","ResponseOk"],"mappings":";;;;;;;;;;;;;;;AAkBO,MAAM,8BAA8B;AAAA,EAC1C,MAAa,4BAA4B,IAAqB,UAAwD;AACrH,WAAO,MAAM,GAAG,aAAa,MAAM,IAAIA,6BAAAA,6BAA6B,QAAQ,CAAC;AAAA,EAC9E;AAAA,EAEA,MAAa,2BAA2B,IAAqB,QAAmB,cAAsD;AACrI,WAAO,MAAM,GAAG,YAAY,OAAMC,QAAM;AACvC,YAAM,cAAc,MAAMA,IAAG,aAAa,MAAM,IAAID,6BAAAA,6BAA6B,OAAO,EAAE,CAAC;AAC3F,YAAM,aAAa,YAAY,KAAK,CAAA,OAAM,GAAG,OAAO,YAAY;AAChE,UAAI,CAAC,YAAY;AAChB,eAAO,IAAIE,SAAAA,cAAc,aAAa,8DAA8D,YAAY,IAAI;AAAA,MACrH;AAEA,YAAM,gBAAgB,MAAMD,IAAG,aAAa,MAAM,IAAIE,mBAAAA,mBAAmBF,IAAG,SAAS,CAAC;AACtF,YAAM,cAAc,OAAO,kBAAkB;AAC7C,YAAM,kBAAkBG,sBAAAA,sBAAsB,cAAc,aAAa,SAAS,OAAO,oBAAoB;AAC7G,YAAM,yBAAyB,YAAY,OAAO,CAAA,OAAM,GAAG,OAAO,WAAW,MAAM,GAAG,+BAA+B,IAAI;AACzH,YAAM,eAAe,CAAC,eAAe,CAAC,mBAAmB,uBAAuB,WAAW;AAC3F,UAAI,cAAc;AACjB,eAAO,IAAIF,SAAAA;AAAAA,UACV;AAAA,UACA;AAAA,QAAA;AAAA,MAEF;AAEA,YAAMD,IAAG,WAAW,QAAQ,IAAII,wCAAAA,wCAAwC,OAAO,IAAI,WAAW,EAAE,CAAC;AACjG,aAAO,IAAIC,SAAAA,WAAW,EAAE,MAAM,WAAW,sBAAsB;AAAA,IAChE,CAAC;AAAA,EACF;AACD;;"}
|
|
1
|
+
{"version":3,"file":"PersonIdentityProviderManager.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/model/service/idp/PersonIdentityProviderManager.ts"],"sourcesContent":["import { Response, ResponseError, ResponseOk } from '../../utils/Response.js'\nimport { DisconnectIdpErrorCode } from '../../../schema/index.js'\nimport { DatabaseContext } from '../../utils/index.js'\nimport { ConfigurationQuery, PersonIdentityProviderRow, PersonIdentityProvidersQuery, PersonRow } from '../../queries/index.js'\nimport { DisconnectPersonIdentityProviderCommand } from '../../commands/idp/DisconnectPersonIdentityProviderCommand.js'\nimport { isPasswordlessEnabled } from '../helpers/isPasswordlessEnabled.js'\n\n/**\n * Manages the external IdP connections of the *currently authenticated* person.\n *\n * Lock-out policy: a person may always disconnect a provider as long as it does\n * not remove their last remaining sign-in method. A connection is refused only\n * when, after removal, the person would have no password, no passwordless\n * sign-in enabled and no other *usable* IdP connection left — which would lock\n * them out of their account entirely. Whether passwordless counts is decided by\n * the same tenant policy the sign-in flow applies, and connections to disabled\n * providers are not counted as a usable fallback (they cannot sign in).\n */\nexport class PersonIdentityProviderManager {\n\tpublic async listPersonIdentityProviders(db: DatabaseContext, personId: string): Promise<PersonIdentityProviderRow[]> {\n\t\treturn await db.queryHandler.fetch(new PersonIdentityProvidersQuery(personId))\n\t}\n\n\tpublic async disconnectIdentityProvider(db: DatabaseContext, person: PersonRow, connectionId: string): Promise<DisconnectIDPResponse> {\n\t\treturn await db.transaction(async db => {\n\t\t\tconst connections = await db.queryHandler.fetch(new PersonIdentityProvidersQuery(person.id))\n\t\t\tconst connection = connections.find(it => it.id === connectionId)\n\t\t\tif (!connection) {\n\t\t\t\treturn new ResponseError('NOT_FOUND', `You are not connected to the identity provider connection \"${connectionId}\".`)\n\t\t\t}\n\n\t\t\t// The link is what binds the person to the provider's policy, so they cannot remove it themselves.\n\t\t\tif (connection.identityProviderDisableLocalAuthentication) {\n\t\t\t\treturn new ResponseError(\n\t\t\t\t\t'IDP_REQUIRED',\n\t\t\t\t\t`The identity provider \"${connection.identityProviderSlug}\" is the only way you may sign in; its connection cannot be removed.`,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst configuration = await db.queryHandler.fetch(new ConfigurationQuery(db.providers))\n\t\t\tconst hasPassword = person.password_hash !== null\n\t\t\tconst hasPasswordless = isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled)\n\t\t\tconst otherUsableConnections = connections.filter(it => it.id !== connection.id && it.identityProviderDisabledAt === null)\n\t\t\tconst wouldLockOut = !hasPassword && !hasPasswordless && otherUsableConnections.length === 0\n\t\t\tif (wouldLockOut) {\n\t\t\t\treturn new ResponseError(\n\t\t\t\t\t'LAST_AUTH_METHOD',\n\t\t\t\t\t'Cannot disconnect the only remaining sign-in method. Set up a password or another sign-in method first.',\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tawait db.commandBus.execute(new DisconnectPersonIdentityProviderCommand(person.id, connection.id))\n\t\t\treturn new ResponseOk({ slug: connection.identityProviderSlug })\n\t\t})\n\t}\n}\n\nexport interface DisconnectIDPResult {\n\treadonly slug: string\n}\n\nexport type DisconnectIDPResponse = Response<DisconnectIDPResult, DisconnectIdpErrorCode>\n"],"names":["PersonIdentityProvidersQuery","db","ResponseError","ConfigurationQuery","isPasswordlessEnabled","DisconnectPersonIdentityProviderCommand","ResponseOk"],"mappings":";;;;;;;;;;;;;;;AAkBO,MAAM,8BAA8B;AAAA,EAC1C,MAAa,4BAA4B,IAAqB,UAAwD;AACrH,WAAO,MAAM,GAAG,aAAa,MAAM,IAAIA,6BAAAA,6BAA6B,QAAQ,CAAC;AAAA,EAC9E;AAAA,EAEA,MAAa,2BAA2B,IAAqB,QAAmB,cAAsD;AACrI,WAAO,MAAM,GAAG,YAAY,OAAMC,QAAM;AACvC,YAAM,cAAc,MAAMA,IAAG,aAAa,MAAM,IAAID,6BAAAA,6BAA6B,OAAO,EAAE,CAAC;AAC3F,YAAM,aAAa,YAAY,KAAK,CAAA,OAAM,GAAG,OAAO,YAAY;AAChE,UAAI,CAAC,YAAY;AAChB,eAAO,IAAIE,SAAAA,cAAc,aAAa,8DAA8D,YAAY,IAAI;AAAA,MACrH;AAGA,UAAI,WAAW,4CAA4C;AAC1D,eAAO,IAAIA,SAAAA;AAAAA,UACV;AAAA,UACA,0BAA0B,WAAW,oBAAoB;AAAA,QAAA;AAAA,MAE3D;AAEA,YAAM,gBAAgB,MAAMD,IAAG,aAAa,MAAM,IAAIE,mBAAAA,mBAAmBF,IAAG,SAAS,CAAC;AACtF,YAAM,cAAc,OAAO,kBAAkB;AAC7C,YAAM,kBAAkBG,sBAAAA,sBAAsB,cAAc,aAAa,SAAS,OAAO,oBAAoB;AAC7G,YAAM,yBAAyB,YAAY,OAAO,CAAA,OAAM,GAAG,OAAO,WAAW,MAAM,GAAG,+BAA+B,IAAI;AACzH,YAAM,eAAe,CAAC,eAAe,CAAC,mBAAmB,uBAAuB,WAAW;AAC3F,UAAI,cAAc;AACjB,eAAO,IAAIF,SAAAA;AAAAA,UACV;AAAA,UACA;AAAA,QAAA;AAAA,MAEF;AAEA,YAAMD,IAAG,WAAW,QAAQ,IAAII,wCAAAA,wCAAwC,OAAO,IAAI,WAAW,EAAE,CAAC;AACjG,aAAO,IAAIC,SAAAA,WAAW,EAAE,MAAM,WAAW,sBAAsB;AAAA,IAChE,CAAC;AAAA,EACF;AACD;;"}
|
|
@@ -22,6 +22,12 @@ class PersonIdentityProviderManager {
|
|
|
22
22
|
if (!connection) {
|
|
23
23
|
return new ResponseError("NOT_FOUND", `You are not connected to the identity provider connection "${connectionId}".`);
|
|
24
24
|
}
|
|
25
|
+
if (connection.identityProviderDisableLocalAuthentication) {
|
|
26
|
+
return new ResponseError(
|
|
27
|
+
"IDP_REQUIRED",
|
|
28
|
+
`The identity provider "${connection.identityProviderSlug}" is the only way you may sign in; its connection cannot be removed.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
25
31
|
const configuration = await db2.queryHandler.fetch(new ConfigurationQuery(db2.providers));
|
|
26
32
|
const hasPassword = person.password_hash !== null;
|
|
27
33
|
const hasPasswordless = isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersonIdentityProviderManager.js","sources":["../../../../../../../packages/engine-tenant-api/src/model/service/idp/PersonIdentityProviderManager.ts"],"sourcesContent":["import { Response, ResponseError, ResponseOk } from '../../utils/Response.js'\nimport { DisconnectIdpErrorCode } from '../../../schema/index.js'\nimport { DatabaseContext } from '../../utils/index.js'\nimport { ConfigurationQuery, PersonIdentityProviderRow, PersonIdentityProvidersQuery, PersonRow } from '../../queries/index.js'\nimport { DisconnectPersonIdentityProviderCommand } from '../../commands/idp/DisconnectPersonIdentityProviderCommand.js'\nimport { isPasswordlessEnabled } from '../helpers/isPasswordlessEnabled.js'\n\n/**\n * Manages the external IdP connections of the *currently authenticated* person.\n *\n * Lock-out policy: a person may always disconnect a provider as long as it does\n * not remove their last remaining sign-in method. A connection is refused only\n * when, after removal, the person would have no password, no passwordless\n * sign-in enabled and no other *usable* IdP connection left — which would lock\n * them out of their account entirely. Whether passwordless counts is decided by\n * the same tenant policy the sign-in flow applies, and connections to disabled\n * providers are not counted as a usable fallback (they cannot sign in).\n */\nexport class PersonIdentityProviderManager {\n\tpublic async listPersonIdentityProviders(db: DatabaseContext, personId: string): Promise<PersonIdentityProviderRow[]> {\n\t\treturn await db.queryHandler.fetch(new PersonIdentityProvidersQuery(personId))\n\t}\n\n\tpublic async disconnectIdentityProvider(db: DatabaseContext, person: PersonRow, connectionId: string): Promise<DisconnectIDPResponse> {\n\t\treturn await db.transaction(async db => {\n\t\t\tconst connections = await db.queryHandler.fetch(new PersonIdentityProvidersQuery(person.id))\n\t\t\tconst connection = connections.find(it => it.id === connectionId)\n\t\t\tif (!connection) {\n\t\t\t\treturn new ResponseError('NOT_FOUND', `You are not connected to the identity provider connection \"${connectionId}\".`)\n\t\t\t}\n\n\t\t\tconst configuration = await db.queryHandler.fetch(new ConfigurationQuery(db.providers))\n\t\t\tconst hasPassword = person.password_hash !== null\n\t\t\tconst hasPasswordless = isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled)\n\t\t\tconst otherUsableConnections = connections.filter(it => it.id !== connection.id && it.identityProviderDisabledAt === null)\n\t\t\tconst wouldLockOut = !hasPassword && !hasPasswordless && otherUsableConnections.length === 0\n\t\t\tif (wouldLockOut) {\n\t\t\t\treturn new ResponseError(\n\t\t\t\t\t'LAST_AUTH_METHOD',\n\t\t\t\t\t'Cannot disconnect the only remaining sign-in method. Set up a password or another sign-in method first.',\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tawait db.commandBus.execute(new DisconnectPersonIdentityProviderCommand(person.id, connection.id))\n\t\t\treturn new ResponseOk({ slug: connection.identityProviderSlug })\n\t\t})\n\t}\n}\n\nexport interface DisconnectIDPResult {\n\treadonly slug: string\n}\n\nexport type DisconnectIDPResponse = Response<DisconnectIDPResult, DisconnectIdpErrorCode>\n"],"names":["db"],"mappings":";;;;;;;;;;;;;AAkBO,MAAM,8BAA8B;AAAA,EAC1C,MAAa,4BAA4B,IAAqB,UAAwD;AACrH,WAAO,MAAM,GAAG,aAAa,MAAM,IAAI,6BAA6B,QAAQ,CAAC;AAAA,EAC9E;AAAA,EAEA,MAAa,2BAA2B,IAAqB,QAAmB,cAAsD;AACrI,WAAO,MAAM,GAAG,YAAY,OAAMA,QAAM;AACvC,YAAM,cAAc,MAAMA,IAAG,aAAa,MAAM,IAAI,6BAA6B,OAAO,EAAE,CAAC;AAC3F,YAAM,aAAa,YAAY,KAAK,CAAA,OAAM,GAAG,OAAO,YAAY;AAChE,UAAI,CAAC,YAAY;AAChB,eAAO,IAAI,cAAc,aAAa,8DAA8D,YAAY,IAAI;AAAA,MACrH;AAEA,YAAM,gBAAgB,MAAMA,IAAG,aAAa,MAAM,IAAI,mBAAmBA,IAAG,SAAS,CAAC;AACtF,YAAM,cAAc,OAAO,kBAAkB;AAC7C,YAAM,kBAAkB,sBAAsB,cAAc,aAAa,SAAS,OAAO,oBAAoB;AAC7G,YAAM,yBAAyB,YAAY,OAAO,CAAA,OAAM,GAAG,OAAO,WAAW,MAAM,GAAG,+BAA+B,IAAI;AACzH,YAAM,eAAe,CAAC,eAAe,CAAC,mBAAmB,uBAAuB,WAAW;AAC3F,UAAI,cAAc;AACjB,eAAO,IAAI;AAAA,UACV;AAAA,UACA;AAAA,QAAA;AAAA,MAEF;AAEA,YAAMA,IAAG,WAAW,QAAQ,IAAI,wCAAwC,OAAO,IAAI,WAAW,EAAE,CAAC;AACjG,aAAO,IAAI,WAAW,EAAE,MAAM,WAAW,sBAAsB;AAAA,IAChE,CAAC;AAAA,EACF;AACD;"}
|
|
1
|
+
{"version":3,"file":"PersonIdentityProviderManager.js","sources":["../../../../../../../packages/engine-tenant-api/src/model/service/idp/PersonIdentityProviderManager.ts"],"sourcesContent":["import { Response, ResponseError, ResponseOk } from '../../utils/Response.js'\nimport { DisconnectIdpErrorCode } from '../../../schema/index.js'\nimport { DatabaseContext } from '../../utils/index.js'\nimport { ConfigurationQuery, PersonIdentityProviderRow, PersonIdentityProvidersQuery, PersonRow } from '../../queries/index.js'\nimport { DisconnectPersonIdentityProviderCommand } from '../../commands/idp/DisconnectPersonIdentityProviderCommand.js'\nimport { isPasswordlessEnabled } from '../helpers/isPasswordlessEnabled.js'\n\n/**\n * Manages the external IdP connections of the *currently authenticated* person.\n *\n * Lock-out policy: a person may always disconnect a provider as long as it does\n * not remove their last remaining sign-in method. A connection is refused only\n * when, after removal, the person would have no password, no passwordless\n * sign-in enabled and no other *usable* IdP connection left — which would lock\n * them out of their account entirely. Whether passwordless counts is decided by\n * the same tenant policy the sign-in flow applies, and connections to disabled\n * providers are not counted as a usable fallback (they cannot sign in).\n */\nexport class PersonIdentityProviderManager {\n\tpublic async listPersonIdentityProviders(db: DatabaseContext, personId: string): Promise<PersonIdentityProviderRow[]> {\n\t\treturn await db.queryHandler.fetch(new PersonIdentityProvidersQuery(personId))\n\t}\n\n\tpublic async disconnectIdentityProvider(db: DatabaseContext, person: PersonRow, connectionId: string): Promise<DisconnectIDPResponse> {\n\t\treturn await db.transaction(async db => {\n\t\t\tconst connections = await db.queryHandler.fetch(new PersonIdentityProvidersQuery(person.id))\n\t\t\tconst connection = connections.find(it => it.id === connectionId)\n\t\t\tif (!connection) {\n\t\t\t\treturn new ResponseError('NOT_FOUND', `You are not connected to the identity provider connection \"${connectionId}\".`)\n\t\t\t}\n\n\t\t\t// The link is what binds the person to the provider's policy, so they cannot remove it themselves.\n\t\t\tif (connection.identityProviderDisableLocalAuthentication) {\n\t\t\t\treturn new ResponseError(\n\t\t\t\t\t'IDP_REQUIRED',\n\t\t\t\t\t`The identity provider \"${connection.identityProviderSlug}\" is the only way you may sign in; its connection cannot be removed.`,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst configuration = await db.queryHandler.fetch(new ConfigurationQuery(db.providers))\n\t\t\tconst hasPassword = person.password_hash !== null\n\t\t\tconst hasPasswordless = isPasswordlessEnabled(configuration.passwordless.enabled, person.passwordless_enabled)\n\t\t\tconst otherUsableConnections = connections.filter(it => it.id !== connection.id && it.identityProviderDisabledAt === null)\n\t\t\tconst wouldLockOut = !hasPassword && !hasPasswordless && otherUsableConnections.length === 0\n\t\t\tif (wouldLockOut) {\n\t\t\t\treturn new ResponseError(\n\t\t\t\t\t'LAST_AUTH_METHOD',\n\t\t\t\t\t'Cannot disconnect the only remaining sign-in method. Set up a password or another sign-in method first.',\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tawait db.commandBus.execute(new DisconnectPersonIdentityProviderCommand(person.id, connection.id))\n\t\t\treturn new ResponseOk({ slug: connection.identityProviderSlug })\n\t\t})\n\t}\n}\n\nexport interface DisconnectIDPResult {\n\treadonly slug: string\n}\n\nexport type DisconnectIDPResponse = Response<DisconnectIDPResult, DisconnectIdpErrorCode>\n"],"names":["db"],"mappings":";;;;;;;;;;;;;AAkBO,MAAM,8BAA8B;AAAA,EAC1C,MAAa,4BAA4B,IAAqB,UAAwD;AACrH,WAAO,MAAM,GAAG,aAAa,MAAM,IAAI,6BAA6B,QAAQ,CAAC;AAAA,EAC9E;AAAA,EAEA,MAAa,2BAA2B,IAAqB,QAAmB,cAAsD;AACrI,WAAO,MAAM,GAAG,YAAY,OAAMA,QAAM;AACvC,YAAM,cAAc,MAAMA,IAAG,aAAa,MAAM,IAAI,6BAA6B,OAAO,EAAE,CAAC;AAC3F,YAAM,aAAa,YAAY,KAAK,CAAA,OAAM,GAAG,OAAO,YAAY;AAChE,UAAI,CAAC,YAAY;AAChB,eAAO,IAAI,cAAc,aAAa,8DAA8D,YAAY,IAAI;AAAA,MACrH;AAGA,UAAI,WAAW,4CAA4C;AAC1D,eAAO,IAAI;AAAA,UACV;AAAA,UACA,0BAA0B,WAAW,oBAAoB;AAAA,QAAA;AAAA,MAE3D;AAEA,YAAM,gBAAgB,MAAMA,IAAG,aAAa,MAAM,IAAI,mBAAmBA,IAAG,SAAS,CAAC;AACtF,YAAM,cAAc,OAAO,kBAAkB;AAC7C,YAAM,kBAAkB,sBAAsB,cAAc,aAAa,SAAS,OAAO,oBAAoB;AAC7G,YAAM,yBAAyB,YAAY,OAAO,CAAA,OAAM,GAAG,OAAO,WAAW,MAAM,GAAG,+BAA+B,IAAI;AACzH,YAAM,eAAe,CAAC,eAAe,CAAC,mBAAmB,uBAAuB,WAAW;AAC3F,UAAI,cAAc;AACjB,eAAO,IAAI;AAAA,UACV;AAAA,UACA;AAAA,QAAA;AAAA,MAEF;AAEA,YAAMA,IAAG,WAAW,QAAQ,IAAI,wCAAwC,OAAO,IAAI,WAAW,EAAE,CAAC;AACjG,aAAO,IAAI,WAAW,EAAE,MAAM,WAAW,sBAAsB;AAAA,IAChE,CAAC;AAAA,EACF;AACD;"}
|
|
@@ -43,7 +43,8 @@ class AddIDPMutationResolver {
|
|
|
43
43
|
exclusive: args.options?.exclusive ?? false,
|
|
44
44
|
initReturnsConfig: args.options?.initReturnsConfig ?? false,
|
|
45
45
|
requireVerifiedEmail: args.options?.requireVerifiedEmail ?? false,
|
|
46
|
-
assumeEmailVerified: args.options?.assumeEmailVerified ?? false
|
|
46
|
+
assumeEmailVerified: args.options?.assumeEmailVerified ?? false,
|
|
47
|
+
disableLocalAuthentication: args.options?.disableLocalAuthentication ?? false
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
50
|
if (!result.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddIDPMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/AddIDPMutationResolver.ts"],"sourcesContent":["import { AddIdpResponse, MutationAddIdpArgs, MutationResolvers } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class AddIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync addIDP(parent: unknown, args: MutationAddIdpArgs, context: TenantResolverContext): Promise<AddIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_ADD,\n\t\t\tmessage: 'You are not allowed to add IDP',\n\t\t})\n\t\tconst result = await this.idpManager.addIDP(context.db, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\tslug: args.identityProvider,\n\t\t\ttype: args.type,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? false,\n\t\t\t\texclusive: args.options?.exclusive ?? false,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? false,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? false,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? false,\n\t\t\t},\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_create',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: null,\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":["PermissionActions","createErrorResponse","IdentityProviderBySlugQuery","ResponseOk","idpRowToAuditSnapshot"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,uBAAoD;AAAA,EAChE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,OAAO,QAAiB,MAA0B,SAAyD;AAChH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQA,kBAAAA,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,KAAK,WAAW,OAAO,QAAQ,IAAI;AAAA,MACvD,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,MACX,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,MAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"AddIDPMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/AddIDPMutationResolver.ts"],"sourcesContent":["import { AddIdpResponse, MutationAddIdpArgs, MutationResolvers } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class AddIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync addIDP(parent: unknown, args: MutationAddIdpArgs, context: TenantResolverContext): Promise<AddIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_ADD,\n\t\t\tmessage: 'You are not allowed to add IDP',\n\t\t})\n\t\tconst result = await this.idpManager.addIDP(context.db, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\tslug: args.identityProvider,\n\t\t\ttype: args.type,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? false,\n\t\t\t\texclusive: args.options?.exclusive ?? false,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? false,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? false,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? false,\n\t\t\t\tdisableLocalAuthentication: args.options?.disableLocalAuthentication ?? false,\n\t\t\t},\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_create',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: null,\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":["PermissionActions","createErrorResponse","IdentityProviderBySlugQuery","ResponseOk","idpRowToAuditSnapshot"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,uBAAoD;AAAA,EAChE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,OAAO,QAAiB,MAA0B,SAAyD;AAChH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQA,kBAAAA,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,KAAK,WAAW,OAAO,QAAQ,IAAI;AAAA,MACvD,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,MACX,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,QAC1D,4BAA4B,KAAK,SAAS,8BAA8B;AAAA,MAAA;AAAA,IACzE,CACA;AACD,QAAI,CAAC,OAAO,IAAI;AACf,aAAOC,WAAAA,oBAAoB,OAAO,OAAO,OAAO,YAAY;AAAA,IAC7D;AAEA,UAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAIC,4BAAAA,4BAA4B,KAAK,gBAAgB,CAAC;AACxG,UAAM,QAAQ,cAAc;AAAA,MAC3B,MAAM;AAAA,MACN,UAAU,IAAIC,SAAAA,WAAW,IAAI;AAAA,MAC7B,WAAW;AAAA,QACV,MAAM,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,OAAOC,MAAAA,sBAAsB,KAAK;AAAA,MAAA;AAAA,IACnC,CACA;AAED,WAAO,EAAE,IAAI,KAAA;AAAA,EACd;AACD;;"}
|
|
@@ -41,7 +41,8 @@ class AddIDPMutationResolver {
|
|
|
41
41
|
exclusive: args.options?.exclusive ?? false,
|
|
42
42
|
initReturnsConfig: args.options?.initReturnsConfig ?? false,
|
|
43
43
|
requireVerifiedEmail: args.options?.requireVerifiedEmail ?? false,
|
|
44
|
-
assumeEmailVerified: args.options?.assumeEmailVerified ?? false
|
|
44
|
+
assumeEmailVerified: args.options?.assumeEmailVerified ?? false,
|
|
45
|
+
disableLocalAuthentication: args.options?.disableLocalAuthentication ?? false
|
|
45
46
|
}
|
|
46
47
|
});
|
|
47
48
|
if (!result.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddIDPMutationResolver.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/AddIDPMutationResolver.ts"],"sourcesContent":["import { AddIdpResponse, MutationAddIdpArgs, MutationResolvers } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class AddIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync addIDP(parent: unknown, args: MutationAddIdpArgs, context: TenantResolverContext): Promise<AddIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_ADD,\n\t\t\tmessage: 'You are not allowed to add IDP',\n\t\t})\n\t\tconst result = await this.idpManager.addIDP(context.db, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\tslug: args.identityProvider,\n\t\t\ttype: args.type,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? false,\n\t\t\t\texclusive: args.options?.exclusive ?? false,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? false,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? false,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? false,\n\t\t\t},\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_create',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: null,\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,uBAAoD;AAAA,EAChE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,OAAO,QAAiB,MAA0B,SAAyD;AAChH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQ,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,KAAK,WAAW,OAAO,QAAQ,IAAI;AAAA,MACvD,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,MACX,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,MAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"AddIDPMutationResolver.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/AddIDPMutationResolver.ts"],"sourcesContent":["import { AddIdpResponse, MutationAddIdpArgs, MutationResolvers } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class AddIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync addIDP(parent: unknown, args: MutationAddIdpArgs, context: TenantResolverContext): Promise<AddIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_ADD,\n\t\t\tmessage: 'You are not allowed to add IDP',\n\t\t})\n\t\tconst result = await this.idpManager.addIDP(context.db, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\tslug: args.identityProvider,\n\t\t\ttype: args.type,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? false,\n\t\t\t\texclusive: args.options?.exclusive ?? false,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? false,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? false,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? false,\n\t\t\t\tdisableLocalAuthentication: args.options?.disableLocalAuthentication ?? false,\n\t\t\t},\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_create',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: null,\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,uBAAoD;AAAA,EAChE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,OAAO,QAAiB,MAA0B,SAAyD;AAChH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQ,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,KAAK,WAAW,OAAO,QAAQ,IAAI;AAAA,MACvD,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,MACX,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,QAC1D,4BAA4B,KAAK,SAAS,8BAA8B;AAAA,MAAA;AAAA,IACzE,CACA;AACD,QAAI,CAAC,OAAO,IAAI;AACf,aAAO,oBAAoB,OAAO,OAAO,OAAO,YAAY;AAAA,IAC7D;AAEA,UAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAI,4BAA4B,KAAK,gBAAgB,CAAC;AACxG,UAAM,QAAQ,cAAc;AAAA,MAC3B,MAAM;AAAA,MACN,UAAU,IAAI,WAAW,IAAI;AAAA,MAC7B,WAAW;AAAA,QACV,MAAM,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,OAAO,sBAAsB,KAAK;AAAA,MAAA;AAAA,IACnC,CACA;AAED,WAAO,EAAE,IAAI,KAAA;AAAA,EACd;AACD;"}
|
|
@@ -43,7 +43,8 @@ class UpdateIDPMutationResolver {
|
|
|
43
43
|
exclusive: args.options?.exclusive ?? void 0,
|
|
44
44
|
initReturnsConfig: args.options?.initReturnsConfig ?? void 0,
|
|
45
45
|
requireVerifiedEmail: args.options?.requireVerifiedEmail ?? void 0,
|
|
46
|
-
assumeEmailVerified: args.options?.assumeEmailVerified ?? void 0
|
|
46
|
+
assumeEmailVerified: args.options?.assumeEmailVerified ?? void 0,
|
|
47
|
+
disableLocalAuthentication: args.options?.disableLocalAuthentication ?? void 0
|
|
47
48
|
}
|
|
48
49
|
}, args.mergeConfiguration ?? false);
|
|
49
50
|
if (!result.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateIDPMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/UpdateIDPMutationResolver.ts"],"sourcesContent":["import { MutationResolvers, MutationUpdateIdpArgs, UpdateIdpResponse } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class UpdateIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync updateIDP(parent: any, args: MutationUpdateIdpArgs, context: TenantResolverContext): Promise<UpdateIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_UPDATE,\n\t\t\tmessage: 'You are not allowed to update IDP',\n\t\t})\n\t\tconst before = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tconst result = await this.idpManager.updateIDP(context.db, args.identityProvider, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\ttype: args.type ?? undefined,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? undefined,\n\t\t\t\texclusive: args.options?.exclusive ?? undefined,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? undefined,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? undefined,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? undefined,\n\t\t\t},\n\t\t}, args.mergeConfiguration ?? false)\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_update',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: idpRowToAuditSnapshot(before),\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":["PermissionActions","IdentityProviderBySlugQuery","createErrorResponse","ResponseOk","idpRowToAuditSnapshot"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,0BAAuD;AAAA,EACnE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,UAAU,QAAa,MAA6B,SAA4D;AACrH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQA,kBAAAA,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAIC,4BAAAA,4BAA4B,KAAK,gBAAgB,CAAC;AACzG,UAAM,SAAS,MAAM,KAAK,WAAW,UAAU,QAAQ,IAAI,KAAK,kBAAkB;AAAA,MACjF,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK,QAAQ;AAAA,MACnB,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,MAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"UpdateIDPMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/UpdateIDPMutationResolver.ts"],"sourcesContent":["import { MutationResolvers, MutationUpdateIdpArgs, UpdateIdpResponse } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class UpdateIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync updateIDP(parent: any, args: MutationUpdateIdpArgs, context: TenantResolverContext): Promise<UpdateIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_UPDATE,\n\t\t\tmessage: 'You are not allowed to update IDP',\n\t\t})\n\t\tconst before = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tconst result = await this.idpManager.updateIDP(context.db, args.identityProvider, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\ttype: args.type ?? undefined,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? undefined,\n\t\t\t\texclusive: args.options?.exclusive ?? undefined,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? undefined,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? undefined,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? undefined,\n\t\t\t\tdisableLocalAuthentication: args.options?.disableLocalAuthentication ?? undefined,\n\t\t\t},\n\t\t}, args.mergeConfiguration ?? false)\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_update',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: idpRowToAuditSnapshot(before),\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":["PermissionActions","IdentityProviderBySlugQuery","createErrorResponse","ResponseOk","idpRowToAuditSnapshot"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,0BAAuD;AAAA,EACnE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,UAAU,QAAa,MAA6B,SAA4D;AACrH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQA,kBAAAA,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAIC,4BAAAA,4BAA4B,KAAK,gBAAgB,CAAC;AACzG,UAAM,SAAS,MAAM,KAAK,WAAW,UAAU,QAAQ,IAAI,KAAK,kBAAkB;AAAA,MACjF,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK,QAAQ;AAAA,MACnB,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,QAC1D,4BAA4B,KAAK,SAAS,8BAA8B;AAAA,MAAA;AAAA,IACzE,GACE,KAAK,sBAAsB,KAAK;AACnC,QAAI,CAAC,OAAO,IAAI;AACf,aAAOC,WAAAA,oBAAoB,OAAO,OAAO,OAAO,YAAY;AAAA,IAC7D;AAEA,UAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAID,4BAAAA,4BAA4B,KAAK,gBAAgB,CAAC;AACxG,UAAM,QAAQ,cAAc;AAAA,MAC3B,MAAM;AAAA,MACN,UAAU,IAAIE,SAAAA,WAAW,IAAI;AAAA,MAC7B,WAAW;AAAA,QACV,MAAM,KAAK;AAAA,QACX,QAAQC,MAAAA,sBAAsB,MAAM;AAAA,QACpC,OAAOA,MAAAA,sBAAsB,KAAK;AAAA,MAAA;AAAA,IACnC,CACA;AAED,WAAO,EAAE,IAAI,KAAA;AAAA,EACd;AACD;;"}
|
|
@@ -41,7 +41,8 @@ class UpdateIDPMutationResolver {
|
|
|
41
41
|
exclusive: args.options?.exclusive ?? void 0,
|
|
42
42
|
initReturnsConfig: args.options?.initReturnsConfig ?? void 0,
|
|
43
43
|
requireVerifiedEmail: args.options?.requireVerifiedEmail ?? void 0,
|
|
44
|
-
assumeEmailVerified: args.options?.assumeEmailVerified ?? void 0
|
|
44
|
+
assumeEmailVerified: args.options?.assumeEmailVerified ?? void 0,
|
|
45
|
+
disableLocalAuthentication: args.options?.disableLocalAuthentication ?? void 0
|
|
45
46
|
}
|
|
46
47
|
}, args.mergeConfiguration ?? false);
|
|
47
48
|
if (!result.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateIDPMutationResolver.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/UpdateIDPMutationResolver.ts"],"sourcesContent":["import { MutationResolvers, MutationUpdateIdpArgs, UpdateIdpResponse } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class UpdateIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync updateIDP(parent: any, args: MutationUpdateIdpArgs, context: TenantResolverContext): Promise<UpdateIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_UPDATE,\n\t\t\tmessage: 'You are not allowed to update IDP',\n\t\t})\n\t\tconst before = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tconst result = await this.idpManager.updateIDP(context.db, args.identityProvider, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\ttype: args.type ?? undefined,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? undefined,\n\t\t\t\texclusive: args.options?.exclusive ?? undefined,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? undefined,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? undefined,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? undefined,\n\t\t\t},\n\t\t}, args.mergeConfiguration ?? false)\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_update',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: idpRowToAuditSnapshot(before),\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,0BAAuD;AAAA,EACnE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,UAAU,QAAa,MAA6B,SAA4D;AACrH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQ,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAI,4BAA4B,KAAK,gBAAgB,CAAC;AACzG,UAAM,SAAS,MAAM,KAAK,WAAW,UAAU,QAAQ,IAAI,KAAK,kBAAkB;AAAA,MACjF,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK,QAAQ;AAAA,MACnB,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,MAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"UpdateIDPMutationResolver.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/UpdateIDPMutationResolver.ts"],"sourcesContent":["import { MutationResolvers, MutationUpdateIdpArgs, UpdateIdpResponse } from '../../../schema/index.js'\nimport { PermissionActions } from '../../../model/index.js'\nimport { IDPManager } from '../../../model/service/idp/IDPManager.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ResponseOk } from '../../../model/utils/Response.js'\nimport { IdentityProviderBySlugQuery } from '../../../model/queries/index.js'\nimport { idpRowToAuditSnapshot } from './audit.js'\n\nexport class UpdateIDPMutationResolver implements MutationResolvers {\n\tconstructor(private readonly idpManager: IDPManager) {\n\t}\n\n\tasync updateIDP(parent: any, args: MutationUpdateIdpArgs, context: TenantResolverContext): Promise<UpdateIdpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.IDP_UPDATE,\n\t\t\tmessage: 'You are not allowed to update IDP',\n\t\t})\n\t\tconst before = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tconst result = await this.idpManager.updateIDP(context.db, args.identityProvider, {\n\t\t\tconfiguration: args.configuration as any,\n\t\t\ttype: args.type ?? undefined,\n\t\t\toptions: {\n\t\t\t\tautoSignUp: args.options?.autoSignUp ?? undefined,\n\t\t\t\texclusive: args.options?.exclusive ?? undefined,\n\t\t\t\tinitReturnsConfig: args.options?.initReturnsConfig ?? undefined,\n\t\t\t\trequireVerifiedEmail: args.options?.requireVerifiedEmail ?? undefined,\n\t\t\t\tassumeEmailVerified: args.options?.assumeEmailVerified ?? undefined,\n\t\t\t\tdisableLocalAuthentication: args.options?.disableLocalAuthentication ?? undefined,\n\t\t\t},\n\t\t}, args.mergeConfiguration ?? false)\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\n\t\tconst after = await context.db.queryHandler.fetch(new IdentityProviderBySlugQuery(args.identityProvider))\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'idp_update',\n\t\t\tresponse: new ResponseOk(null),\n\t\t\teventData: {\n\t\t\t\tslug: args.identityProvider,\n\t\t\t\tbefore: idpRowToAuditSnapshot(before),\n\t\t\t\tafter: idpRowToAuditSnapshot(after),\n\t\t\t},\n\t\t})\n\n\t\treturn { ok: true }\n\t}\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AASO,MAAM,0BAAuD;AAAA,EACnE,YAA6B,YAAwB;AAAxB,SAAA,aAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,UAAU,QAAa,MAA6B,SAA4D;AACrH,UAAM,QAAQ,cAAc;AAAA,MAC3B,QAAQ,kBAAkB;AAAA,MAC1B,SAAS;AAAA,IAAA,CACT;AACD,UAAM,SAAS,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAI,4BAA4B,KAAK,gBAAgB,CAAC;AACzG,UAAM,SAAS,MAAM,KAAK,WAAW,UAAU,QAAQ,IAAI,KAAK,kBAAkB;AAAA,MACjF,eAAe,KAAK;AAAA,MACpB,MAAM,KAAK,QAAQ;AAAA,MACnB,SAAS;AAAA,QACR,YAAY,KAAK,SAAS,cAAc;AAAA,QACxC,WAAW,KAAK,SAAS,aAAa;AAAA,QACtC,mBAAmB,KAAK,SAAS,qBAAqB;AAAA,QACtD,sBAAsB,KAAK,SAAS,wBAAwB;AAAA,QAC5D,qBAAqB,KAAK,SAAS,uBAAuB;AAAA,QAC1D,4BAA4B,KAAK,SAAS,8BAA8B;AAAA,MAAA;AAAA,IACzE,GACE,KAAK,sBAAsB,KAAK;AACnC,QAAI,CAAC,OAAO,IAAI;AACf,aAAO,oBAAoB,OAAO,OAAO,OAAO,YAAY;AAAA,IAC7D;AAEA,UAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,MAAM,IAAI,4BAA4B,KAAK,gBAAgB,CAAC;AACxG,UAAM,QAAQ,cAAc;AAAA,MAC3B,MAAM;AAAA,MACN,UAAU,IAAI,WAAW,IAAI;AAAA,MAC7B,WAAW;AAAA,QACV,MAAM,KAAK;AAAA,QACX,QAAQ,sBAAsB,MAAM;AAAA,QACpC,OAAO,sBAAsB,KAAK;AAAA,MAAA;AAAA,IACnC,CACA;AAED,WAAO,EAAE,IAAI,KAAA;AAAA,EACd;AACD;"}
|
|
@@ -13,6 +13,7 @@ const idpRowToAuditSnapshot = (row) => {
|
|
|
13
13
|
initReturnsConfig: row.initReturnsConfig,
|
|
14
14
|
requireVerifiedEmail: row.requireVerifiedEmail,
|
|
15
15
|
assumeEmailVerified: row.assumeEmailVerified,
|
|
16
|
+
disableLocalAuthentication: row.disableLocalAuthentication,
|
|
16
17
|
configurationKeys: Object.keys(row.configuration).sort()
|
|
17
18
|
};
|
|
18
19
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/audit.ts"],"sourcesContent":["import { JSONValue } from '@contember/schema'\nimport { IdentityProviderRow } from '../../../model/queries/idp/types.js'\n\nexport const idpRowToAuditSnapshot = (row: IdentityProviderRow | null): JSONValue => {\n\tif (!row) {\n\t\treturn null\n\t}\n\treturn {\n\t\tslug: row.slug,\n\t\ttype: row.type,\n\t\tdisabledAt: row.disabledAt ? row.disabledAt.toISOString() : null,\n\t\tautoSignUp: row.autoSignUp,\n\t\texclusive: row.exclusive,\n\t\tinitReturnsConfig: row.initReturnsConfig,\n\t\trequireVerifiedEmail: row.requireVerifiedEmail,\n\t\tassumeEmailVerified: row.assumeEmailVerified,\n\t\tconfigurationKeys: Object.keys(row.configuration).sort(),\n\t}\n}\n"],"names":[],"mappings":";;AAGO,MAAM,wBAAwB,CAAC,QAA+C;AACpF,MAAI,CAAC,KAAK;AACT,WAAO;AAAA,EACR;AACA,SAAO;AAAA,IACN,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,YAAY,IAAI,aAAa,IAAI,WAAW,gBAAgB;AAAA,IAC5D,YAAY,IAAI;AAAA,IAChB,WAAW,IAAI;AAAA,IACf,mBAAmB,IAAI;AAAA,IACvB,sBAAsB,IAAI;AAAA,IAC1B,qBAAqB,IAAI;AAAA,IACzB,mBAAmB,OAAO,KAAK,IAAI,aAAa,EAAE,KAAA;AAAA,EAAK;AAEzD;;"}
|
|
1
|
+
{"version":3,"file":"audit.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/audit.ts"],"sourcesContent":["import { JSONValue } from '@contember/schema'\nimport { IdentityProviderRow } from '../../../model/queries/idp/types.js'\n\nexport const idpRowToAuditSnapshot = (row: IdentityProviderRow | null): JSONValue => {\n\tif (!row) {\n\t\treturn null\n\t}\n\treturn {\n\t\tslug: row.slug,\n\t\ttype: row.type,\n\t\tdisabledAt: row.disabledAt ? row.disabledAt.toISOString() : null,\n\t\tautoSignUp: row.autoSignUp,\n\t\texclusive: row.exclusive,\n\t\tinitReturnsConfig: row.initReturnsConfig,\n\t\trequireVerifiedEmail: row.requireVerifiedEmail,\n\t\tassumeEmailVerified: row.assumeEmailVerified,\n\t\tdisableLocalAuthentication: row.disableLocalAuthentication,\n\t\tconfigurationKeys: Object.keys(row.configuration).sort(),\n\t}\n}\n"],"names":[],"mappings":";;AAGO,MAAM,wBAAwB,CAAC,QAA+C;AACpF,MAAI,CAAC,KAAK;AACT,WAAO;AAAA,EACR;AACA,SAAO;AAAA,IACN,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,YAAY,IAAI,aAAa,IAAI,WAAW,gBAAgB;AAAA,IAC5D,YAAY,IAAI;AAAA,IAChB,WAAW,IAAI;AAAA,IACf,mBAAmB,IAAI;AAAA,IACvB,sBAAsB,IAAI;AAAA,IAC1B,qBAAqB,IAAI;AAAA,IACzB,4BAA4B,IAAI;AAAA,IAChC,mBAAmB,OAAO,KAAK,IAAI,aAAa,EAAE,KAAA;AAAA,EAAK;AAEzD;;"}
|
|
@@ -11,6 +11,7 @@ const idpRowToAuditSnapshot = (row) => {
|
|
|
11
11
|
initReturnsConfig: row.initReturnsConfig,
|
|
12
12
|
requireVerifiedEmail: row.requireVerifiedEmail,
|
|
13
13
|
assumeEmailVerified: row.assumeEmailVerified,
|
|
14
|
+
disableLocalAuthentication: row.disableLocalAuthentication,
|
|
14
15
|
configurationKeys: Object.keys(row.configuration).sort()
|
|
15
16
|
};
|
|
16
17
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/audit.ts"],"sourcesContent":["import { JSONValue } from '@contember/schema'\nimport { IdentityProviderRow } from '../../../model/queries/idp/types.js'\n\nexport const idpRowToAuditSnapshot = (row: IdentityProviderRow | null): JSONValue => {\n\tif (!row) {\n\t\treturn null\n\t}\n\treturn {\n\t\tslug: row.slug,\n\t\ttype: row.type,\n\t\tdisabledAt: row.disabledAt ? row.disabledAt.toISOString() : null,\n\t\tautoSignUp: row.autoSignUp,\n\t\texclusive: row.exclusive,\n\t\tinitReturnsConfig: row.initReturnsConfig,\n\t\trequireVerifiedEmail: row.requireVerifiedEmail,\n\t\tassumeEmailVerified: row.assumeEmailVerified,\n\t\tconfigurationKeys: Object.keys(row.configuration).sort(),\n\t}\n}\n"],"names":[],"mappings":"AAGO,MAAM,wBAAwB,CAAC,QAA+C;AACpF,MAAI,CAAC,KAAK;AACT,WAAO;AAAA,EACR;AACA,SAAO;AAAA,IACN,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,YAAY,IAAI,aAAa,IAAI,WAAW,gBAAgB;AAAA,IAC5D,YAAY,IAAI;AAAA,IAChB,WAAW,IAAI;AAAA,IACf,mBAAmB,IAAI;AAAA,IACvB,sBAAsB,IAAI;AAAA,IAC1B,qBAAqB,IAAI;AAAA,IACzB,mBAAmB,OAAO,KAAK,IAAI,aAAa,EAAE,KAAA;AAAA,EAAK;AAEzD;"}
|
|
1
|
+
{"version":3,"file":"audit.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/idp/audit.ts"],"sourcesContent":["import { JSONValue } from '@contember/schema'\nimport { IdentityProviderRow } from '../../../model/queries/idp/types.js'\n\nexport const idpRowToAuditSnapshot = (row: IdentityProviderRow | null): JSONValue => {\n\tif (!row) {\n\t\treturn null\n\t}\n\treturn {\n\t\tslug: row.slug,\n\t\ttype: row.type,\n\t\tdisabledAt: row.disabledAt ? row.disabledAt.toISOString() : null,\n\t\tautoSignUp: row.autoSignUp,\n\t\texclusive: row.exclusive,\n\t\tinitReturnsConfig: row.initReturnsConfig,\n\t\trequireVerifiedEmail: row.requireVerifiedEmail,\n\t\tassumeEmailVerified: row.assumeEmailVerified,\n\t\tdisableLocalAuthentication: row.disableLocalAuthentication,\n\t\tconfigurationKeys: Object.keys(row.configuration).sort(),\n\t}\n}\n"],"names":[],"mappings":"AAGO,MAAM,wBAAwB,CAAC,QAA+C;AACpF,MAAI,CAAC,KAAK;AACT,WAAO;AAAA,EACR;AACA,SAAO;AAAA,IACN,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,YAAY,IAAI,aAAa,IAAI,WAAW,gBAAgB;AAAA,IAC5D,YAAY,IAAI;AAAA,IAChB,WAAW,IAAI;AAAA,IACf,mBAAmB,IAAI;AAAA,IACvB,sBAAsB,IAAI;AAAA,IAC1B,qBAAqB,IAAI;AAAA,IACzB,4BAA4B,IAAI;AAAA,IAChC,mBAAmB,OAAO,KAAK,IAAI,aAAa,EAAE,KAAA;AAAA,EAAK;AAEzD;"}
|
|
@@ -66,6 +66,10 @@ class PasswordlessMutationResolver {
|
|
|
66
66
|
if (result.error === "PERSON_NOT_FOUND" && !configuration.login.revealUserExists) {
|
|
67
67
|
return errorUtils.createErrorResponse("PASSWORDLESS_DISABLED", "Passwordless sign-in is disabled for this person");
|
|
68
68
|
}
|
|
69
|
+
const { revealUserExists, revealLoginMethod } = configuration.login;
|
|
70
|
+
if (result.error === "IDP_REQUIRED" && !(revealUserExists && revealLoginMethod)) {
|
|
71
|
+
return errorUtils.createErrorResponse("PASSWORDLESS_DISABLED", "Passwordless sign-in is disabled for this person");
|
|
72
|
+
}
|
|
69
73
|
return errorUtils.createErrorResponse(result.error, result.errorMessage);
|
|
70
74
|
}
|
|
71
75
|
return { ok: true, result: result.result };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PasswordlessMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/person/PasswordlessMutationResolver.ts"],"sourcesContent":["import {\n\tActivatePasswordlessOtpResponse,\n\tInitSignInPasswordlessResponse,\n\tMutationActivatePasswordlessOtpArgs,\n\tMutationInitSignInPasswordlessArgs,\n\tMutationResolvers,\n\tMutationSignInPasswordlessArgs,\n\tSignInPasswordlessResponse,\n} from '../../../schema/index.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { CaptchaValidator, ConfigurationQuery, PermissionActions, RateLimiter } from '../../../model/index.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { SignInResponseFactory } from '../../responseHelpers/SignInResponseFactory.js'\nimport { PasswordlessSignInManager } from '../../../model/service/PasswordlessSignInManager.js'\n\nexport class PasswordlessMutationResolver\n\timplements Pick<MutationResolvers, 'initSignInPasswordless' | 'signInPasswordless' | 'activatePasswordlessOtp'>\n{\n\tconstructor(\n\t\tprivate readonly passwordlessSignInManager: PasswordlessSignInManager,\n\t\tprivate readonly signInResponseFactory: SignInResponseFactory,\n\t\tprivate readonly captchaValidator: CaptchaValidator,\n\t\tprivate readonly rateLimiter: RateLimiter,\n\t) {}\n\n\tasync initSignInPasswordless(\n\t\tparent: any,\n\t\targs: MutationInitSignInPasswordlessArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<InitSignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_REQUEST_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to request passwordless sign in',\n\t\t})\n\n\t\tconst configuration = await context.db.queryHandler.fetch(new ConfigurationQuery(context.db.providers))\n\n\t\tconst rl = await this.rateLimiter.consume(context.db, 'passwordless_init_per_ip', context.httpInfo.ip, configuration)\n\t\tif (!rl.ok) {\n\t\t\treturn createErrorResponse('RATE_LIMIT_EXCEEDED', `Too many passwordless sign-in requests. Retry after ${rl.retryAfterSeconds}s.`)\n\t\t}\n\n\t\tconst captchaConfig = this.captchaValidator.extractConfig(configuration)\n\t\tif (this.captchaValidator.isEnabledFor(captchaConfig, 'passwordlessInit')) {\n\t\t\tconst captcha = await this.captchaValidator.verify({\n\t\t\t\tconfig: captchaConfig,\n\t\t\t\ttoken: args.captchaToken ?? undefined,\n\t\t\t\tremoteIp: context.httpInfo.ip,\n\t\t\t})\n\t\t\tif (!captcha.ok) {\n\t\t\t\treturn createErrorResponse('INVALID_CAPTCHA', `Captcha verification failed: ${captcha.reason}`)\n\t\t\t}\n\t\t}\n\n\t\tconst result = await this.passwordlessSignInManager.initSignInPasswordless({\n\t\t\tdb: context.db,\n\t\t\tpermissionContext: context.permissionContext,\n\t\t\temail: args.email,\n\t\t\tmailVariant: args.options?.mailVariant ?? undefined,\n\t\t\tmailProject: args.options?.mailProject ?? undefined,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_init',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\tif (result.error === 'PERSON_NOT_FOUND' && !configuration.login.revealUserExists) {\n\t\t\t\t// If the user does not exist, we don't want to reveal that\n\t\t\t\treturn createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')\n\t\t\t}\n\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true, result: result.result }\n\t}\n\n\tasync signInPasswordless(parent: any, args: MutationSignInPasswordlessArgs, context: TenantResolverContext): Promise<SignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to sign in using passwordless',\n\t\t})\n\t\tconst signIn = await this.passwordlessSignInManager.signInPasswordless({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\tmfaOtp: args.mfaOtp ?? undefined,\n\t\t\tbackupCode: args.backupCode ?? undefined,\n\t\t\tvalidationType: args.validationType,\n\t\t\texpiration: args.expiration ?? undefined,\n\t\t\trequestInfo: context.httpInfo,\n\t\t\ttrustForwardedInfo: args.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login',\n\t\t\tresponse: signIn,\n\t\t})\n\t\tif (signIn.ok && signIn.result.usedBackupCode) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'backup_code_used',\n\t\t\t\tresponse: signIn,\n\t\t\t\tpersonId: signIn.result.person.id,\n\t\t\t})\n\t\t}\n\t\tif (!signIn.ok) {\n\t\t\treturn createErrorResponse(signIn.error, signIn.errorMessage)\n\t\t}\n\t\treturn {\n\t\t\tok: true,\n\t\t\tresult: await this.signInResponseFactory.createResponse(signIn.result, context),\n\t\t}\n\t}\n\n\tasync activatePasswordlessOtp(\n\t\tparent: any,\n\t\targs: MutationActivatePasswordlessOtpArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<ActivatePasswordlessOtpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to activate passwordless OTP',\n\t\t})\n\t\tconst result = await this.passwordlessSignInManager.activatePasswordlessOtp({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\totpHash: args.otpHash,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_exchange',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true }\n\t}\n}\n"],"names":["PermissionActions","ConfigurationQuery","createErrorResponse"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PasswordlessMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/person/PasswordlessMutationResolver.ts"],"sourcesContent":["import {\n\tActivatePasswordlessOtpResponse,\n\tInitSignInPasswordlessResponse,\n\tMutationActivatePasswordlessOtpArgs,\n\tMutationInitSignInPasswordlessArgs,\n\tMutationResolvers,\n\tMutationSignInPasswordlessArgs,\n\tSignInPasswordlessResponse,\n} from '../../../schema/index.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { CaptchaValidator, ConfigurationQuery, PermissionActions, RateLimiter } from '../../../model/index.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { SignInResponseFactory } from '../../responseHelpers/SignInResponseFactory.js'\nimport { PasswordlessSignInManager } from '../../../model/service/PasswordlessSignInManager.js'\n\nexport class PasswordlessMutationResolver\n\timplements Pick<MutationResolvers, 'initSignInPasswordless' | 'signInPasswordless' | 'activatePasswordlessOtp'>\n{\n\tconstructor(\n\t\tprivate readonly passwordlessSignInManager: PasswordlessSignInManager,\n\t\tprivate readonly signInResponseFactory: SignInResponseFactory,\n\t\tprivate readonly captchaValidator: CaptchaValidator,\n\t\tprivate readonly rateLimiter: RateLimiter,\n\t) {}\n\n\tasync initSignInPasswordless(\n\t\tparent: any,\n\t\targs: MutationInitSignInPasswordlessArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<InitSignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_REQUEST_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to request passwordless sign in',\n\t\t})\n\n\t\tconst configuration = await context.db.queryHandler.fetch(new ConfigurationQuery(context.db.providers))\n\n\t\tconst rl = await this.rateLimiter.consume(context.db, 'passwordless_init_per_ip', context.httpInfo.ip, configuration)\n\t\tif (!rl.ok) {\n\t\t\treturn createErrorResponse('RATE_LIMIT_EXCEEDED', `Too many passwordless sign-in requests. Retry after ${rl.retryAfterSeconds}s.`)\n\t\t}\n\n\t\tconst captchaConfig = this.captchaValidator.extractConfig(configuration)\n\t\tif (this.captchaValidator.isEnabledFor(captchaConfig, 'passwordlessInit')) {\n\t\t\tconst captcha = await this.captchaValidator.verify({\n\t\t\t\tconfig: captchaConfig,\n\t\t\t\ttoken: args.captchaToken ?? undefined,\n\t\t\t\tremoteIp: context.httpInfo.ip,\n\t\t\t})\n\t\t\tif (!captcha.ok) {\n\t\t\t\treturn createErrorResponse('INVALID_CAPTCHA', `Captcha verification failed: ${captcha.reason}`)\n\t\t\t}\n\t\t}\n\n\t\tconst result = await this.passwordlessSignInManager.initSignInPasswordless({\n\t\t\tdb: context.db,\n\t\t\tpermissionContext: context.permissionContext,\n\t\t\temail: args.email,\n\t\t\tmailVariant: args.options?.mailVariant ?? undefined,\n\t\t\tmailProject: args.options?.mailProject ?? undefined,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_init',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\tif (result.error === 'PERSON_NOT_FOUND' && !configuration.login.revealUserExists) {\n\t\t\t\t// If the user does not exist, we don't want to reveal that\n\t\t\t\treturn createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')\n\t\t\t}\n\t\t\t// Naming the provider tells the caller both that the person exists and how\n\t\t\t// they authenticate, so it needs both reveal flags. PASSWORDLESS_DISABLED is\n\t\t\t// the neutral answer this endpoint already gives for an unknown address.\n\t\t\tconst { revealUserExists, revealLoginMethod } = configuration.login\n\t\t\tif (result.error === 'IDP_REQUIRED' && !(revealUserExists && revealLoginMethod)) {\n\t\t\t\treturn createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')\n\t\t\t}\n\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true, result: result.result }\n\t}\n\n\tasync signInPasswordless(parent: any, args: MutationSignInPasswordlessArgs, context: TenantResolverContext): Promise<SignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to sign in using passwordless',\n\t\t})\n\t\tconst signIn = await this.passwordlessSignInManager.signInPasswordless({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\tmfaOtp: args.mfaOtp ?? undefined,\n\t\t\tbackupCode: args.backupCode ?? undefined,\n\t\t\tvalidationType: args.validationType,\n\t\t\texpiration: args.expiration ?? undefined,\n\t\t\trequestInfo: context.httpInfo,\n\t\t\ttrustForwardedInfo: args.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login',\n\t\t\tresponse: signIn,\n\t\t})\n\t\tif (signIn.ok && signIn.result.usedBackupCode) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'backup_code_used',\n\t\t\t\tresponse: signIn,\n\t\t\t\tpersonId: signIn.result.person.id,\n\t\t\t})\n\t\t}\n\t\tif (!signIn.ok) {\n\t\t\treturn createErrorResponse(signIn.error, signIn.errorMessage)\n\t\t}\n\t\treturn {\n\t\t\tok: true,\n\t\t\tresult: await this.signInResponseFactory.createResponse(signIn.result, context),\n\t\t}\n\t}\n\n\tasync activatePasswordlessOtp(\n\t\tparent: any,\n\t\targs: MutationActivatePasswordlessOtpArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<ActivatePasswordlessOtpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to activate passwordless OTP',\n\t\t})\n\t\tconst result = await this.passwordlessSignInManager.activatePasswordlessOtp({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\totpHash: args.otpHash,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_exchange',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true }\n\t}\n}\n"],"names":["PermissionActions","ConfigurationQuery","createErrorResponse"],"mappings":"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"}
|
|
@@ -64,6 +64,10 @@ class PasswordlessMutationResolver {
|
|
|
64
64
|
if (result.error === "PERSON_NOT_FOUND" && !configuration.login.revealUserExists) {
|
|
65
65
|
return createErrorResponse("PASSWORDLESS_DISABLED", "Passwordless sign-in is disabled for this person");
|
|
66
66
|
}
|
|
67
|
+
const { revealUserExists, revealLoginMethod } = configuration.login;
|
|
68
|
+
if (result.error === "IDP_REQUIRED" && !(revealUserExists && revealLoginMethod)) {
|
|
69
|
+
return createErrorResponse("PASSWORDLESS_DISABLED", "Passwordless sign-in is disabled for this person");
|
|
70
|
+
}
|
|
67
71
|
return createErrorResponse(result.error, result.errorMessage);
|
|
68
72
|
}
|
|
69
73
|
return { ok: true, result: result.result };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PasswordlessMutationResolver.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/person/PasswordlessMutationResolver.ts"],"sourcesContent":["import {\n\tActivatePasswordlessOtpResponse,\n\tInitSignInPasswordlessResponse,\n\tMutationActivatePasswordlessOtpArgs,\n\tMutationInitSignInPasswordlessArgs,\n\tMutationResolvers,\n\tMutationSignInPasswordlessArgs,\n\tSignInPasswordlessResponse,\n} from '../../../schema/index.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { CaptchaValidator, ConfigurationQuery, PermissionActions, RateLimiter } from '../../../model/index.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { SignInResponseFactory } from '../../responseHelpers/SignInResponseFactory.js'\nimport { PasswordlessSignInManager } from '../../../model/service/PasswordlessSignInManager.js'\n\nexport class PasswordlessMutationResolver\n\timplements Pick<MutationResolvers, 'initSignInPasswordless' | 'signInPasswordless' | 'activatePasswordlessOtp'>\n{\n\tconstructor(\n\t\tprivate readonly passwordlessSignInManager: PasswordlessSignInManager,\n\t\tprivate readonly signInResponseFactory: SignInResponseFactory,\n\t\tprivate readonly captchaValidator: CaptchaValidator,\n\t\tprivate readonly rateLimiter: RateLimiter,\n\t) {}\n\n\tasync initSignInPasswordless(\n\t\tparent: any,\n\t\targs: MutationInitSignInPasswordlessArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<InitSignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_REQUEST_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to request passwordless sign in',\n\t\t})\n\n\t\tconst configuration = await context.db.queryHandler.fetch(new ConfigurationQuery(context.db.providers))\n\n\t\tconst rl = await this.rateLimiter.consume(context.db, 'passwordless_init_per_ip', context.httpInfo.ip, configuration)\n\t\tif (!rl.ok) {\n\t\t\treturn createErrorResponse('RATE_LIMIT_EXCEEDED', `Too many passwordless sign-in requests. Retry after ${rl.retryAfterSeconds}s.`)\n\t\t}\n\n\t\tconst captchaConfig = this.captchaValidator.extractConfig(configuration)\n\t\tif (this.captchaValidator.isEnabledFor(captchaConfig, 'passwordlessInit')) {\n\t\t\tconst captcha = await this.captchaValidator.verify({\n\t\t\t\tconfig: captchaConfig,\n\t\t\t\ttoken: args.captchaToken ?? undefined,\n\t\t\t\tremoteIp: context.httpInfo.ip,\n\t\t\t})\n\t\t\tif (!captcha.ok) {\n\t\t\t\treturn createErrorResponse('INVALID_CAPTCHA', `Captcha verification failed: ${captcha.reason}`)\n\t\t\t}\n\t\t}\n\n\t\tconst result = await this.passwordlessSignInManager.initSignInPasswordless({\n\t\t\tdb: context.db,\n\t\t\tpermissionContext: context.permissionContext,\n\t\t\temail: args.email,\n\t\t\tmailVariant: args.options?.mailVariant ?? undefined,\n\t\t\tmailProject: args.options?.mailProject ?? undefined,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_init',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\tif (result.error === 'PERSON_NOT_FOUND' && !configuration.login.revealUserExists) {\n\t\t\t\t// If the user does not exist, we don't want to reveal that\n\t\t\t\treturn createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')\n\t\t\t}\n\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true, result: result.result }\n\t}\n\n\tasync signInPasswordless(parent: any, args: MutationSignInPasswordlessArgs, context: TenantResolverContext): Promise<SignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to sign in using passwordless',\n\t\t})\n\t\tconst signIn = await this.passwordlessSignInManager.signInPasswordless({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\tmfaOtp: args.mfaOtp ?? undefined,\n\t\t\tbackupCode: args.backupCode ?? undefined,\n\t\t\tvalidationType: args.validationType,\n\t\t\texpiration: args.expiration ?? undefined,\n\t\t\trequestInfo: context.httpInfo,\n\t\t\ttrustForwardedInfo: args.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login',\n\t\t\tresponse: signIn,\n\t\t})\n\t\tif (signIn.ok && signIn.result.usedBackupCode) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'backup_code_used',\n\t\t\t\tresponse: signIn,\n\t\t\t\tpersonId: signIn.result.person.id,\n\t\t\t})\n\t\t}\n\t\tif (!signIn.ok) {\n\t\t\treturn createErrorResponse(signIn.error, signIn.errorMessage)\n\t\t}\n\t\treturn {\n\t\t\tok: true,\n\t\t\tresult: await this.signInResponseFactory.createResponse(signIn.result, context),\n\t\t}\n\t}\n\n\tasync activatePasswordlessOtp(\n\t\tparent: any,\n\t\targs: MutationActivatePasswordlessOtpArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<ActivatePasswordlessOtpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to activate passwordless OTP',\n\t\t})\n\t\tconst result = await this.passwordlessSignInManager.activatePasswordlessOtp({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\totpHash: args.otpHash,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_exchange',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true }\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PasswordlessMutationResolver.js","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/person/PasswordlessMutationResolver.ts"],"sourcesContent":["import {\n\tActivatePasswordlessOtpResponse,\n\tInitSignInPasswordlessResponse,\n\tMutationActivatePasswordlessOtpArgs,\n\tMutationInitSignInPasswordlessArgs,\n\tMutationResolvers,\n\tMutationSignInPasswordlessArgs,\n\tSignInPasswordlessResponse,\n} from '../../../schema/index.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { CaptchaValidator, ConfigurationQuery, PermissionActions, RateLimiter } from '../../../model/index.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { SignInResponseFactory } from '../../responseHelpers/SignInResponseFactory.js'\nimport { PasswordlessSignInManager } from '../../../model/service/PasswordlessSignInManager.js'\n\nexport class PasswordlessMutationResolver\n\timplements Pick<MutationResolvers, 'initSignInPasswordless' | 'signInPasswordless' | 'activatePasswordlessOtp'>\n{\n\tconstructor(\n\t\tprivate readonly passwordlessSignInManager: PasswordlessSignInManager,\n\t\tprivate readonly signInResponseFactory: SignInResponseFactory,\n\t\tprivate readonly captchaValidator: CaptchaValidator,\n\t\tprivate readonly rateLimiter: RateLimiter,\n\t) {}\n\n\tasync initSignInPasswordless(\n\t\tparent: any,\n\t\targs: MutationInitSignInPasswordlessArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<InitSignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_REQUEST_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to request passwordless sign in',\n\t\t})\n\n\t\tconst configuration = await context.db.queryHandler.fetch(new ConfigurationQuery(context.db.providers))\n\n\t\tconst rl = await this.rateLimiter.consume(context.db, 'passwordless_init_per_ip', context.httpInfo.ip, configuration)\n\t\tif (!rl.ok) {\n\t\t\treturn createErrorResponse('RATE_LIMIT_EXCEEDED', `Too many passwordless sign-in requests. Retry after ${rl.retryAfterSeconds}s.`)\n\t\t}\n\n\t\tconst captchaConfig = this.captchaValidator.extractConfig(configuration)\n\t\tif (this.captchaValidator.isEnabledFor(captchaConfig, 'passwordlessInit')) {\n\t\t\tconst captcha = await this.captchaValidator.verify({\n\t\t\t\tconfig: captchaConfig,\n\t\t\t\ttoken: args.captchaToken ?? undefined,\n\t\t\t\tremoteIp: context.httpInfo.ip,\n\t\t\t})\n\t\t\tif (!captcha.ok) {\n\t\t\t\treturn createErrorResponse('INVALID_CAPTCHA', `Captcha verification failed: ${captcha.reason}`)\n\t\t\t}\n\t\t}\n\n\t\tconst result = await this.passwordlessSignInManager.initSignInPasswordless({\n\t\t\tdb: context.db,\n\t\t\tpermissionContext: context.permissionContext,\n\t\t\temail: args.email,\n\t\t\tmailVariant: args.options?.mailVariant ?? undefined,\n\t\t\tmailProject: args.options?.mailProject ?? undefined,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_init',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\tif (result.error === 'PERSON_NOT_FOUND' && !configuration.login.revealUserExists) {\n\t\t\t\t// If the user does not exist, we don't want to reveal that\n\t\t\t\treturn createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')\n\t\t\t}\n\t\t\t// Naming the provider tells the caller both that the person exists and how\n\t\t\t// they authenticate, so it needs both reveal flags. PASSWORDLESS_DISABLED is\n\t\t\t// the neutral answer this endpoint already gives for an unknown address.\n\t\t\tconst { revealUserExists, revealLoginMethod } = configuration.login\n\t\t\tif (result.error === 'IDP_REQUIRED' && !(revealUserExists && revealLoginMethod)) {\n\t\t\t\treturn createErrorResponse('PASSWORDLESS_DISABLED', 'Passwordless sign-in is disabled for this person')\n\t\t\t}\n\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true, result: result.result }\n\t}\n\n\tasync signInPasswordless(parent: any, args: MutationSignInPasswordlessArgs, context: TenantResolverContext): Promise<SignInPasswordlessResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to sign in using passwordless',\n\t\t})\n\t\tconst signIn = await this.passwordlessSignInManager.signInPasswordless({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\tmfaOtp: args.mfaOtp ?? undefined,\n\t\t\tbackupCode: args.backupCode ?? undefined,\n\t\t\tvalidationType: args.validationType,\n\t\t\texpiration: args.expiration ?? undefined,\n\t\t\trequestInfo: context.httpInfo,\n\t\t\ttrustForwardedInfo: args.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login',\n\t\t\tresponse: signIn,\n\t\t})\n\t\tif (signIn.ok && signIn.result.usedBackupCode) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'backup_code_used',\n\t\t\t\tresponse: signIn,\n\t\t\t\tpersonId: signIn.result.person.id,\n\t\t\t})\n\t\t}\n\t\tif (!signIn.ok) {\n\t\t\treturn createErrorResponse(signIn.error, signIn.errorMessage)\n\t\t}\n\t\treturn {\n\t\t\tok: true,\n\t\t\tresult: await this.signInResponseFactory.createResponse(signIn.result, context),\n\t\t}\n\t}\n\n\tasync activatePasswordlessOtp(\n\t\tparent: any,\n\t\targs: MutationActivatePasswordlessOtpArgs,\n\t\tcontext: TenantResolverContext,\n\t): Promise<ActivatePasswordlessOtpResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_PASSWORDLESS_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to activate passwordless OTP',\n\t\t})\n\t\tconst result = await this.passwordlessSignInManager.activatePasswordlessOtp({\n\t\t\tdb: context.db,\n\t\t\trequestId: args.requestId,\n\t\t\ttoken: args.token,\n\t\t\totpHash: args.otpHash,\n\t\t})\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'passwordless_login_exchange',\n\t\t\tresponse: result,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\treturn createErrorResponse(result.error, result.errorMessage)\n\t\t}\n\t\treturn { ok: true }\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -111,8 +111,8 @@ class SignInMutationResolver {
|
|
|
111
111
|
}
|
|
112
112
|
if (!response.ok) {
|
|
113
113
|
const { revealUserExists, revealLoginMethod } = configuration.login;
|
|
114
|
-
const collapseAll = !revealUserExists && ["NO_PASSWORD_SET", "PERSON_DISABLED", "INVALID_PASSWORD", "UNKNOWN_EMAIL"].includes(response.error);
|
|
115
|
-
const collapseMethodOnly = revealUserExists && !revealLoginMethod && ["NO_PASSWORD_SET", "INVALID_PASSWORD"].includes(response.error);
|
|
114
|
+
const collapseAll = !revealUserExists && ["NO_PASSWORD_SET", "PERSON_DISABLED", "INVALID_PASSWORD", "UNKNOWN_EMAIL", "IDP_REQUIRED"].includes(response.error);
|
|
115
|
+
const collapseMethodOnly = revealUserExists && !revealLoginMethod && ["NO_PASSWORD_SET", "INVALID_PASSWORD", "IDP_REQUIRED"].includes(response.error);
|
|
116
116
|
if (collapseAll || collapseMethodOnly) {
|
|
117
117
|
return errorUtils.createErrorResponse("INVALID_CREDENTIALS", "Invalid credentials");
|
|
118
118
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignInMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/person/SignInMutationResolver.ts"],"sourcesContent":["import {\n\tCreateSessionTokenResponse,\n\tMutationCreateSessionTokenArgs,\n\tMutationResolvers,\n\tMutationSignInArgs,\n\tSignInResponse,\n} from '../../../schema/index.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ConfigurationQuery, PermissionActions, PersonUniqueIdentifier, RateLimiter, SignInManager } from '../../../model/index.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { SignInResponseFactory } from '../../responseHelpers/SignInResponseFactory.js'\nimport { UserInputError } from '@contember/graphql-utils'\nimport { NextLoginAttemptQuery } from '../../../model/queries/authLog/NextLoginAttemptQuery.js'\nimport { ResponseError } from '../../../model/utils/Response.js'\n\nexport class SignInMutationResolver implements MutationResolvers {\n\tconstructor(\n\t\tprivate readonly signInManager: SignInManager,\n\t\tprivate readonly signInResponseFactory: SignInResponseFactory,\n\t\tprivate readonly rateLimiter: RateLimiter,\n\t) {}\n\n\tasync signIn(parent: any, args: MutationSignInArgs, context: TenantResolverContext): Promise<SignInResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to sign in',\n\t\t})\n\n\t\tconst configuration = await context.db.queryHandler.fetch(new ConfigurationQuery(context.db.providers))\n\n\t\tconst ipGate = await this.rateLimiter.consume(context.db, 'login_per_ip', context.httpInfo.ip, configuration)\n\t\tif (!ipGate.ok) {\n\t\t\treturn createErrorResponse(\n\t\t\t\tnew ResponseError('RATE_LIMIT_EXCEEDED', `Too many sign-in attempts from this IP. Retry after ${ipGate.retryAfterSeconds}s.`, {\n\t\t\t\t\tretryAfter: ipGate.retryAfterSeconds,\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\n\t\tconst nextAllowedSignIn = await context.db.queryHandler.fetch(new NextLoginAttemptQuery(args.email))\n\t\tif (nextAllowedSignIn > new Date()) {\n\t\t\treturn createErrorResponse(\n\t\t\t\tnew ResponseError('RATE_LIMIT_EXCEEDED', `Too many attempts, please try again later.`, {\n\t\t\t\t\tretryAfter: Math.ceil((nextAllowedSignIn.getTime() - Date.now()) / 1000),\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\n\t\tconst response = await this.signInManager.signIn(\n\t\t\tcontext.db,\n\t\t\targs.email,\n\t\t\targs.password,\n\t\t\targs.expiration || undefined,\n\t\t\targs.otpToken || undefined,\n\t\t\tcontext.httpInfo,\n\t\t\targs.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t\targs.backupCode || undefined,\n\t\t\tconfiguration,\n\t\t\tcontext.httpInfo.geoCountry,\n\t\t)\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'login',\n\t\t\tresponse,\n\t\t})\n\t\tif (response.ok && response.result.usedBackupCode) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'backup_code_used',\n\t\t\t\tresponse,\n\t\t\t\tpersonId: response.result.person.id,\n\t\t\t})\n\t\t}\n\t\tif (!response.ok && response.metadata?.emailOtpSent) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'email_otp_sent',\n\t\t\t\tresponse,\n\t\t\t})\n\t\t}\n\t\tif (!response.ok && response.metadata?.mfaEnrollmentRequired) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'mfa_enrollment_required',\n\t\t\t\tresponse,\n\t\t\t})\n\t\t}\n\t\t// A03 anomaly audit. The risk scoring runs in SignInManager; the audit\n\t\t// entries are emitted here at the resolver layer (per convention), with the\n\t\t// score + reasons snapshot in event_data.\n\t\tif (response.ok && response.result.unusualLoginDetected) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'unusual_login_detected',\n\t\t\t\tresponse,\n\t\t\t\tpersonId: response.result.person.id,\n\t\t\t\teventData: response.result.risk ? { ...response.result.risk } : undefined,\n\t\t\t})\n\t\t}\n\t\tif (!response.ok && response.metadata?.stepUpRequired) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'unusual_login_detected',\n\t\t\t\tresponse,\n\t\t\t\teventData: response.metadata.risk ? { ...response.metadata.risk } : undefined,\n\t\t\t})\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'step_up_required',\n\t\t\t\tresponse,\n\t\t\t\teventData: response.metadata.risk ? { ...response.metadata.risk } : undefined,\n\t\t\t})\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst { revealUserExists, revealLoginMethod } = configuration.login\n\t\t\tconst collapseAll = !revealUserExists\n\t\t\t\t&& ['NO_PASSWORD_SET', 'PERSON_DISABLED', 'INVALID_PASSWORD', 'UNKNOWN_EMAIL'].includes(response.error)\n\t\t\tconst collapseMethodOnly = revealUserExists\n\t\t\t\t&& !revealLoginMethod\n\t\t\t\t&& ['NO_PASSWORD_SET', 'INVALID_PASSWORD'].includes(response.error)\n\t\t\tif (collapseAll || collapseMethodOnly) {\n\t\t\t\treturn createErrorResponse('INVALID_CREDENTIALS', 'Invalid credentials')\n\t\t\t}\n\t\t\t// Preserve the additive mfaEnrollment payload on MFA_ENROLLMENT_REQUIRED (A06).\n\t\t\tif (response.error === 'MFA_ENROLLMENT_REQUIRED' && response.metadata?.mfaEnrollment) {\n\t\t\t\tconst error = {\n\t\t\t\t\tcode: response.error,\n\t\t\t\t\tdeveloperMessage: response.errorMessage,\n\t\t\t\t\tmfaEnrollment: response.metadata.mfaEnrollment,\n\t\t\t\t}\n\t\t\t\treturn { ok: false, error, errors: [error] }\n\t\t\t}\n\t\t\treturn createErrorResponse(response.error, response.errorMessage)\n\t\t}\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\terrors: [],\n\t\t\tresult: await this.signInResponseFactory.createResponse(response.result, context),\n\t\t}\n\t}\n\n\tasync createSessionToken(parent: any, args: MutationCreateSessionTokenArgs, context: TenantResolverContext): Promise<CreateSessionTokenResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_CREATE_SESSION_KEY(),\n\t\t\tmessage: 'You are not allowed to create a session key',\n\t\t})\n\t\tlet identifier: PersonUniqueIdentifier\n\t\tif (args.email) {\n\t\t\tidentifier = { type: 'email', email: args.email }\n\t\t} else if (args.personId) {\n\t\t\tidentifier = { type: 'id', id: args.personId }\n\t\t} else {\n\t\t\tthrow new UserInputError(`Please provide either email or personId`)\n\t\t}\n\n\t\tconst response = await this.signInManager.createSessionToken(\n\t\t\tcontext.db,\n\t\t\tidentifier,\n\t\t\targs.expiration || undefined,\n\t\t\tasync person =>\n\t\t\t\tawait context.requireAccess({\n\t\t\t\t\taction: PermissionActions.PERSON_CREATE_SESSION_KEY(person.roles),\n\t\t\t\t\tmessage: 'You are not allowed to create a session key for this person.',\n\t\t\t\t}),\n\t\t\tcontext.httpInfo,\n\t\t\targs.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t)\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'create_session_token',\n\t\t\tresponse: response,\n\t\t})\n\n\t\tif (!response.ok) {\n\t\t\treturn createErrorResponse(response.error, response.errorMessage)\n\t\t}\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\tresult: await this.signInResponseFactory.createResponse(response.result, context),\n\t\t}\n\t}\n}\n"],"names":["PermissionActions","ConfigurationQuery","createErrorResponse","ResponseError","NextLoginAttemptQuery","UserInputError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SignInMutationResolver.cjs","sources":["../../../../../../../packages/engine-tenant-api/src/resolvers/mutation/person/SignInMutationResolver.ts"],"sourcesContent":["import {\n\tCreateSessionTokenResponse,\n\tMutationCreateSessionTokenArgs,\n\tMutationResolvers,\n\tMutationSignInArgs,\n\tSignInResponse,\n} from '../../../schema/index.js'\nimport { TenantResolverContext } from '../../TenantResolverContext.js'\nimport { ConfigurationQuery, PermissionActions, PersonUniqueIdentifier, RateLimiter, SignInManager } from '../../../model/index.js'\nimport { createErrorResponse } from '../../errorUtils.js'\nimport { SignInResponseFactory } from '../../responseHelpers/SignInResponseFactory.js'\nimport { UserInputError } from '@contember/graphql-utils'\nimport { NextLoginAttemptQuery } from '../../../model/queries/authLog/NextLoginAttemptQuery.js'\nimport { ResponseError } from '../../../model/utils/Response.js'\n\nexport class SignInMutationResolver implements MutationResolvers {\n\tconstructor(\n\t\tprivate readonly signInManager: SignInManager,\n\t\tprivate readonly signInResponseFactory: SignInResponseFactory,\n\t\tprivate readonly rateLimiter: RateLimiter,\n\t) {}\n\n\tasync signIn(parent: any, args: MutationSignInArgs, context: TenantResolverContext): Promise<SignInResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_SIGN_IN,\n\t\t\tmessage: 'You are not allowed to sign in',\n\t\t})\n\n\t\tconst configuration = await context.db.queryHandler.fetch(new ConfigurationQuery(context.db.providers))\n\n\t\tconst ipGate = await this.rateLimiter.consume(context.db, 'login_per_ip', context.httpInfo.ip, configuration)\n\t\tif (!ipGate.ok) {\n\t\t\treturn createErrorResponse(\n\t\t\t\tnew ResponseError('RATE_LIMIT_EXCEEDED', `Too many sign-in attempts from this IP. Retry after ${ipGate.retryAfterSeconds}s.`, {\n\t\t\t\t\tretryAfter: ipGate.retryAfterSeconds,\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\n\t\tconst nextAllowedSignIn = await context.db.queryHandler.fetch(new NextLoginAttemptQuery(args.email))\n\t\tif (nextAllowedSignIn > new Date()) {\n\t\t\treturn createErrorResponse(\n\t\t\t\tnew ResponseError('RATE_LIMIT_EXCEEDED', `Too many attempts, please try again later.`, {\n\t\t\t\t\tretryAfter: Math.ceil((nextAllowedSignIn.getTime() - Date.now()) / 1000),\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\n\t\tconst response = await this.signInManager.signIn(\n\t\t\tcontext.db,\n\t\t\targs.email,\n\t\t\targs.password,\n\t\t\targs.expiration || undefined,\n\t\t\targs.otpToken || undefined,\n\t\t\tcontext.httpInfo,\n\t\t\targs.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t\targs.backupCode || undefined,\n\t\t\tconfiguration,\n\t\t\tcontext.httpInfo.geoCountry,\n\t\t)\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'login',\n\t\t\tresponse,\n\t\t})\n\t\tif (response.ok && response.result.usedBackupCode) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'backup_code_used',\n\t\t\t\tresponse,\n\t\t\t\tpersonId: response.result.person.id,\n\t\t\t})\n\t\t}\n\t\tif (!response.ok && response.metadata?.emailOtpSent) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'email_otp_sent',\n\t\t\t\tresponse,\n\t\t\t})\n\t\t}\n\t\tif (!response.ok && response.metadata?.mfaEnrollmentRequired) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'mfa_enrollment_required',\n\t\t\t\tresponse,\n\t\t\t})\n\t\t}\n\t\t// A03 anomaly audit. The risk scoring runs in SignInManager; the audit\n\t\t// entries are emitted here at the resolver layer (per convention), with the\n\t\t// score + reasons snapshot in event_data.\n\t\tif (response.ok && response.result.unusualLoginDetected) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'unusual_login_detected',\n\t\t\t\tresponse,\n\t\t\t\tpersonId: response.result.person.id,\n\t\t\t\teventData: response.result.risk ? { ...response.result.risk } : undefined,\n\t\t\t})\n\t\t}\n\t\tif (!response.ok && response.metadata?.stepUpRequired) {\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'unusual_login_detected',\n\t\t\t\tresponse,\n\t\t\t\teventData: response.metadata.risk ? { ...response.metadata.risk } : undefined,\n\t\t\t})\n\t\t\tawait context.logAuthAction({\n\t\t\t\ttype: 'step_up_required',\n\t\t\t\tresponse,\n\t\t\t\teventData: response.metadata.risk ? { ...response.metadata.risk } : undefined,\n\t\t\t})\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst { revealUserExists, revealLoginMethod } = configuration.login\n\t\t\tconst collapseAll = !revealUserExists\n\t\t\t\t&& ['NO_PASSWORD_SET', 'PERSON_DISABLED', 'INVALID_PASSWORD', 'UNKNOWN_EMAIL', 'IDP_REQUIRED'].includes(response.error)\n\t\t\tconst collapseMethodOnly = revealUserExists\n\t\t\t\t&& !revealLoginMethod\n\t\t\t\t&& ['NO_PASSWORD_SET', 'INVALID_PASSWORD', 'IDP_REQUIRED'].includes(response.error)\n\t\t\tif (collapseAll || collapseMethodOnly) {\n\t\t\t\treturn createErrorResponse('INVALID_CREDENTIALS', 'Invalid credentials')\n\t\t\t}\n\t\t\t// Preserve the additive mfaEnrollment payload on MFA_ENROLLMENT_REQUIRED (A06).\n\t\t\tif (response.error === 'MFA_ENROLLMENT_REQUIRED' && response.metadata?.mfaEnrollment) {\n\t\t\t\tconst error = {\n\t\t\t\t\tcode: response.error,\n\t\t\t\t\tdeveloperMessage: response.errorMessage,\n\t\t\t\t\tmfaEnrollment: response.metadata.mfaEnrollment,\n\t\t\t\t}\n\t\t\t\treturn { ok: false, error, errors: [error] }\n\t\t\t}\n\t\t\treturn createErrorResponse(response.error, response.errorMessage)\n\t\t}\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\terrors: [],\n\t\t\tresult: await this.signInResponseFactory.createResponse(response.result, context),\n\t\t}\n\t}\n\n\tasync createSessionToken(parent: any, args: MutationCreateSessionTokenArgs, context: TenantResolverContext): Promise<CreateSessionTokenResponse> {\n\t\tawait context.requireAccess({\n\t\t\taction: PermissionActions.PERSON_CREATE_SESSION_KEY(),\n\t\t\tmessage: 'You are not allowed to create a session key',\n\t\t})\n\t\tlet identifier: PersonUniqueIdentifier\n\t\tif (args.email) {\n\t\t\tidentifier = { type: 'email', email: args.email }\n\t\t} else if (args.personId) {\n\t\t\tidentifier = { type: 'id', id: args.personId }\n\t\t} else {\n\t\t\tthrow new UserInputError(`Please provide either email or personId`)\n\t\t}\n\n\t\tconst response = await this.signInManager.createSessionToken(\n\t\t\tcontext.db,\n\t\t\tidentifier,\n\t\t\targs.expiration || undefined,\n\t\t\tasync person =>\n\t\t\t\tawait context.requireAccess({\n\t\t\t\t\taction: PermissionActions.PERSON_CREATE_SESSION_KEY(person.roles),\n\t\t\t\t\tmessage: 'You are not allowed to create a session key for this person.',\n\t\t\t\t}),\n\t\t\tcontext.httpInfo,\n\t\t\targs.options?.trustForwardedClientInfo === true && context.trustForwardedInfo,\n\t\t)\n\t\tawait context.logAuthAction({\n\t\t\ttype: 'create_session_token',\n\t\t\tresponse: response,\n\t\t})\n\n\t\tif (!response.ok) {\n\t\t\treturn createErrorResponse(response.error, response.errorMessage)\n\t\t}\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\tresult: await this.signInResponseFactory.createResponse(response.result, context),\n\t\t}\n\t}\n}\n"],"names":["PermissionActions","ConfigurationQuery","createErrorResponse","ResponseError","NextLoginAttemptQuery","UserInputError"],"mappings":"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"}
|
|
@@ -109,8 +109,8 @@ class SignInMutationResolver {
|
|
|
109
109
|
}
|
|
110
110
|
if (!response.ok) {
|
|
111
111
|
const { revealUserExists, revealLoginMethod } = configuration.login;
|
|
112
|
-
const collapseAll = !revealUserExists && ["NO_PASSWORD_SET", "PERSON_DISABLED", "INVALID_PASSWORD", "UNKNOWN_EMAIL"].includes(response.error);
|
|
113
|
-
const collapseMethodOnly = revealUserExists && !revealLoginMethod && ["NO_PASSWORD_SET", "INVALID_PASSWORD"].includes(response.error);
|
|
112
|
+
const collapseAll = !revealUserExists && ["NO_PASSWORD_SET", "PERSON_DISABLED", "INVALID_PASSWORD", "UNKNOWN_EMAIL", "IDP_REQUIRED"].includes(response.error);
|
|
113
|
+
const collapseMethodOnly = revealUserExists && !revealLoginMethod && ["NO_PASSWORD_SET", "INVALID_PASSWORD", "IDP_REQUIRED"].includes(response.error);
|
|
114
114
|
if (collapseAll || collapseMethodOnly) {
|
|
115
115
|
return createErrorResponse("INVALID_CREDENTIALS", "Invalid credentials");
|
|
116
116
|
}
|