@portaidentity/server 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/applications/cache.d.ts +64 -0
- package/dist/applications/cache.d.ts.map +1 -0
- package/dist/applications/cache.js +161 -0
- package/dist/applications/cache.js.map +1 -0
- package/dist/applications/errors.d.ts +25 -0
- package/dist/applications/errors.d.ts.map +1 -0
- package/dist/applications/errors.js +31 -0
- package/dist/applications/errors.js.map +1 -0
- package/dist/applications/index.d.ts +17 -0
- package/dist/applications/index.d.ts.map +1 -0
- package/dist/applications/index.js +19 -0
- package/dist/applications/index.js.map +1 -0
- package/dist/applications/repository.d.ts +184 -0
- package/dist/applications/repository.d.ts.map +1 -0
- package/dist/applications/repository.js +367 -0
- package/dist/applications/repository.js.map +1 -0
- package/dist/applications/service.d.ts +160 -0
- package/dist/applications/service.d.ts.map +1 -0
- package/dist/applications/service.js +388 -0
- package/dist/applications/service.js.map +1 -0
- package/dist/applications/slugs.d.ts +61 -0
- package/dist/applications/slugs.d.ts.map +1 -0
- package/dist/applications/slugs.js +115 -0
- package/dist/applications/slugs.js.map +1 -0
- package/dist/applications/types.d.ts +129 -0
- package/dist/applications/types.d.ts.map +1 -0
- package/dist/applications/types.js +62 -0
- package/dist/applications/types.js.map +1 -0
- package/dist/auth/csrf.d.ts +71 -0
- package/dist/auth/csrf.d.ts.map +1 -0
- package/dist/auth/csrf.js +112 -0
- package/dist/auth/csrf.js.map +1 -0
- package/dist/auth/email-renderer.d.ts +37 -0
- package/dist/auth/email-renderer.d.ts.map +1 -0
- package/dist/auth/email-renderer.js +116 -0
- package/dist/auth/email-renderer.js.map +1 -0
- package/dist/auth/email-service.d.ts +154 -0
- package/dist/auth/email-service.d.ts.map +1 -0
- package/dist/auth/email-service.js +415 -0
- package/dist/auth/email-service.js.map +1 -0
- package/dist/auth/email-transport.d.ts +65 -0
- package/dist/auth/email-transport.d.ts.map +1 -0
- package/dist/auth/email-transport.js +82 -0
- package/dist/auth/email-transport.js.map +1 -0
- package/dist/auth/i18n.d.ts +81 -0
- package/dist/auth/i18n.d.ts.map +1 -0
- package/dist/auth/i18n.js +395 -0
- package/dist/auth/i18n.js.map +1 -0
- package/dist/auth/index.d.ts +28 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +31 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/interaction-authority.d.ts +39 -0
- package/dist/auth/interaction-authority.d.ts.map +1 -0
- package/dist/auth/interaction-authority.js +30 -0
- package/dist/auth/interaction-authority.js.map +1 -0
- package/dist/auth/magic-link-session.d.ts +81 -0
- package/dist/auth/magic-link-session.d.ts.map +1 -0
- package/dist/auth/magic-link-session.js +183 -0
- package/dist/auth/magic-link-session.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +143 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +220 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/auth/recovery-crypto.d.ts +34 -0
- package/dist/auth/recovery-crypto.d.ts.map +1 -0
- package/dist/auth/recovery-crypto.js +103 -0
- package/dist/auth/recovery-crypto.js.map +1 -0
- package/dist/auth/recovery-job-processor.d.ts +24 -0
- package/dist/auth/recovery-job-processor.d.ts.map +1 -0
- package/dist/auth/recovery-job-processor.js +123 -0
- package/dist/auth/recovery-job-processor.js.map +1 -0
- package/dist/auth/recovery-job-repository.d.ts +219 -0
- package/dist/auth/recovery-job-repository.d.ts.map +1 -0
- package/dist/auth/recovery-job-repository.js +340 -0
- package/dist/auth/recovery-job-repository.js.map +1 -0
- package/dist/auth/recovery-service.d.ts +27 -0
- package/dist/auth/recovery-service.d.ts.map +1 -0
- package/dist/auth/recovery-service.js +53 -0
- package/dist/auth/recovery-service.js.map +1 -0
- package/dist/auth/recovery-worker.d.ts +158 -0
- package/dist/auth/recovery-worker.d.ts.map +1 -0
- package/dist/auth/recovery-worker.js +306 -0
- package/dist/auth/recovery-worker.js.map +1 -0
- package/dist/auth/template-engine.d.ts +134 -0
- package/dist/auth/template-engine.d.ts.map +1 -0
- package/dist/auth/template-engine.js +225 -0
- package/dist/auth/template-engine.js.map +1 -0
- package/dist/auth/token-repository.d.ts +238 -0
- package/dist/auth/token-repository.d.ts.map +1 -0
- package/dist/auth/token-repository.js +475 -0
- package/dist/auth/token-repository.js.map +1 -0
- package/dist/auth/tokens.d.ts +45 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +51 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/cli/bootstrap.d.ts +55 -0
- package/dist/cli/bootstrap.d.ts.map +1 -0
- package/dist/cli/bootstrap.js +90 -0
- package/dist/cli/bootstrap.js.map +1 -0
- package/dist/cli/commands/health.d.ts +20 -0
- package/dist/cli/commands/health.d.ts.map +1 -0
- package/dist/cli/commands/health.js +80 -0
- package/dist/cli/commands/health.js.map +1 -0
- package/dist/cli/commands/init.d.ts +48 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +349 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/migrate.d.ts +19 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -0
- package/dist/cli/commands/migrate.js +88 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/commands/seed.d.ts +19 -0
- package/dist/cli/commands/seed.d.ts.map +1 -0
- package/dist/cli/commands/seed.js +71 -0
- package/dist/cli/commands/seed.js.map +1 -0
- package/dist/cli/commands/user-2fa.d.ts +29 -0
- package/dist/cli/commands/user-2fa.d.ts.map +1 -0
- package/dist/cli/commands/user-2fa.js +113 -0
- package/dist/cli/commands/user-2fa.js.map +1 -0
- package/dist/cli/error-handler.d.ts +31 -0
- package/dist/cli/error-handler.d.ts.map +1 -0
- package/dist/cli/error-handler.js +65 -0
- package/dist/cli/error-handler.js.map +1 -0
- package/dist/cli/index.d.ts +48 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +99 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +94 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +124 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/prompt.d.ts +48 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +96 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/clients/cache.d.ts +63 -0
- package/dist/clients/cache.d.ts.map +1 -0
- package/dist/clients/cache.js +160 -0
- package/dist/clients/cache.js.map +1 -0
- package/dist/clients/crypto.d.ts +69 -0
- package/dist/clients/crypto.d.ts.map +1 -0
- package/dist/clients/crypto.js +83 -0
- package/dist/clients/crypto.js.map +1 -0
- package/dist/clients/errors.d.ts +27 -0
- package/dist/clients/errors.d.ts.map +1 -0
- package/dist/clients/errors.js +33 -0
- package/dist/clients/errors.js.map +1 -0
- package/dist/clients/index.d.ts +19 -0
- package/dist/clients/index.d.ts.map +1 -0
- package/dist/clients/index.js +23 -0
- package/dist/clients/index.js.map +1 -0
- package/dist/clients/repository.d.ts +161 -0
- package/dist/clients/repository.d.ts.map +1 -0
- package/dist/clients/repository.js +318 -0
- package/dist/clients/repository.js.map +1 -0
- package/dist/clients/resolve-login-methods.d.ts +74 -0
- package/dist/clients/resolve-login-methods.d.ts.map +1 -0
- package/dist/clients/resolve-login-methods.js +68 -0
- package/dist/clients/resolve-login-methods.js.map +1 -0
- package/dist/clients/secret-repository.d.ts +109 -0
- package/dist/clients/secret-repository.d.ts.map +1 -0
- package/dist/clients/secret-repository.js +166 -0
- package/dist/clients/secret-repository.js.map +1 -0
- package/dist/clients/secret-service.d.ts +87 -0
- package/dist/clients/secret-service.d.ts.map +1 -0
- package/dist/clients/secret-service.js +204 -0
- package/dist/clients/secret-service.js.map +1 -0
- package/dist/clients/service.d.ts +158 -0
- package/dist/clients/service.d.ts.map +1 -0
- package/dist/clients/service.js +523 -0
- package/dist/clients/service.js.map +1 -0
- package/dist/clients/types.d.ts +214 -0
- package/dist/clients/types.d.ts.map +1 -0
- package/dist/clients/types.js +77 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/clients/validators.d.ts +87 -0
- package/dist/clients/validators.d.ts.map +1 -0
- package/dist/clients/validators.js +164 -0
- package/dist/clients/validators.js.map +1 -0
- package/dist/config/index.d.ts +23 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +52 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +35 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +167 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/custom-claims/cache.d.ts +48 -0
- package/dist/custom-claims/cache.d.ts.map +1 -0
- package/dist/custom-claims/cache.js +122 -0
- package/dist/custom-claims/cache.js.map +1 -0
- package/dist/custom-claims/errors.d.ts +25 -0
- package/dist/custom-claims/errors.d.ts.map +1 -0
- package/dist/custom-claims/errors.js +31 -0
- package/dist/custom-claims/errors.js.map +1 -0
- package/dist/custom-claims/index.d.ts +14 -0
- package/dist/custom-claims/index.d.ts.map +1 -0
- package/dist/custom-claims/index.js +17 -0
- package/dist/custom-claims/index.js.map +1 -0
- package/dist/custom-claims/repository.d.ts +147 -0
- package/dist/custom-claims/repository.d.ts.map +1 -0
- package/dist/custom-claims/repository.js +338 -0
- package/dist/custom-claims/repository.js.map +1 -0
- package/dist/custom-claims/service.d.ts +129 -0
- package/dist/custom-claims/service.d.ts.map +1 -0
- package/dist/custom-claims/service.js +296 -0
- package/dist/custom-claims/service.js.map +1 -0
- package/dist/custom-claims/types.d.ts +132 -0
- package/dist/custom-claims/types.d.ts.map +1 -0
- package/dist/custom-claims/types.js +61 -0
- package/dist/custom-claims/types.js.map +1 -0
- package/dist/custom-claims/validators.d.ts +61 -0
- package/dist/custom-claims/validators.d.ts.map +1 -0
- package/dist/custom-claims/validators.js +136 -0
- package/dist/custom-claims/validators.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/admin-permissions.d.ts +147 -0
- package/dist/lib/admin-permissions.d.ts.map +1 -0
- package/dist/lib/admin-permissions.js +261 -0
- package/dist/lib/admin-permissions.js.map +1 -0
- package/dist/lib/audit-log.d.ts +45 -0
- package/dist/lib/audit-log.d.ts.map +1 -0
- package/dist/lib/audit-log.js +79 -0
- package/dist/lib/audit-log.js.map +1 -0
- package/dist/lib/branding-assets.d.ts +47 -0
- package/dist/lib/branding-assets.d.ts.map +1 -0
- package/dist/lib/branding-assets.js +93 -0
- package/dist/lib/branding-assets.js.map +1 -0
- package/dist/lib/bulk-operations.d.ts +54 -0
- package/dist/lib/bulk-operations.d.ts.map +1 -0
- package/dist/lib/bulk-operations.js +201 -0
- package/dist/lib/bulk-operations.js.map +1 -0
- package/dist/lib/cursor.d.ts +110 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +115 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/data-export.d.ts +67 -0
- package/dist/lib/data-export.d.ts.map +1 -0
- package/dist/lib/data-export.js +272 -0
- package/dist/lib/data-export.js.map +1 -0
- package/dist/lib/data-import-plan.d.ts +50 -0
- package/dist/lib/data-import-plan.d.ts.map +1 -0
- package/dist/lib/data-import-plan.js +235 -0
- package/dist/lib/data-import-plan.js.map +1 -0
- package/dist/lib/data-import.d.ts +276 -0
- package/dist/lib/data-import.d.ts.map +1 -0
- package/dist/lib/data-import.js +1694 -0
- package/dist/lib/data-import.js.map +1 -0
- package/dist/lib/database.d.ts +22 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +103 -0
- package/dist/lib/database.js.map +1 -0
- package/dist/lib/entity-history.d.ts +52 -0
- package/dist/lib/entity-history.d.ts.map +1 -0
- package/dist/lib/entity-history.js +121 -0
- package/dist/lib/entity-history.js.map +1 -0
- package/dist/lib/etag.d.ts +92 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +155 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/image-validator.d.ts +44 -0
- package/dist/lib/image-validator.d.ts.map +1 -0
- package/dist/lib/image-validator.js +204 -0
- package/dist/lib/image-validator.js.map +1 -0
- package/dist/lib/logger.d.ts +15 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +84 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/migrator.d.ts +14 -0
- package/dist/lib/migrator.d.ts.map +1 -0
- package/dist/lib/migrator.js +34 -0
- package/dist/lib/migrator.js.map +1 -0
- package/dist/lib/redis.d.ts +5 -0
- package/dist/lib/redis.d.ts.map +1 -0
- package/dist/lib/redis.js +29 -0
- package/dist/lib/redis.js.map +1 -0
- package/dist/lib/runtime-paths.d.ts +38 -0
- package/dist/lib/runtime-paths.d.ts.map +1 -0
- package/dist/lib/runtime-paths.js +50 -0
- package/dist/lib/runtime-paths.js.map +1 -0
- package/dist/lib/session-tracking.d.ts +81 -0
- package/dist/lib/session-tracking.d.ts.map +1 -0
- package/dist/lib/session-tracking.js +145 -0
- package/dist/lib/session-tracking.js.map +1 -0
- package/dist/lib/signing-key-crypto.d.ts +53 -0
- package/dist/lib/signing-key-crypto.d.ts.map +1 -0
- package/dist/lib/signing-key-crypto.js +108 -0
- package/dist/lib/signing-key-crypto.js.map +1 -0
- package/dist/lib/signing-keys.d.ts +141 -0
- package/dist/lib/signing-keys.d.ts.map +1 -0
- package/dist/lib/signing-keys.js +206 -0
- package/dist/lib/signing-keys.js.map +1 -0
- package/dist/lib/stats.d.ts +83 -0
- package/dist/lib/stats.d.ts.map +1 -0
- package/dist/lib/stats.js +187 -0
- package/dist/lib/stats.js.map +1 -0
- package/dist/lib/super-admin-protection.d.ts +69 -0
- package/dist/lib/super-admin-protection.d.ts.map +1 -0
- package/dist/lib/super-admin-protection.js +102 -0
- package/dist/lib/super-admin-protection.js.map +1 -0
- package/dist/lib/system-config.d.ts +79 -0
- package/dist/lib/system-config.d.ts.map +1 -0
- package/dist/lib/system-config.js +159 -0
- package/dist/lib/system-config.js.map +1 -0
- package/dist/middleware/admin-auth.d.ts +100 -0
- package/dist/middleware/admin-auth.d.ts.map +1 -0
- package/dist/middleware/admin-auth.js +241 -0
- package/dist/middleware/admin-auth.js.map +1 -0
- package/dist/middleware/admin-cors.d.ts +41 -0
- package/dist/middleware/admin-cors.d.ts.map +1 -0
- package/dist/middleware/admin-cors.js +83 -0
- package/dist/middleware/admin-cors.js.map +1 -0
- package/dist/middleware/admin-mutation-audit.d.ts +9 -0
- package/dist/middleware/admin-mutation-audit.d.ts.map +1 -0
- package/dist/middleware/admin-mutation-audit.js +76 -0
- package/dist/middleware/admin-mutation-audit.js.map +1 -0
- package/dist/middleware/admin-rate-limiter.d.ts +56 -0
- package/dist/middleware/admin-rate-limiter.d.ts.map +1 -0
- package/dist/middleware/admin-rate-limiter.js +97 -0
- package/dist/middleware/admin-rate-limiter.js.map +1 -0
- package/dist/middleware/client-secret-hash.d.ts +34 -0
- package/dist/middleware/client-secret-hash.d.ts.map +1 -0
- package/dist/middleware/client-secret-hash.js +74 -0
- package/dist/middleware/client-secret-hash.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +3 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +93 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/health.d.ts +3 -0
- package/dist/middleware/health.d.ts.map +1 -0
- package/dist/middleware/health.js +35 -0
- package/dist/middleware/health.js.map +1 -0
- package/dist/middleware/metrics.d.ts +40 -0
- package/dist/middleware/metrics.d.ts.map +1 -0
- package/dist/middleware/metrics.js +75 -0
- package/dist/middleware/metrics.js.map +1 -0
- package/dist/middleware/oidc-client-tenant.d.ts +37 -0
- package/dist/middleware/oidc-client-tenant.d.ts.map +1 -0
- package/dist/middleware/oidc-client-tenant.js +156 -0
- package/dist/middleware/oidc-client-tenant.js.map +1 -0
- package/dist/middleware/oidc-cors.d.ts +33 -0
- package/dist/middleware/oidc-cors.d.ts.map +1 -0
- package/dist/middleware/oidc-cors.js +68 -0
- package/dist/middleware/oidc-cors.js.map +1 -0
- package/dist/middleware/oidc-preflight-cors.d.ts +54 -0
- package/dist/middleware/oidc-preflight-cors.d.ts.map +1 -0
- package/dist/middleware/oidc-preflight-cors.js +267 -0
- package/dist/middleware/oidc-preflight-cors.js.map +1 -0
- package/dist/middleware/prompt-login-reset.d.ts +103 -0
- package/dist/middleware/prompt-login-reset.d.ts.map +1 -0
- package/dist/middleware/prompt-login-reset.js +204 -0
- package/dist/middleware/prompt-login-reset.js.map +1 -0
- package/dist/middleware/ready.d.ts +23 -0
- package/dist/middleware/ready.d.ts.map +1 -0
- package/dist/middleware/ready.js +60 -0
- package/dist/middleware/ready.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +27 -0
- package/dist/middleware/request-logger.d.ts.map +1 -0
- package/dist/middleware/request-logger.js +46 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/require-permission.d.ts +28 -0
- package/dist/middleware/require-permission.d.ts.map +1 -0
- package/dist/middleware/require-permission.js +63 -0
- package/dist/middleware/require-permission.js.map +1 -0
- package/dist/middleware/require-user-organization.d.ts +18 -0
- package/dist/middleware/require-user-organization.d.ts.map +1 -0
- package/dist/middleware/require-user-organization.js +53 -0
- package/dist/middleware/require-user-organization.js.map +1 -0
- package/dist/middleware/root-page.d.ts +62 -0
- package/dist/middleware/root-page.d.ts.map +1 -0
- package/dist/middleware/root-page.js +134 -0
- package/dist/middleware/root-page.js.map +1 -0
- package/dist/middleware/security-headers.d.ts +98 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +159 -0
- package/dist/middleware/security-headers.js.map +1 -0
- package/dist/middleware/tenant-resolver.d.ts +37 -0
- package/dist/middleware/tenant-resolver.d.ts.map +1 -0
- package/dist/middleware/tenant-resolver.js +71 -0
- package/dist/middleware/tenant-resolver.js.map +1 -0
- package/dist/middleware/token-rate-limiter.d.ts +89 -0
- package/dist/middleware/token-rate-limiter.d.ts.map +1 -0
- package/dist/middleware/token-rate-limiter.js +189 -0
- package/dist/middleware/token-rate-limiter.js.map +1 -0
- package/dist/oidc/account-finder.d.ts +52 -0
- package/dist/oidc/account-finder.d.ts.map +1 -0
- package/dist/oidc/account-finder.js +134 -0
- package/dist/oidc/account-finder.js.map +1 -0
- package/dist/oidc/adapter-factory.d.ts +90 -0
- package/dist/oidc/adapter-factory.d.ts.map +1 -0
- package/dist/oidc/adapter-factory.js +165 -0
- package/dist/oidc/adapter-factory.js.map +1 -0
- package/dist/oidc/configuration.d.ts +155 -0
- package/dist/oidc/configuration.d.ts.map +1 -0
- package/dist/oidc/configuration.js +531 -0
- package/dist/oidc/configuration.js.map +1 -0
- package/dist/oidc/postgres-adapter.d.ts +159 -0
- package/dist/oidc/postgres-adapter.d.ts.map +1 -0
- package/dist/oidc/postgres-adapter.js +222 -0
- package/dist/oidc/postgres-adapter.js.map +1 -0
- package/dist/oidc/protocol-security-observer.d.ts +38 -0
- package/dist/oidc/protocol-security-observer.d.ts.map +1 -0
- package/dist/oidc/protocol-security-observer.js +93 -0
- package/dist/oidc/protocol-security-observer.js.map +1 -0
- package/dist/oidc/provider.d.ts +37 -0
- package/dist/oidc/provider.d.ts.map +1 -0
- package/dist/oidc/provider.js +71 -0
- package/dist/oidc/provider.js.map +1 -0
- package/dist/oidc/redis-adapter.d.ts +140 -0
- package/dist/oidc/redis-adapter.d.ts.map +1 -0
- package/dist/oidc/redis-adapter.js +344 -0
- package/dist/oidc/redis-adapter.js.map +1 -0
- package/dist/organizations/cache.d.ts +60 -0
- package/dist/organizations/cache.d.ts.map +1 -0
- package/dist/organizations/cache.js +157 -0
- package/dist/organizations/cache.js.map +1 -0
- package/dist/organizations/errors.d.ts +25 -0
- package/dist/organizations/errors.d.ts.map +1 -0
- package/dist/organizations/errors.js +31 -0
- package/dist/organizations/errors.js.map +1 -0
- package/dist/organizations/index.d.ts +15 -0
- package/dist/organizations/index.d.ts.map +1 -0
- package/dist/organizations/index.js +15 -0
- package/dist/organizations/index.js.map +1 -0
- package/dist/organizations/repository.d.ts +183 -0
- package/dist/organizations/repository.d.ts.map +1 -0
- package/dist/organizations/repository.js +354 -0
- package/dist/organizations/repository.js.map +1 -0
- package/dist/organizations/service.d.ts +168 -0
- package/dist/organizations/service.d.ts.map +1 -0
- package/dist/organizations/service.js +511 -0
- package/dist/organizations/service.js.map +1 -0
- package/dist/organizations/slugs.d.ts +57 -0
- package/dist/organizations/slugs.d.ts.map +1 -0
- package/dist/organizations/slugs.js +130 -0
- package/dist/organizations/slugs.js.map +1 -0
- package/dist/organizations/types.d.ts +146 -0
- package/dist/organizations/types.d.ts.map +1 -0
- package/dist/organizations/types.js +49 -0
- package/dist/organizations/types.js.map +1 -0
- package/dist/rbac/cache.d.ts +110 -0
- package/dist/rbac/cache.d.ts.map +1 -0
- package/dist/rbac/cache.js +271 -0
- package/dist/rbac/cache.js.map +1 -0
- package/dist/rbac/errors.d.ts +34 -0
- package/dist/rbac/errors.d.ts.map +1 -0
- package/dist/rbac/errors.js +43 -0
- package/dist/rbac/errors.js.map +1 -0
- package/dist/rbac/index.d.ts +17 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +21 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/rbac/mapping-repository.d.ts +121 -0
- package/dist/rbac/mapping-repository.d.ts.map +1 -0
- package/dist/rbac/mapping-repository.js +224 -0
- package/dist/rbac/mapping-repository.js.map +1 -0
- package/dist/rbac/permission-repository.d.ts +100 -0
- package/dist/rbac/permission-repository.d.ts.map +1 -0
- package/dist/rbac/permission-repository.js +190 -0
- package/dist/rbac/permission-repository.js.map +1 -0
- package/dist/rbac/permission-service.d.ts +85 -0
- package/dist/rbac/permission-service.d.ts.map +1 -0
- package/dist/rbac/permission-service.js +179 -0
- package/dist/rbac/permission-service.js.map +1 -0
- package/dist/rbac/role-repository.d.ts +104 -0
- package/dist/rbac/role-repository.d.ts.map +1 -0
- package/dist/rbac/role-repository.js +202 -0
- package/dist/rbac/role-repository.js.map +1 -0
- package/dist/rbac/role-service.d.ts +111 -0
- package/dist/rbac/role-service.d.ts.map +1 -0
- package/dist/rbac/role-service.js +256 -0
- package/dist/rbac/role-service.js.map +1 -0
- package/dist/rbac/slugs.d.ts +116 -0
- package/dist/rbac/slugs.d.ts.map +1 -0
- package/dist/rbac/slugs.js +178 -0
- package/dist/rbac/slugs.js.map +1 -0
- package/dist/rbac/types.d.ts +187 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +75 -0
- package/dist/rbac/types.js.map +1 -0
- package/dist/rbac/user-role-service.d.ts +101 -0
- package/dist/rbac/user-role-service.d.ts.map +1 -0
- package/dist/rbac/user-role-service.js +166 -0
- package/dist/rbac/user-role-service.js.map +1 -0
- package/dist/routes/applications.d.ts +40 -0
- package/dist/routes/applications.d.ts.map +1 -0
- package/dist/routes/applications.js +284 -0
- package/dist/routes/applications.js.map +1 -0
- package/dist/routes/audit.d.ts +31 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +140 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/branding.d.ts +17 -0
- package/dist/routes/branding.d.ts.map +1 -0
- package/dist/routes/branding.js +104 -0
- package/dist/routes/branding.js.map +1 -0
- package/dist/routes/bulk.d.ts +37 -0
- package/dist/routes/bulk.d.ts.map +1 -0
- package/dist/routes/bulk.js +94 -0
- package/dist/routes/bulk.js.map +1 -0
- package/dist/routes/clients.d.ts +44 -0
- package/dist/routes/clients.d.ts.map +1 -0
- package/dist/routes/clients.js +353 -0
- package/dist/routes/clients.js.map +1 -0
- package/dist/routes/config.d.ts +35 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +115 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/custom-claims.d.ts +38 -0
- package/dist/routes/custom-claims.d.ts.map +1 -0
- package/dist/routes/custom-claims.js +208 -0
- package/dist/routes/custom-claims.js.map +1 -0
- package/dist/routes/exports.d.ts +21 -0
- package/dist/routes/exports.d.ts.map +1 -0
- package/dist/routes/exports.js +98 -0
- package/dist/routes/exports.js.map +1 -0
- package/dist/routes/imports.d.ts +18 -0
- package/dist/routes/imports.d.ts.map +1 -0
- package/dist/routes/imports.js +72 -0
- package/dist/routes/imports.js.map +1 -0
- package/dist/routes/interactions.d.ts +111 -0
- package/dist/routes/interactions.d.ts.map +1 -0
- package/dist/routes/interactions.js +1074 -0
- package/dist/routes/interactions.js.map +1 -0
- package/dist/routes/invitation.d.ts +31 -0
- package/dist/routes/invitation.d.ts.map +1 -0
- package/dist/routes/invitation.js +342 -0
- package/dist/routes/invitation.js.map +1 -0
- package/dist/routes/keys.d.ts +28 -0
- package/dist/routes/keys.d.ts.map +1 -0
- package/dist/routes/keys.js +91 -0
- package/dist/routes/keys.js.map +1 -0
- package/dist/routes/magic-link.d.ts +43 -0
- package/dist/routes/magic-link.d.ts.map +1 -0
- package/dist/routes/magic-link.js +264 -0
- package/dist/routes/magic-link.js.map +1 -0
- package/dist/routes/organizations.d.ts +38 -0
- package/dist/routes/organizations.d.ts.map +1 -0
- package/dist/routes/organizations.js +347 -0
- package/dist/routes/organizations.js.map +1 -0
- package/dist/routes/password-reset.d.ts +65 -0
- package/dist/routes/password-reset.d.ts.map +1 -0
- package/dist/routes/password-reset.js +493 -0
- package/dist/routes/password-reset.js.map +1 -0
- package/dist/routes/permissions.d.ts +33 -0
- package/dist/routes/permissions.d.ts.map +1 -0
- package/dist/routes/permissions.js +166 -0
- package/dist/routes/permissions.js.map +1 -0
- package/dist/routes/roles.d.ts +37 -0
- package/dist/routes/roles.d.ts.map +1 -0
- package/dist/routes/roles.js +222 -0
- package/dist/routes/roles.js.map +1 -0
- package/dist/routes/sessions.d.ts +37 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +121 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/stats.d.ts +28 -0
- package/dist/routes/stats.d.ts.map +1 -0
- package/dist/routes/stats.js +65 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/two-factor-admin.d.ts +54 -0
- package/dist/routes/two-factor-admin.d.ts.map +1 -0
- package/dist/routes/two-factor-admin.js +344 -0
- package/dist/routes/two-factor-admin.js.map +1 -0
- package/dist/routes/two-factor.d.ts +31 -0
- package/dist/routes/two-factor.d.ts.map +1 -0
- package/dist/routes/two-factor.js +595 -0
- package/dist/routes/two-factor.js.map +1 -0
- package/dist/routes/user-roles.d.ts +33 -0
- package/dist/routes/user-roles.d.ts.map +1 -0
- package/dist/routes/user-roles.js +129 -0
- package/dist/routes/user-roles.js.map +1 -0
- package/dist/routes/users.d.ts +61 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +868 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/security/decision-context.d.ts +52 -0
- package/dist/security/decision-context.d.ts.map +1 -0
- package/dist/security/decision-context.js +150 -0
- package/dist/security/decision-context.js.map +1 -0
- package/dist/security/decision-event.d.ts +128 -0
- package/dist/security/decision-event.d.ts.map +1 -0
- package/dist/security/decision-event.js +225 -0
- package/dist/security/decision-event.js.map +1 -0
- package/dist/security/transport-decision.d.ts +6 -0
- package/dist/security/transport-decision.d.ts.map +1 -0
- package/dist/security/transport-decision.js +37 -0
- package/dist/security/transport-decision.js.map +1 -0
- package/dist/server.d.ts +50 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +491 -0
- package/dist/server.js.map +1 -0
- package/dist/two-factor/cache.d.ts +47 -0
- package/dist/two-factor/cache.d.ts.map +1 -0
- package/dist/two-factor/cache.js +122 -0
- package/dist/two-factor/cache.js.map +1 -0
- package/dist/two-factor/crypto.d.ts +41 -0
- package/dist/two-factor/crypto.d.ts.map +1 -0
- package/dist/two-factor/crypto.js +94 -0
- package/dist/two-factor/crypto.js.map +1 -0
- package/dist/two-factor/errors.d.ts +82 -0
- package/dist/two-factor/errors.d.ts.map +1 -0
- package/dist/two-factor/errors.js +112 -0
- package/dist/two-factor/errors.js.map +1 -0
- package/dist/two-factor/index.d.ts +18 -0
- package/dist/two-factor/index.d.ts.map +1 -0
- package/dist/two-factor/index.js +25 -0
- package/dist/two-factor/index.js.map +1 -0
- package/dist/two-factor/otp.d.ts +43 -0
- package/dist/two-factor/otp.d.ts.map +1 -0
- package/dist/two-factor/otp.js +57 -0
- package/dist/two-factor/otp.js.map +1 -0
- package/dist/two-factor/recovery.d.ts +45 -0
- package/dist/two-factor/recovery.d.ts.map +1 -0
- package/dist/two-factor/recovery.js +104 -0
- package/dist/two-factor/recovery.js.map +1 -0
- package/dist/two-factor/repository.d.ts +156 -0
- package/dist/two-factor/repository.d.ts.map +1 -0
- package/dist/two-factor/repository.js +251 -0
- package/dist/two-factor/repository.js.map +1 -0
- package/dist/two-factor/service.d.ts +232 -0
- package/dist/two-factor/service.d.ts.map +1 -0
- package/dist/two-factor/service.js +599 -0
- package/dist/two-factor/service.js.map +1 -0
- package/dist/two-factor/totp.d.ts +57 -0
- package/dist/two-factor/totp.d.ts.map +1 -0
- package/dist/two-factor/totp.js +96 -0
- package/dist/two-factor/totp.js.map +1 -0
- package/dist/two-factor/types.d.ts +152 -0
- package/dist/two-factor/types.d.ts.map +1 -0
- package/dist/two-factor/types.js +64 -0
- package/dist/two-factor/types.js.map +1 -0
- package/dist/users/cache.d.ts +49 -0
- package/dist/users/cache.d.ts.map +1 -0
- package/dist/users/cache.js +140 -0
- package/dist/users/cache.js.map +1 -0
- package/dist/users/claims.d.ts +67 -0
- package/dist/users/claims.d.ts.map +1 -0
- package/dist/users/claims.js +104 -0
- package/dist/users/claims.js.map +1 -0
- package/dist/users/errors.d.ts +25 -0
- package/dist/users/errors.d.ts.map +1 -0
- package/dist/users/errors.js +31 -0
- package/dist/users/errors.js.map +1 -0
- package/dist/users/gdpr.d.ts +110 -0
- package/dist/users/gdpr.d.ts.map +1 -0
- package/dist/users/gdpr.js +211 -0
- package/dist/users/gdpr.js.map +1 -0
- package/dist/users/index.d.ts +20 -0
- package/dist/users/index.d.ts.map +1 -0
- package/dist/users/index.js +19 -0
- package/dist/users/index.js.map +1 -0
- package/dist/users/password.d.ts +85 -0
- package/dist/users/password.d.ts.map +1 -0
- package/dist/users/password.js +138 -0
- package/dist/users/password.js.map +1 -0
- package/dist/users/repository.d.ts +263 -0
- package/dist/users/repository.d.ts.map +1 -0
- package/dist/users/repository.js +499 -0
- package/dist/users/repository.js.map +1 -0
- package/dist/users/service.d.ts +299 -0
- package/dist/users/service.d.ts.map +1 -0
- package/dist/users/service.js +698 -0
- package/dist/users/service.js.map +1 -0
- package/dist/users/types.d.ts +220 -0
- package/dist/users/types.d.ts.map +1 -0
- package/dist/users/types.js +77 -0
- package/dist/users/types.js.map +1 -0
- package/locales/default/en/common.json +10 -0
- package/locales/default/en/consent.json +13 -0
- package/locales/default/en/emails.json +17 -0
- package/locales/default/en/errors.json +21 -0
- package/locales/default/en/forgot-password.json +11 -0
- package/locales/default/en/invitation.json +18 -0
- package/locales/default/en/login.json +17 -0
- package/locales/default/en/logout.json +9 -0
- package/locales/default/en/magic-link.json +12 -0
- package/locales/default/en/reset-password.json +16 -0
- package/locales/default/en/two-factor.json +29 -0
- package/migrations/.gitkeep +0 -0
- package/migrations/001_extensions.sql +22 -0
- package/migrations/002_organizations.sql +43 -0
- package/migrations/003_applications.sql +50 -0
- package/migrations/004_clients.sql +74 -0
- package/migrations/005_users.sql +119 -0
- package/migrations/006_roles_permissions.sql +80 -0
- package/migrations/007_custom_claims.sql +55 -0
- package/migrations/008_config.sql +51 -0
- package/migrations/009_audit_log.sql +31 -0
- package/migrations/010_oidc_adapter.sql +34 -0
- package/migrations/011_seed.sql +53 -0
- package/migrations/012_two_factor.sql +88 -0
- package/migrations/013_client_secret_sha256.sql +22 -0
- package/migrations/014_login_methods.sql +34 -0
- package/migrations/015_signing_key_encryption.sql +20 -0
- package/migrations/016_failed_login_count.sql +27 -0
- package/migrations/017_audit_retention.sql +11 -0
- package/migrations/018_admin_api_enhancements.sql +78 -0
- package/migrations/019_invitation_details.sql +27 -0
- package/migrations/020_two_factor_admin_permission.sql +108 -0
- package/migrations/021_auth_recovery_jobs.sql +74 -0
- package/migrations/022_recovery_artifact_ownership.sql +25 -0
- package/migrations/023_magic_link_authority.sql +39 -0
- package/package.json +106 -0
- package/templates/default/emails/invitation.hbs +39 -0
- package/templates/default/emails/invitation.txt.hbs +18 -0
- package/templates/default/emails/magic-link.hbs +30 -0
- package/templates/default/emails/magic-link.txt.hbs +11 -0
- package/templates/default/emails/otp-code.hbs +31 -0
- package/templates/default/emails/otp-code.txt.hbs +15 -0
- package/templates/default/emails/password-changed.hbs +24 -0
- package/templates/default/emails/password-changed.txt.hbs +7 -0
- package/templates/default/emails/password-reset.hbs +30 -0
- package/templates/default/emails/password-reset.txt.hbs +11 -0
- package/templates/default/emails/welcome.hbs +25 -0
- package/templates/default/emails/welcome.txt.hbs +9 -0
- package/templates/default/layouts/main.hbs +49 -0
- package/templates/default/pages/accept-invite.hbs +25 -0
- package/templates/default/pages/consent.hbs +27 -0
- package/templates/default/pages/error.hbs +15 -0
- package/templates/default/pages/forgot-password.hbs +21 -0
- package/templates/default/pages/invite-expired.hbs +6 -0
- package/templates/default/pages/invite-success.hbs +9 -0
- package/templates/default/pages/login.hbs +108 -0
- package/templates/default/pages/logout-success.hbs +5 -0
- package/templates/default/pages/logout.hbs +24 -0
- package/templates/default/pages/magic-link-sent.hbs +10 -0
- package/templates/default/pages/magic-link-success.hbs +17 -0
- package/templates/default/pages/reset-password.hbs +20 -0
- package/templates/default/pages/reset-success.hbs +9 -0
- package/templates/default/pages/two-factor-setup.hbs +65 -0
- package/templates/default/pages/two-factor-verify.hbs +77 -0
- package/templates/default/partials/flash-messages.hbs +10 -0
- package/templates/default/partials/footer.hbs +4 -0
- package/templates/default/partials/header.hbs +8 -0
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction route handlers for the OIDC login, consent, and logout flows.
|
|
3
|
+
*
|
|
4
|
+
* These routes handle the `/interaction/:uid` path where the UID is a
|
|
5
|
+
* node-oidc-provider interaction identifier. The provider redirects users
|
|
6
|
+
* here during the OIDC authorization flow for login and consent prompts.
|
|
7
|
+
*
|
|
8
|
+
* Route structure:
|
|
9
|
+
* GET /interaction/:uid → showLogin or showConsent (based on prompt)
|
|
10
|
+
* POST /interaction/:uid/login → processLogin (password auth)
|
|
11
|
+
* POST /interaction/:uid/magic-link → sendMagicLink (passwordless auth)
|
|
12
|
+
* GET /interaction/:uid/consent → showConsent
|
|
13
|
+
* POST /interaction/:uid/confirm → processConsent (approve/deny)
|
|
14
|
+
* GET /interaction/:uid/abort → abortInteraction
|
|
15
|
+
*
|
|
16
|
+
* Security features:
|
|
17
|
+
* - CSRF protection on all POST endpoints
|
|
18
|
+
* - Rate limiting on login and magic link endpoints
|
|
19
|
+
* - User enumeration prevention (generic error messages)
|
|
20
|
+
* - Audit logging for all auth events
|
|
21
|
+
*/
|
|
22
|
+
import Router from '@koa/router';
|
|
23
|
+
import { generateCsrfToken, verifyCsrfToken, setCsrfCookie, getCsrfFromCookie, } from '../auth/csrf.js';
|
|
24
|
+
import { checkRateLimit, resetRateLimit, buildLoginRateLimitKey, buildMagicLinkRateLimitKey, loadLoginRateLimitConfig, loadMagicLinkRateLimitConfig, } from '../auth/rate-limiter.js';
|
|
25
|
+
import { sendOtpCodeEmail } from '../auth/email-service.js';
|
|
26
|
+
import { enqueueAccountRecovery } from '../auth/recovery-service.js';
|
|
27
|
+
import { resolveLocale, getTranslationFunction } from '../auth/i18n.js';
|
|
28
|
+
import { renderPage } from '../auth/template-engine.js';
|
|
29
|
+
import { prepareUserForPasswordLogin, verifyLoginPassword, recordLogin, recordPasswordFailure, } from '../users/service.js';
|
|
30
|
+
import { requiresTwoFactor, determineTwoFactorMethod, sendOtpCode } from '../two-factor/service.js';
|
|
31
|
+
import { writeAuditLog } from '../lib/audit-log.js';
|
|
32
|
+
import { logger } from '../lib/logger.js';
|
|
33
|
+
import { getClientByClientId } from '../clients/service.js';
|
|
34
|
+
import { getOrganizationById } from '../organizations/service.js';
|
|
35
|
+
import { getRedis } from '../lib/redis.js';
|
|
36
|
+
import { hasMagicLinkSession, consumeMagicLinkSession } from '../auth/magic-link-session.js';
|
|
37
|
+
import { resolveLoginMethods } from '../clients/resolve-login-methods.js';
|
|
38
|
+
import { purgeExpired } from '../oidc/postgres-adapter.js';
|
|
39
|
+
import { observeProtocolSecurityRejection } from '../oidc/protocol-security-observer.js';
|
|
40
|
+
const defaultEnumerationDependencies = {
|
|
41
|
+
getCsrfFromCookie,
|
|
42
|
+
verifyCsrfToken,
|
|
43
|
+
generateCsrfToken,
|
|
44
|
+
setCsrfCookie,
|
|
45
|
+
buildLoginRateLimitKey,
|
|
46
|
+
buildMagicLinkRateLimitKey,
|
|
47
|
+
loadLoginRateLimitConfig,
|
|
48
|
+
loadMagicLinkRateLimitConfig,
|
|
49
|
+
checkRateLimit,
|
|
50
|
+
resetRateLimit,
|
|
51
|
+
resolveLocale,
|
|
52
|
+
getTranslationFunction,
|
|
53
|
+
renderPage,
|
|
54
|
+
prepareUserForPasswordLogin,
|
|
55
|
+
verifyLoginPassword,
|
|
56
|
+
recordPasswordFailure,
|
|
57
|
+
recordLogin,
|
|
58
|
+
enqueueAccountRecovery,
|
|
59
|
+
writeAuditLog,
|
|
60
|
+
};
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/**
|
|
65
|
+
* Build a branding context object from organization data.
|
|
66
|
+
* Used to populate template context with org-specific branding.
|
|
67
|
+
*
|
|
68
|
+
* @param org - Organization with branding fields
|
|
69
|
+
* @returns Branding context for templates
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the effective login methods for an OIDC provider Client.
|
|
73
|
+
*
|
|
74
|
+
* Reads the raw `urn:porta:login_methods` metadata (set by {@link findForOidc}
|
|
75
|
+
* on the clients service) and applies the inheritance rule via
|
|
76
|
+
* {@link resolveLoginMethods}:
|
|
77
|
+
* - `null` / missing / malformed → inherit `org.defaultLoginMethods`
|
|
78
|
+
* - explicit non-empty array → use it verbatim
|
|
79
|
+
*
|
|
80
|
+
* Malformed metadata (e.g., from a misconfigured adapter) is treated as
|
|
81
|
+
* `null` to match the safe-fallback semantics of the raw resolver — the
|
|
82
|
+
* organization default governs in that case rather than failing closed,
|
|
83
|
+
* which would break all logins if a single client's metadata were corrupt.
|
|
84
|
+
*
|
|
85
|
+
* @param client - OIDC provider Client instance (or null/undefined — safe fallback)
|
|
86
|
+
* @param org - Organization owning the client (read for `defaultLoginMethods`)
|
|
87
|
+
* @returns Resolved `LoginMethod[]` (always non-empty, matches resolver contract)
|
|
88
|
+
*/
|
|
89
|
+
function resolveLoginMethodsFromOidcClient(client, org) {
|
|
90
|
+
// oidc-provider's Client objects expose metadata() → Record<string, unknown>.
|
|
91
|
+
// Missing client or missing metadata function → treat as fully inherited.
|
|
92
|
+
const metadata = client?.metadata?.();
|
|
93
|
+
const raw = metadata?.['urn:porta:login_methods'];
|
|
94
|
+
// Defensive: accept only null or string[]; treat anything else as null.
|
|
95
|
+
const normalized = Array.isArray(raw) ? raw : null;
|
|
96
|
+
return resolveLoginMethods(org, { loginMethods: normalized });
|
|
97
|
+
}
|
|
98
|
+
function buildBrandingFromOrg(org) {
|
|
99
|
+
return {
|
|
100
|
+
logoUrl: org.brandingLogoUrl,
|
|
101
|
+
faviconUrl: org.brandingFaviconUrl,
|
|
102
|
+
primaryColor: org.brandingPrimaryColor ?? '#3B82F6',
|
|
103
|
+
companyName: org.brandingCompanyName ?? org.name,
|
|
104
|
+
customCss: org.brandingCustomCss,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build a base template context from an interaction context.
|
|
109
|
+
* Shared across all interaction route handlers to provide consistent
|
|
110
|
+
* branding, locale, and CSRF state.
|
|
111
|
+
*
|
|
112
|
+
* @param ctx - Koa context with organization state
|
|
113
|
+
* @param locale - Resolved locale string
|
|
114
|
+
* @param csrfToken - CSRF token for form protection
|
|
115
|
+
* @param orgSlug - Organization slug for template resolution
|
|
116
|
+
* @returns Base template context (without the `t` translation function)
|
|
117
|
+
*/
|
|
118
|
+
function buildBaseContext(ctx, locale, csrfToken, orgSlug) {
|
|
119
|
+
return {
|
|
120
|
+
branding: buildBrandingFromOrg(ctx.state.organization),
|
|
121
|
+
locale,
|
|
122
|
+
csrfToken,
|
|
123
|
+
orgSlug,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Render an HTML page and send it as the response.
|
|
128
|
+
* Sets Content-Type to text/html and assigns the rendered body to ctx.body.
|
|
129
|
+
*
|
|
130
|
+
* @param ctx - Koa context
|
|
131
|
+
* @param pageName - Page template name
|
|
132
|
+
* @param context - Full template context
|
|
133
|
+
* @param statusCode - HTTP status code (default: 200)
|
|
134
|
+
*/
|
|
135
|
+
async function renderAndRespond(ctx, pageName, context, statusCode = 200, renderer = renderPage) {
|
|
136
|
+
const html = await renderer(pageName, context);
|
|
137
|
+
ctx.status = statusCode;
|
|
138
|
+
ctx.type = 'text/html';
|
|
139
|
+
ctx.body = html;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the organization from the OIDC interaction's client_id.
|
|
143
|
+
*
|
|
144
|
+
* Interaction routes (`/interaction/:uid`) don't go through the tenant
|
|
145
|
+
* resolver middleware, so `ctx.state.organization` is not set automatically.
|
|
146
|
+
* This helper resolves the org by looking up the client → organization chain
|
|
147
|
+
* from the interaction's `client_id` parameter, and sets `ctx.state.organization`.
|
|
148
|
+
*
|
|
149
|
+
* Also used by two-factor routes which share the same `/interaction` prefix
|
|
150
|
+
* and likewise don't go through the tenant resolver middleware.
|
|
151
|
+
*
|
|
152
|
+
* @param ctx - Koa context to populate with organization
|
|
153
|
+
* @param clientId - OIDC client_id from the interaction params
|
|
154
|
+
* @throws Error if client or organization cannot be found
|
|
155
|
+
*/
|
|
156
|
+
export async function resolveOrganizationForInteraction(ctx, clientId, interactionUid) {
|
|
157
|
+
// If already resolved (e.g., by tenant resolver), skip
|
|
158
|
+
if (ctx.state.organization)
|
|
159
|
+
return;
|
|
160
|
+
// Prefer the auth-flow org stored in Redis during interaction creation.
|
|
161
|
+
// OIDC entry middleware has already proved that this organization owns the client.
|
|
162
|
+
if (interactionUid) {
|
|
163
|
+
try {
|
|
164
|
+
const storedOrgId = await getRedis().get(`interaction:org:${interactionUid}`);
|
|
165
|
+
if (storedOrgId) {
|
|
166
|
+
const org = await getOrganizationById(storedOrgId);
|
|
167
|
+
if (org) {
|
|
168
|
+
ctx.state.organization = org;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Redis error — fall through to client-based resolution
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Fallback: resolve from the client's organizationId
|
|
178
|
+
const client = await getClientByClientId(clientId);
|
|
179
|
+
if (!client) {
|
|
180
|
+
throw new Error(`Client not found for interaction: ${clientId}`);
|
|
181
|
+
}
|
|
182
|
+
const org = await getOrganizationById(client.organizationId);
|
|
183
|
+
if (!org) {
|
|
184
|
+
throw new Error(`Organization not found for client: ${client.organizationId}`);
|
|
185
|
+
}
|
|
186
|
+
ctx.state.organization = org;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve continuation authority independently from the magic-link session value.
|
|
190
|
+
*
|
|
191
|
+
* The provider writes this tenant mapping when it creates the interaction. A missing, expired, or
|
|
192
|
+
* malformed mapping cannot authorize consumption and leaves the short-lived session untouched.
|
|
193
|
+
*
|
|
194
|
+
* @param interactionUid - Exact interaction selected by the public route.
|
|
195
|
+
* @returns Active interaction authority, or null when it cannot be proven.
|
|
196
|
+
*/
|
|
197
|
+
async function resolveMagicLinkContinuationAuthority(interactionUid) {
|
|
198
|
+
if (interactionUid.length < 1 || interactionUid.length > 128)
|
|
199
|
+
return null;
|
|
200
|
+
const organizationId = await getRedis().get(`interaction:org:${interactionUid}`);
|
|
201
|
+
if (!organizationId)
|
|
202
|
+
return null;
|
|
203
|
+
const organization = await getOrganizationById(organizationId);
|
|
204
|
+
if (!organization || organization.status !== 'active')
|
|
205
|
+
return null;
|
|
206
|
+
return { authority: { organizationId, interactionUid }, organization };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Derive recovery-job authority from the provider interaction and its persisted client owner.
|
|
210
|
+
*
|
|
211
|
+
* @param routeInteractionUid - Interaction selected by the route.
|
|
212
|
+
* @param interactionUid - Interaction identifier returned by oidc-provider.
|
|
213
|
+
* @param clientIdValue - Client identifier returned by oidc-provider.
|
|
214
|
+
* @param clientOrganizationIdValue - Tenant metadata returned by the admitted provider client.
|
|
215
|
+
* @param organization - Tenant resolved for the interaction.
|
|
216
|
+
* @returns Exact job authority, or null when route, provider, client, and tenant do not agree.
|
|
217
|
+
*/
|
|
218
|
+
function deriveMagicLinkRecoveryAuthority(routeInteractionUid, interactionUid, clientIdValue, clientOrganizationIdValue, organization) {
|
|
219
|
+
if (routeInteractionUid !== interactionUid ||
|
|
220
|
+
interactionUid.length < 1 ||
|
|
221
|
+
interactionUid.length > 128 ||
|
|
222
|
+
typeof clientIdValue !== 'string' ||
|
|
223
|
+
clientIdValue.length < 1 ||
|
|
224
|
+
clientOrganizationIdValue !== organization.id) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return { organizationId: organization.id, interactionUid };
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Router factory
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
/**
|
|
233
|
+
* Create the interaction router with all OIDC interaction handlers.
|
|
234
|
+
*
|
|
235
|
+
* The router handles login, consent, magic link, and abort flows.
|
|
236
|
+
* Requires a Provider instance to interact with the OIDC session state.
|
|
237
|
+
*
|
|
238
|
+
* @param provider - node-oidc-provider instance for interaction management
|
|
239
|
+
* @returns Koa router with interaction routes mounted at /interaction
|
|
240
|
+
*/
|
|
241
|
+
export function createInteractionRouter(provider, enumerationDependencies = defaultEnumerationDependencies) {
|
|
242
|
+
const router = new Router({ prefix: '/interaction' });
|
|
243
|
+
router.use(async (ctx, next) => {
|
|
244
|
+
try {
|
|
245
|
+
await next();
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (isMissingInteraction(error)) {
|
|
249
|
+
observeProtocolSecurityRejection({
|
|
250
|
+
request: ctx.req,
|
|
251
|
+
eventClass: 'interaction-context-rejected',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
// GET /interaction/:uid — Show login or consent page
|
|
259
|
+
// -------------------------------------------------------------------------
|
|
260
|
+
router.get('/:uid', async (ctx) => {
|
|
261
|
+
await showLogin(ctx, provider);
|
|
262
|
+
});
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// POST /interaction/:uid/login — Process password login
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
router.post('/:uid/login', async (ctx) => {
|
|
267
|
+
await processLogin(ctx, provider, enumerationDependencies);
|
|
268
|
+
});
|
|
269
|
+
// -------------------------------------------------------------------------
|
|
270
|
+
// POST /interaction/:uid/magic-link — Send magic link email
|
|
271
|
+
// -------------------------------------------------------------------------
|
|
272
|
+
router.post('/:uid/magic-link', async (ctx) => {
|
|
273
|
+
await handleSendMagicLink(ctx, provider, enumerationDependencies);
|
|
274
|
+
});
|
|
275
|
+
// -------------------------------------------------------------------------
|
|
276
|
+
// GET /interaction/:uid/consent — Show consent page
|
|
277
|
+
// -------------------------------------------------------------------------
|
|
278
|
+
router.get('/:uid/consent', async (ctx) => {
|
|
279
|
+
await showConsent(ctx, provider);
|
|
280
|
+
});
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
// POST /interaction/:uid/confirm — Process consent (approve/deny)
|
|
283
|
+
// -------------------------------------------------------------------------
|
|
284
|
+
router.post('/:uid/confirm', async (ctx) => {
|
|
285
|
+
await processConsent(ctx, provider);
|
|
286
|
+
});
|
|
287
|
+
// -------------------------------------------------------------------------
|
|
288
|
+
// GET /interaction/:uid/abort — Abort the interaction
|
|
289
|
+
// -------------------------------------------------------------------------
|
|
290
|
+
router.get('/:uid/abort', async (ctx) => {
|
|
291
|
+
await abortInteraction(ctx, provider);
|
|
292
|
+
});
|
|
293
|
+
return router;
|
|
294
|
+
}
|
|
295
|
+
/** Identifies the provider's exact expired, absent, or mismatched interaction state error. */
|
|
296
|
+
function isMissingInteraction(error) {
|
|
297
|
+
if (typeof error !== 'object' || error === null || Array.isArray(error))
|
|
298
|
+
return false;
|
|
299
|
+
return Reflect.get(error, 'name') === 'SessionNotFound' && Reflect.get(error, 'status') === 400;
|
|
300
|
+
}
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Route handlers
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
/**
|
|
305
|
+
* Show the login page for an OIDC interaction.
|
|
306
|
+
*
|
|
307
|
+
* Checks for a `_ml_session` cookie first (set by the magic link handler).
|
|
308
|
+
* If present, consumes the session and either:
|
|
309
|
+
* - Completes the OIDC flow via interactionFinished() (same browser)
|
|
310
|
+
* - Shows a "You're signed in" success page (different browser)
|
|
311
|
+
*
|
|
312
|
+
* If no magic link session, falls through to the normal login flow:
|
|
313
|
+
* 1. Loads the interaction details from the provider
|
|
314
|
+
* 2. If the prompt is "consent", redirects to the consent page
|
|
315
|
+
* 3. Resolves the locale from OIDC params, Accept-Language, and org default
|
|
316
|
+
* 4. Generates a CSRF token for form protection
|
|
317
|
+
* 5. Renders the login page with branding, i18n, and interaction data
|
|
318
|
+
*
|
|
319
|
+
* @param ctx - Koa context with organization state
|
|
320
|
+
* @param provider - OIDC provider instance
|
|
321
|
+
*/
|
|
322
|
+
async function showLogin(ctx, provider) {
|
|
323
|
+
// -----------------------------------------------------------------------
|
|
324
|
+
// Opportunistic cleanup: purge expired oidc_payloads records.
|
|
325
|
+
// Fire-and-forget — no await, no error propagation. Keeps the database
|
|
326
|
+
// clean without a cron job. Proven pattern from Porta v4.
|
|
327
|
+
// -----------------------------------------------------------------------
|
|
328
|
+
purgeExpired();
|
|
329
|
+
// -----------------------------------------------------------------------
|
|
330
|
+
// Magic link session detection
|
|
331
|
+
// -----------------------------------------------------------------------
|
|
332
|
+
if (hasMagicLinkSession(ctx)) {
|
|
333
|
+
const continuationAuthority = await resolveMagicLinkContinuationAuthority(ctx.params.uid);
|
|
334
|
+
const session = continuationAuthority
|
|
335
|
+
? await consumeMagicLinkSession(ctx, continuationAuthority.authority)
|
|
336
|
+
: null;
|
|
337
|
+
if (session && continuationAuthority) {
|
|
338
|
+
const org = continuationAuthority.organization;
|
|
339
|
+
// Try to complete the OIDC flow (same browser — interaction cookies present)
|
|
340
|
+
try {
|
|
341
|
+
const result = {
|
|
342
|
+
login: { accountId: session.userId },
|
|
343
|
+
};
|
|
344
|
+
await provider.interactionFinished(ctx.req, ctx.res, result, {
|
|
345
|
+
mergeWithLastSubmission: false,
|
|
346
|
+
});
|
|
347
|
+
return; // Success! Browser redirected to callback with code
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Different browser — interaction cookies not present, or interaction expired.
|
|
351
|
+
// Show the magic link success page instead.
|
|
352
|
+
logger.info({ event: 'magic_link_cross_browser_success' }, 'Magic link interaction cookies unavailable; showing success page');
|
|
353
|
+
await renderMagicLinkSuccessPage(ctx, org);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Invalid/expired session — fall through to normal login
|
|
358
|
+
}
|
|
359
|
+
// -----------------------------------------------------------------------
|
|
360
|
+
// Normal login flow
|
|
361
|
+
// -----------------------------------------------------------------------
|
|
362
|
+
try {
|
|
363
|
+
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
|
|
364
|
+
const { prompt, params } = interaction;
|
|
365
|
+
// Resolve organization from the interaction's client_id.
|
|
366
|
+
// Interaction routes don't go through the tenant resolver middleware,
|
|
367
|
+
// so we resolve the org from the client → organization chain.
|
|
368
|
+
await resolveOrganizationForInteraction(ctx, params.client_id, interaction.uid);
|
|
369
|
+
// If the prompt is consent, handle it directly (no redirect).
|
|
370
|
+
// Redirecting to /consent as a separate request can lose the interaction
|
|
371
|
+
// cookie in some flows (e.g., pre-auth magic link), because the cookie
|
|
372
|
+
// was set by the provider on the redirect chain and a Koa redirect
|
|
373
|
+
// doesn't carry it forward reliably.
|
|
374
|
+
if (prompt.name === 'consent') {
|
|
375
|
+
await showConsent(ctx, provider);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const org = ctx.state.organization;
|
|
379
|
+
// Resolve locale from OIDC ui_locales, Accept-Language, and org default
|
|
380
|
+
const uiLocales = params.ui_locales;
|
|
381
|
+
const acceptLanguage = ctx.get('Accept-Language') || undefined;
|
|
382
|
+
const locale = await resolveLocale(uiLocales, acceptLanguage, org.defaultLocale);
|
|
383
|
+
// Get translation function with org-specific overrides
|
|
384
|
+
const t = getTranslationFunction(locale, org.slug);
|
|
385
|
+
// Generate CSRF token for form protection
|
|
386
|
+
const csrfToken = generateCsrfToken();
|
|
387
|
+
setCsrfCookie(ctx, csrfToken);
|
|
388
|
+
// Pre-fill email from login_hint if provided.
|
|
389
|
+
//
|
|
390
|
+
// login_hint comes from the client-supplied authorization URL — OIDC spec
|
|
391
|
+
// allows any opaque identifier, not just emails, so we do NOT validate
|
|
392
|
+
// format. We DO defensively trim and length-cap at RFC 5321 max (320 chars)
|
|
393
|
+
// to avoid pathological template renders. Handlebars HTML-escapes on
|
|
394
|
+
// interpolation, which blocks XSS via the value="" attribute.
|
|
395
|
+
const rawHint = typeof params.login_hint === 'string' ? params.login_hint.trim().slice(0, 320) : '';
|
|
396
|
+
const emailHint = rawHint.length > 0 ? rawHint : undefined;
|
|
397
|
+
// Resolve human-readable client name from OIDC provider metadata.
|
|
398
|
+
// Falls back to the raw client_id if not found.
|
|
399
|
+
const oidcClient = await provider.Client.find(params.client_id);
|
|
400
|
+
const clientName = oidcClient?.metadata()?.client_name ?? params.client_id;
|
|
401
|
+
// Resolve the effective login methods from the client's OIDC metadata.
|
|
402
|
+
// When no client is found (edge case — client deleted mid-flow), fall back
|
|
403
|
+
// to the org default methods so the user sees SOMETHING actionable instead
|
|
404
|
+
// of a blank page.
|
|
405
|
+
const effectiveMethods = oidcClient
|
|
406
|
+
? resolveLoginMethodsFromOidcClient(oidcClient, org)
|
|
407
|
+
: org.defaultLoginMethods;
|
|
408
|
+
const showPassword = effectiveMethods.includes('password');
|
|
409
|
+
const showMagicLink = effectiveMethods.includes('magic_link');
|
|
410
|
+
const context = {
|
|
411
|
+
...buildBaseContext(ctx, locale, csrfToken, org.slug),
|
|
412
|
+
t,
|
|
413
|
+
interaction: {
|
|
414
|
+
uid: interaction.uid,
|
|
415
|
+
prompt: prompt.name,
|
|
416
|
+
params: params,
|
|
417
|
+
client: { clientName },
|
|
418
|
+
},
|
|
419
|
+
email: emailHint ?? '',
|
|
420
|
+
emailHint,
|
|
421
|
+
// Login method rendering flags — consumed by login.hbs
|
|
422
|
+
showPassword,
|
|
423
|
+
showMagicLink,
|
|
424
|
+
showDivider: showPassword && showMagicLink,
|
|
425
|
+
loginMethods: effectiveMethods,
|
|
426
|
+
};
|
|
427
|
+
// Render the login page with CSRF token in both cookie and hidden field
|
|
428
|
+
await renderAndRespond(ctx, 'login', context);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
logger.error({ event: 'login-page-failed' }, 'Login page rendering failed');
|
|
432
|
+
await renderErrorPage(ctx, 'errors.interaction_expired');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Render the magic link success page — "You're signed in".
|
|
437
|
+
*
|
|
438
|
+
* Shown when a magic link is opened in a different browser than the one
|
|
439
|
+
* that started the OIDC authorization flow. The OIDC flow can't be
|
|
440
|
+
* completed (no interaction cookies), but the user IS authenticated.
|
|
441
|
+
*
|
|
442
|
+
* Security guard: this function is ONLY called when a valid `_ml_session`
|
|
443
|
+
* was present and successfully consumed. The `_ml_session` is single-use
|
|
444
|
+
* (Redis key deleted), so this page cannot be shown twice or by URL crafting.
|
|
445
|
+
*
|
|
446
|
+
* @param ctx - Koa context
|
|
447
|
+
* @param org - Organization for branding
|
|
448
|
+
*/
|
|
449
|
+
async function renderMagicLinkSuccessPage(ctx, org) {
|
|
450
|
+
const locale = await resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale);
|
|
451
|
+
const t = getTranslationFunction(locale, org.slug);
|
|
452
|
+
const csrfToken = generateCsrfToken();
|
|
453
|
+
const context = {
|
|
454
|
+
branding: buildBrandingFromOrg(org),
|
|
455
|
+
locale,
|
|
456
|
+
t,
|
|
457
|
+
csrfToken,
|
|
458
|
+
orgSlug: org.slug,
|
|
459
|
+
};
|
|
460
|
+
await renderAndRespond(ctx, 'magic-link-success', context);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Process a password-based login attempt.
|
|
464
|
+
*
|
|
465
|
+
* 1. Verifies CSRF token from form submission
|
|
466
|
+
* 2. Extracts email + password from the request body
|
|
467
|
+
* 3. Checks rate limits (per IP+email combination)
|
|
468
|
+
* 4. Looks up user by org + email
|
|
469
|
+
* 5. Verifies password and checks user status
|
|
470
|
+
* 6. On success: records login, resets rate limit, finishes interaction
|
|
471
|
+
* 7. On failure: renders login page with generic error (prevents enumeration)
|
|
472
|
+
*
|
|
473
|
+
* @param ctx - Koa context with organization state
|
|
474
|
+
* @param provider - OIDC provider instance
|
|
475
|
+
*/
|
|
476
|
+
async function processLogin(ctx, provider, dependencies) {
|
|
477
|
+
const body = ctx.request.body;
|
|
478
|
+
const email = (body.email ?? '').trim().toLowerCase();
|
|
479
|
+
const password = body.password ?? '';
|
|
480
|
+
const submittedCsrf = body._csrf ?? '';
|
|
481
|
+
const storedCsrf = dependencies.getCsrfFromCookie(ctx) ?? '';
|
|
482
|
+
try {
|
|
483
|
+
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
|
|
484
|
+
// Resolve organization from the interaction's client_id
|
|
485
|
+
await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
|
|
486
|
+
const org = ctx.state.organization;
|
|
487
|
+
// Resolve locale for error messages
|
|
488
|
+
const locale = await dependencies.resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale);
|
|
489
|
+
const t = dependencies.getTranslationFunction(locale, org.slug);
|
|
490
|
+
// Step 1: Verify CSRF token (cookie vs form field)
|
|
491
|
+
if (!dependencies.verifyCsrfToken(storedCsrf, submittedCsrf)) {
|
|
492
|
+
logger.warn({ event: 'login-csrf-rejected' }, 'Login request CSRF validation failed');
|
|
493
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.csrf_invalid'), 200, dependencies);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Step 1.5: Enforce login method — 'password' must be allowed for this client.
|
|
497
|
+
//
|
|
498
|
+
// This check protects against three scenarios:
|
|
499
|
+
// 1. User submits the password form after an admin switched the client
|
|
500
|
+
// to magic-link-only (stale form cached in the browser).
|
|
501
|
+
// 2. Adversary crafts a direct POST to bypass the UI guard (the template
|
|
502
|
+
// hides the password form, but a raw POST from curl/Postman still
|
|
503
|
+
// reaches this handler).
|
|
504
|
+
// 3. Browser back button resurrects the form after login method change.
|
|
505
|
+
//
|
|
506
|
+
// Rendering the login page (not a raw 403) preserves CSRF/state and gives
|
|
507
|
+
// the user a clear, actionable error message.
|
|
508
|
+
const oidcClientForLogin = await provider.Client.find(interaction.params.client_id);
|
|
509
|
+
const effectiveLoginMethods = oidcClientForLogin
|
|
510
|
+
? resolveLoginMethodsFromOidcClient(oidcClientForLogin, org)
|
|
511
|
+
: org.defaultLoginMethods;
|
|
512
|
+
if (!effectiveLoginMethods.includes('password')) {
|
|
513
|
+
logger.warn({ event: 'password-login-method-disabled' }, 'Password login method is disabled');
|
|
514
|
+
dependencies.writeAuditLog({
|
|
515
|
+
organizationId: org.id,
|
|
516
|
+
eventType: 'security.login_method_disabled',
|
|
517
|
+
eventCategory: 'security',
|
|
518
|
+
description: 'Password login attempted where the method is disabled',
|
|
519
|
+
ipAddress: ctx.ip,
|
|
520
|
+
metadata: {
|
|
521
|
+
clientId: interaction.params.client_id,
|
|
522
|
+
attemptedMethod: 'password',
|
|
523
|
+
effectiveMethods: effectiveLoginMethods,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.login_method_disabled'), 403, dependencies);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
// Step 2: Check rate limit
|
|
530
|
+
const rateLimitKey = dependencies.buildLoginRateLimitKey(org.id, ctx.ip, email);
|
|
531
|
+
const rateLimitConfig = await dependencies.loadLoginRateLimitConfig();
|
|
532
|
+
const rateLimitResult = await dependencies.checkRateLimit(rateLimitKey, rateLimitConfig);
|
|
533
|
+
if (!rateLimitResult.allowed) {
|
|
534
|
+
ctx.set('Retry-After', String(rateLimitResult.retryAfter));
|
|
535
|
+
// Audit: rate limit exceeded
|
|
536
|
+
dependencies.writeAuditLog({
|
|
537
|
+
organizationId: org.id,
|
|
538
|
+
eventType: 'rate_limit.login',
|
|
539
|
+
eventCategory: 'security',
|
|
540
|
+
description: 'Login rate limit exceeded',
|
|
541
|
+
ipAddress: ctx.ip,
|
|
542
|
+
});
|
|
543
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.rate_limit_exceeded'), 429, dependencies);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
// Resolve cooldown state and verify exactly one real or dummy Argon2id hash. Only active
|
|
547
|
+
// accounts with passwords receive authentication authority; every other state follows the
|
|
548
|
+
// same verification and persistence shape.
|
|
549
|
+
const observedUser = await dependencies.prepareUserForPasswordLogin(org.id, email);
|
|
550
|
+
const eligibleUser = observedUser?.status === 'active' && observedUser.hasPassword ? observedUser : null;
|
|
551
|
+
const passwordValid = await dependencies.verifyLoginPassword(eligibleUser?.id ?? null, password);
|
|
552
|
+
if (!eligibleUser || !passwordValid) {
|
|
553
|
+
const lockResult = await dependencies.recordPasswordFailure(eligibleUser);
|
|
554
|
+
void dependencies.writeAuditLog({
|
|
555
|
+
organizationId: org.id,
|
|
556
|
+
eventType: 'user.login.password.failed',
|
|
557
|
+
eventCategory: 'security',
|
|
558
|
+
description: 'Password login rejected',
|
|
559
|
+
ipAddress: ctx.ip,
|
|
560
|
+
metadata: { failedCount: lockResult.failedCount, autoLocked: lockResult.locked },
|
|
561
|
+
});
|
|
562
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('login.error_invalid'), 200, dependencies);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const user = eligibleUser;
|
|
566
|
+
// Step 6: Password valid — check if 2FA is required
|
|
567
|
+
await dependencies.resetRateLimit(rateLimitKey);
|
|
568
|
+
// Determine 2FA requirements based on org policy and user state
|
|
569
|
+
const twoFactorRequired = user.twoFactorEnabled || requiresTwoFactor(org, user);
|
|
570
|
+
const twoFactorMethod = determineTwoFactorMethod(org, user);
|
|
571
|
+
if (twoFactorRequired) {
|
|
572
|
+
// Store pending login in the interaction session — user must complete 2FA
|
|
573
|
+
await provider.interactionResult(ctx.req, ctx.res, {
|
|
574
|
+
twoFactor: {
|
|
575
|
+
pendingAccountId: user.id,
|
|
576
|
+
method: twoFactorMethod ?? 'email',
|
|
577
|
+
email: user.email,
|
|
578
|
+
},
|
|
579
|
+
}, { mergeWithLastSubmission: true });
|
|
580
|
+
// If method is email, auto-send the first OTP code
|
|
581
|
+
if (twoFactorMethod === 'email' || (!user.twoFactorEnabled && !twoFactorMethod)) {
|
|
582
|
+
try {
|
|
583
|
+
const otpCode = await sendOtpCode(user.id, user.email, org.id);
|
|
584
|
+
// Send the OTP code via email (fire-and-forget)
|
|
585
|
+
sendOtpCodeEmail({
|
|
586
|
+
id: user.id,
|
|
587
|
+
email: user.email,
|
|
588
|
+
givenName: user.givenName,
|
|
589
|
+
familyName: user.familyName,
|
|
590
|
+
}, {
|
|
591
|
+
id: org.id,
|
|
592
|
+
slug: org.slug,
|
|
593
|
+
brandingLogoUrl: org.brandingLogoUrl,
|
|
594
|
+
brandingPrimaryColor: org.brandingPrimaryColor,
|
|
595
|
+
brandingCompanyName: org.brandingCompanyName,
|
|
596
|
+
}, otpCode, 10, locale);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
logger.warn({ event: 'login-otp-delivery-failed' }, 'Initial OTP delivery failed');
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
writeAuditLog({
|
|
603
|
+
organizationId: org.id,
|
|
604
|
+
userId: user.id,
|
|
605
|
+
eventType: '2fa.challenge.started',
|
|
606
|
+
eventCategory: 'authentication',
|
|
607
|
+
description: `Two-factor challenge initiated (${twoFactorMethod ?? 'setup_required'})`,
|
|
608
|
+
ipAddress: ctx.ip,
|
|
609
|
+
});
|
|
610
|
+
// Redirect to the 2FA verification page (or setup page if not enrolled)
|
|
611
|
+
if (user.twoFactorEnabled) {
|
|
612
|
+
ctx.redirect(`/interaction/${interaction.uid}/two-factor`);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
// User needs to set up 2FA first (org requires it but user hasn't enrolled)
|
|
616
|
+
ctx.redirect(`/interaction/${interaction.uid}/two-factor/setup`);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// No 2FA required — complete login normally
|
|
621
|
+
await dependencies.recordLogin(user.id);
|
|
622
|
+
// Audit: successful login (no 2FA)
|
|
623
|
+
dependencies.writeAuditLog({
|
|
624
|
+
organizationId: org.id,
|
|
625
|
+
userId: user.id,
|
|
626
|
+
eventType: 'user.login.password',
|
|
627
|
+
eventCategory: 'authentication',
|
|
628
|
+
description: 'Password login successful',
|
|
629
|
+
ipAddress: ctx.ip,
|
|
630
|
+
});
|
|
631
|
+
// Finish the OIDC interaction with the logged-in user
|
|
632
|
+
const result = {
|
|
633
|
+
login: { accountId: user.id },
|
|
634
|
+
};
|
|
635
|
+
await provider.interactionFinished(ctx.req, ctx.res, result, {
|
|
636
|
+
mergeWithLastSubmission: false,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
logger.error({ event: 'login-processing-failed' }, 'Login processing failed');
|
|
641
|
+
await renderErrorPage(ctx, 'errors.interaction_expired');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Send a magic link email for passwordless authentication.
|
|
646
|
+
*
|
|
647
|
+
* Always shows the "check your email" page regardless of whether the
|
|
648
|
+
* user exists — this prevents user enumeration attacks. If the user is
|
|
649
|
+
* found and active, generates a token, stores it, and sends the email.
|
|
650
|
+
*
|
|
651
|
+
* @param ctx - Koa context with organization state
|
|
652
|
+
* @param provider - OIDC provider instance
|
|
653
|
+
*/
|
|
654
|
+
async function handleSendMagicLink(ctx, provider, dependencies) {
|
|
655
|
+
const body = ctx.request.body;
|
|
656
|
+
const email = (body.email ?? '').trim().toLowerCase();
|
|
657
|
+
const submittedCsrf = body._csrf ?? '';
|
|
658
|
+
const storedCsrf = dependencies.getCsrfFromCookie(ctx) ?? '';
|
|
659
|
+
try {
|
|
660
|
+
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
|
|
661
|
+
// Resolve organization from the interaction's client_id
|
|
662
|
+
await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
|
|
663
|
+
const org = ctx.state.organization;
|
|
664
|
+
// Resolve locale for page rendering
|
|
665
|
+
const locale = await dependencies.resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale);
|
|
666
|
+
const t = dependencies.getTranslationFunction(locale, org.slug);
|
|
667
|
+
// Verify CSRF token (cookie vs form field)
|
|
668
|
+
if (!dependencies.verifyCsrfToken(storedCsrf, submittedCsrf)) {
|
|
669
|
+
logger.warn({ event: 'magic_link_csrf_rejected' }, 'Magic link request rejected by CSRF validation');
|
|
670
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.csrf_invalid'), 200, dependencies);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// Enforce login method — 'magic_link' must be allowed for this client.
|
|
674
|
+
// Mirror of the password-login enforcement above. Same rationale: backend
|
|
675
|
+
// is the authoritative guard; the template hiding the button is cosmetic.
|
|
676
|
+
const oidcClientForMagicLink = await provider.Client.find(interaction.params.client_id);
|
|
677
|
+
const effectiveMagicLinkMethods = oidcClientForMagicLink
|
|
678
|
+
? resolveLoginMethodsFromOidcClient(oidcClientForMagicLink, org)
|
|
679
|
+
: org.defaultLoginMethods;
|
|
680
|
+
if (!effectiveMagicLinkMethods.includes('magic_link')) {
|
|
681
|
+
logger.warn({
|
|
682
|
+
event: 'magic_link_method_disabled',
|
|
683
|
+
organizationId: org.id,
|
|
684
|
+
clientId: interaction.params.client_id,
|
|
685
|
+
}, 'Magic link attempted for client where method is disabled');
|
|
686
|
+
dependencies.writeAuditLog({
|
|
687
|
+
organizationId: org.id,
|
|
688
|
+
eventType: 'security.login_method_disabled',
|
|
689
|
+
eventCategory: 'security',
|
|
690
|
+
description: 'Magic link attempted on client where method is disabled',
|
|
691
|
+
ipAddress: ctx.ip,
|
|
692
|
+
metadata: {
|
|
693
|
+
clientId: interaction.params.client_id,
|
|
694
|
+
attemptedMethod: 'magic_link',
|
|
695
|
+
effectiveMethods: effectiveMagicLinkMethods,
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.login_method_disabled'), 403, dependencies);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const recoveryAuthority = deriveMagicLinkRecoveryAuthority(ctx.params.uid, interaction.uid, interaction.params.client_id, oidcClientForMagicLink?.metadata().organizationId, org);
|
|
702
|
+
if (!recoveryAuthority) {
|
|
703
|
+
logger.warn({ event: 'magic_link_authority_rejected' }, 'Magic link authority rejected');
|
|
704
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.interaction_expired'), 400, dependencies);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
// Check rate limit for magic link requests
|
|
708
|
+
const rateLimitKey = dependencies.buildMagicLinkRateLimitKey(org.id, email);
|
|
709
|
+
const rateLimitConfig = await dependencies.loadMagicLinkRateLimitConfig();
|
|
710
|
+
const rateLimitResult = await dependencies.checkRateLimit(rateLimitKey, rateLimitConfig);
|
|
711
|
+
if (!rateLimitResult.allowed) {
|
|
712
|
+
ctx.set('Retry-After', String(rateLimitResult.retryAfter));
|
|
713
|
+
dependencies.writeAuditLog({
|
|
714
|
+
organizationId: org.id,
|
|
715
|
+
eventType: 'rate_limit.magic_link',
|
|
716
|
+
eventCategory: 'security',
|
|
717
|
+
description: 'Magic link rate limit exceeded',
|
|
718
|
+
ipAddress: ctx.ip,
|
|
719
|
+
});
|
|
720
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.rate_limit_exceeded'), 429, dependencies);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
await dependencies.enqueueAccountRecovery({
|
|
725
|
+
jobType: 'magic_link',
|
|
726
|
+
organizationId: recoveryAuthority.organizationId,
|
|
727
|
+
email,
|
|
728
|
+
interactionUid: recoveryAuthority.interactionUid,
|
|
729
|
+
actionNonce: submittedCsrf,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
logger.error({ event: 'magic_link_enqueue_failed' }, 'Failed to enqueue magic link');
|
|
734
|
+
await renderLoginWithError(ctx, provider, interaction, t, locale, email, t('errors.interaction_expired'), 503, dependencies);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// Always render the "check your email" page — prevents user enumeration
|
|
738
|
+
const csrfToken = dependencies.generateCsrfToken();
|
|
739
|
+
const context = {
|
|
740
|
+
...buildBaseContext(ctx, locale, csrfToken, org.slug),
|
|
741
|
+
t,
|
|
742
|
+
email,
|
|
743
|
+
loginUrl: `/interaction/${interaction.uid}`,
|
|
744
|
+
};
|
|
745
|
+
await renderAndRespond(ctx, 'magic-link-sent', context, 200, dependencies.renderPage);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
logger.error({ event: 'magic_link_request_failed' }, 'Magic link request failed');
|
|
749
|
+
await renderErrorPage(ctx, 'errors.interaction_expired');
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Show the consent page for an OIDC interaction.
|
|
754
|
+
*
|
|
755
|
+
* If the client belongs to the same organization (first-party app),
|
|
756
|
+
* auto-consents by immediately finishing the interaction. Otherwise,
|
|
757
|
+
* renders the consent page with requested scopes and client info.
|
|
758
|
+
*
|
|
759
|
+
* @param ctx - Koa context with organization state
|
|
760
|
+
* @param provider - OIDC provider instance
|
|
761
|
+
*/
|
|
762
|
+
async function showConsent(ctx, provider) {
|
|
763
|
+
try {
|
|
764
|
+
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
|
|
765
|
+
const { prompt, params } = interaction;
|
|
766
|
+
// Resolve organization from the interaction's client_id
|
|
767
|
+
await resolveOrganizationForInteraction(ctx, params.client_id, interaction.uid);
|
|
768
|
+
const org = ctx.state.organization;
|
|
769
|
+
// Resolve locale for the consent page
|
|
770
|
+
const uiLocales = params.ui_locales;
|
|
771
|
+
const acceptLanguage = ctx.get('Accept-Language') || undefined;
|
|
772
|
+
const locale = await resolveLocale(uiLocales, acceptLanguage, org.defaultLocale);
|
|
773
|
+
const t = getTranslationFunction(locale, org.slug);
|
|
774
|
+
// Auto-consent for first-party apps: if the client metadata includes
|
|
775
|
+
// the org ID matching the current tenant, skip the consent screen
|
|
776
|
+
const clientId = params.client_id;
|
|
777
|
+
const client = await provider.Client.find(clientId);
|
|
778
|
+
if (client) {
|
|
779
|
+
// Check if this is a first-party client (belongs to the same org)
|
|
780
|
+
const clientOrgId = client.metadata()?.organizationId;
|
|
781
|
+
if (clientOrgId && clientOrgId === org.id) {
|
|
782
|
+
// Auto-consent: finish interaction immediately with consent grant
|
|
783
|
+
const grant = new provider.Grant({ accountId: interaction.session?.accountId, clientId });
|
|
784
|
+
// Use prompt.details to grant exactly what the provider requires
|
|
785
|
+
const details = prompt.details;
|
|
786
|
+
if (details.missingOIDCScope) {
|
|
787
|
+
const scopes = details.missingOIDCScope;
|
|
788
|
+
grant.addOIDCScope([...scopes].join(' '));
|
|
789
|
+
}
|
|
790
|
+
if (details.missingOIDCClaims) {
|
|
791
|
+
const claims = details.missingOIDCClaims;
|
|
792
|
+
grant.addOIDCClaims([...claims]);
|
|
793
|
+
}
|
|
794
|
+
if (details.missingResourceScopes) {
|
|
795
|
+
for (const [indicator, scopes] of Object.entries(details.missingResourceScopes)) {
|
|
796
|
+
grant.addResourceScope(indicator, [...scopes].join(' '));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Explicitly grant offline_access if requested — node-oidc-provider
|
|
800
|
+
// treats it as a consent signal and may not include it in missingOIDCScope,
|
|
801
|
+
// but the Grant must include it for a refresh_token to be issued.
|
|
802
|
+
const requestedScope = params.scope ?? '';
|
|
803
|
+
if (requestedScope.includes('offline_access')) {
|
|
804
|
+
grant.addOIDCScope('offline_access');
|
|
805
|
+
}
|
|
806
|
+
const grantId = await grant.save();
|
|
807
|
+
const result = {
|
|
808
|
+
consent: { grantId },
|
|
809
|
+
};
|
|
810
|
+
writeAuditLog({
|
|
811
|
+
organizationId: org.id,
|
|
812
|
+
userId: interaction.session?.accountId,
|
|
813
|
+
eventType: 'user.consent.granted',
|
|
814
|
+
eventCategory: 'authentication',
|
|
815
|
+
description: `Auto-consent granted for first-party client ${clientId}`,
|
|
816
|
+
});
|
|
817
|
+
await provider.interactionFinished(ctx.req, ctx.res, result, {
|
|
818
|
+
mergeWithLastSubmission: true,
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// Third-party client: show the consent page
|
|
824
|
+
const csrfToken = generateCsrfToken();
|
|
825
|
+
setCsrfCookie(ctx, csrfToken);
|
|
826
|
+
const requestedScopes = (params.scope ?? '').split(' ').filter(Boolean);
|
|
827
|
+
const context = {
|
|
828
|
+
...buildBaseContext(ctx, locale, csrfToken, org.slug),
|
|
829
|
+
t,
|
|
830
|
+
interaction: {
|
|
831
|
+
uid: interaction.uid,
|
|
832
|
+
prompt: prompt.name,
|
|
833
|
+
params: params,
|
|
834
|
+
client: { clientName: client?.metadata()?.client_name ?? clientId },
|
|
835
|
+
},
|
|
836
|
+
scopes: requestedScopes,
|
|
837
|
+
clientName: client?.metadata()?.client_name ?? clientId,
|
|
838
|
+
};
|
|
839
|
+
await renderAndRespond(ctx, 'consent', context);
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
logger.error({ event: 'consent-page-failed' }, 'Consent page rendering failed');
|
|
843
|
+
await renderErrorPage(ctx, 'errors.interaction_expired');
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Process a consent decision (approve or deny).
|
|
848
|
+
*
|
|
849
|
+
* Reads the decision from the form body. On approval, creates a Grant
|
|
850
|
+
* with the requested scopes. On denial, finishes the interaction with
|
|
851
|
+
* an access_denied error.
|
|
852
|
+
*
|
|
853
|
+
* @param ctx - Koa context with organization state
|
|
854
|
+
* @param provider - OIDC provider instance
|
|
855
|
+
*/
|
|
856
|
+
async function processConsent(ctx, provider) {
|
|
857
|
+
const body = ctx.request.body;
|
|
858
|
+
const submittedCsrf = body._csrf ?? '';
|
|
859
|
+
const storedCsrf = getCsrfFromCookie(ctx) ?? '';
|
|
860
|
+
const decision = body.decision; // 'approve' or 'deny'
|
|
861
|
+
try {
|
|
862
|
+
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
|
|
863
|
+
// Resolve organization from the interaction's client_id
|
|
864
|
+
await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
|
|
865
|
+
const org = ctx.state.organization;
|
|
866
|
+
// Verify CSRF token
|
|
867
|
+
if (!verifyCsrfToken(storedCsrf, submittedCsrf)) {
|
|
868
|
+
logger.warn({ event: 'consent-csrf-rejected' }, 'Consent request CSRF validation failed');
|
|
869
|
+
ctx.status = 403;
|
|
870
|
+
ctx.body = 'Invalid CSRF token';
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (decision === 'approve') {
|
|
874
|
+
// Create a grant for the approved scopes and claims
|
|
875
|
+
const clientId = interaction.params.client_id;
|
|
876
|
+
const grant = new provider.Grant({
|
|
877
|
+
accountId: interaction.session?.accountId,
|
|
878
|
+
clientId,
|
|
879
|
+
});
|
|
880
|
+
// Use prompt.details to grant exactly what the provider requires
|
|
881
|
+
const details = interaction.prompt.details;
|
|
882
|
+
// Grant missing OIDC scopes
|
|
883
|
+
if (details.missingOIDCScope) {
|
|
884
|
+
const scopes = details.missingOIDCScope;
|
|
885
|
+
grant.addOIDCScope([...scopes].join(' '));
|
|
886
|
+
}
|
|
887
|
+
// Grant missing OIDC claims
|
|
888
|
+
if (details.missingOIDCClaims) {
|
|
889
|
+
const claims = details.missingOIDCClaims;
|
|
890
|
+
grant.addOIDCClaims([...claims]);
|
|
891
|
+
}
|
|
892
|
+
// Grant missing resource scopes
|
|
893
|
+
if (details.missingResourceScopes) {
|
|
894
|
+
for (const [indicator, scopes] of Object.entries(details.missingResourceScopes)) {
|
|
895
|
+
grant.addResourceScope(indicator, [...scopes].join(' '));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// Explicitly grant offline_access if requested (see showConsent for details)
|
|
899
|
+
const requestedScope = interaction.params.scope ?? '';
|
|
900
|
+
if (requestedScope.includes('offline_access')) {
|
|
901
|
+
grant.addOIDCScope('offline_access');
|
|
902
|
+
}
|
|
903
|
+
const grantId = await grant.save();
|
|
904
|
+
writeAuditLog({
|
|
905
|
+
organizationId: org.id,
|
|
906
|
+
userId: interaction.session?.accountId,
|
|
907
|
+
eventType: 'user.consent.granted',
|
|
908
|
+
eventCategory: 'authentication',
|
|
909
|
+
description: `Consent granted for client ${clientId}`,
|
|
910
|
+
});
|
|
911
|
+
const result = {
|
|
912
|
+
consent: { grantId },
|
|
913
|
+
};
|
|
914
|
+
await provider.interactionFinished(ctx.req, ctx.res, result, {
|
|
915
|
+
mergeWithLastSubmission: true,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
// Deny consent
|
|
920
|
+
writeAuditLog({
|
|
921
|
+
organizationId: org.id,
|
|
922
|
+
userId: interaction.session?.accountId,
|
|
923
|
+
eventType: 'user.consent.denied',
|
|
924
|
+
eventCategory: 'authentication',
|
|
925
|
+
description: `Consent denied for client ${interaction.params.client_id}`,
|
|
926
|
+
});
|
|
927
|
+
const result = {
|
|
928
|
+
error: 'access_denied',
|
|
929
|
+
error_description: 'User denied consent',
|
|
930
|
+
};
|
|
931
|
+
await provider.interactionFinished(ctx.req, ctx.res, result, {
|
|
932
|
+
mergeWithLastSubmission: false,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
logger.error({ event: 'consent-processing-failed' }, 'Consent processing failed');
|
|
938
|
+
await renderErrorPage(ctx, 'errors.interaction_expired');
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Abort an OIDC interaction.
|
|
943
|
+
*
|
|
944
|
+
* Finishes the interaction with an access_denied error, effectively
|
|
945
|
+
* cancelling the authorization flow and redirecting back to the client.
|
|
946
|
+
*
|
|
947
|
+
* @param ctx - Koa context with organization state
|
|
948
|
+
* @param provider - OIDC provider instance
|
|
949
|
+
*/
|
|
950
|
+
async function abortInteraction(ctx, provider) {
|
|
951
|
+
try {
|
|
952
|
+
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
|
|
953
|
+
// Resolve organization from the interaction's client_id
|
|
954
|
+
await resolveOrganizationForInteraction(ctx, interaction.params.client_id, interaction.uid);
|
|
955
|
+
const org = ctx.state.organization;
|
|
956
|
+
writeAuditLog({
|
|
957
|
+
organizationId: org.id,
|
|
958
|
+
userId: interaction.session?.accountId,
|
|
959
|
+
eventType: 'user.consent.denied',
|
|
960
|
+
eventCategory: 'authentication',
|
|
961
|
+
description: 'Interaction aborted by user',
|
|
962
|
+
});
|
|
963
|
+
const result = {
|
|
964
|
+
error: 'access_denied',
|
|
965
|
+
error_description: 'User aborted the interaction',
|
|
966
|
+
};
|
|
967
|
+
await provider.interactionFinished(ctx.req, ctx.res, result, {
|
|
968
|
+
mergeWithLastSubmission: false,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
logger.error({ event: 'interaction-abort-failed' }, 'Interaction abort failed');
|
|
973
|
+
await renderErrorPage(ctx, 'errors.interaction_expired');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// ---------------------------------------------------------------------------
|
|
977
|
+
// Helper: render login page with error
|
|
978
|
+
// ---------------------------------------------------------------------------
|
|
979
|
+
/**
|
|
980
|
+
* Render the login page with an error flash message.
|
|
981
|
+
* Used when login fails (wrong password, rate limited, CSRF mismatch, etc.).
|
|
982
|
+
*
|
|
983
|
+
* @param ctx - Koa context
|
|
984
|
+
* @param provider - OIDC provider instance (used to resolve human-readable client name)
|
|
985
|
+
* @param interaction - OIDC interaction details
|
|
986
|
+
* @param t - Translation function
|
|
987
|
+
* @param locale - Resolved locale
|
|
988
|
+
* @param email - Pre-filled email value
|
|
989
|
+
* @param errorMessage - Error message to display
|
|
990
|
+
* @param statusCode - HTTP status code (default: 200)
|
|
991
|
+
*/
|
|
992
|
+
async function renderLoginWithError(ctx, provider, interaction, t, locale, email, errorMessage, statusCode = 200, dependencies = defaultEnumerationDependencies) {
|
|
993
|
+
const org = ctx.state.organization;
|
|
994
|
+
const csrfToken = dependencies.generateCsrfToken();
|
|
995
|
+
dependencies.setCsrfCookie(ctx, csrfToken);
|
|
996
|
+
// Resolve human-readable client name from provider metadata.
|
|
997
|
+
// Falls back to raw client_id if the client is not found.
|
|
998
|
+
const clientId = interaction.params.client_id ?? '';
|
|
999
|
+
const oidcClient = await provider.Client.find(clientId);
|
|
1000
|
+
const clientName = oidcClient?.metadata()?.client_name ?? clientId;
|
|
1001
|
+
// Resolve effective login methods so the re-rendered login page shows
|
|
1002
|
+
// the correct forms (password, magic-link, or both). Without these flags,
|
|
1003
|
+
// the template's no-methods fallback would incorrectly appear alongside
|
|
1004
|
+
// the flash error message.
|
|
1005
|
+
const effectiveMethods = oidcClient
|
|
1006
|
+
? resolveLoginMethodsFromOidcClient(oidcClient, org)
|
|
1007
|
+
: org.defaultLoginMethods;
|
|
1008
|
+
const showPassword = effectiveMethods.includes('password');
|
|
1009
|
+
const showMagicLink = effectiveMethods.includes('magic_link');
|
|
1010
|
+
const context = {
|
|
1011
|
+
...buildBaseContext(ctx, locale, csrfToken, org.slug),
|
|
1012
|
+
t,
|
|
1013
|
+
interaction: {
|
|
1014
|
+
uid: interaction.uid,
|
|
1015
|
+
prompt: interaction.prompt.name,
|
|
1016
|
+
params: interaction.params,
|
|
1017
|
+
client: { clientName },
|
|
1018
|
+
},
|
|
1019
|
+
email,
|
|
1020
|
+
flash: { error: errorMessage },
|
|
1021
|
+
// Login method rendering flags — must match showLogin() to avoid
|
|
1022
|
+
// the "no methods configured" fallback appearing on error re-renders.
|
|
1023
|
+
showPassword,
|
|
1024
|
+
showMagicLink,
|
|
1025
|
+
showDivider: showPassword && showMagicLink,
|
|
1026
|
+
loginMethods: effectiveMethods,
|
|
1027
|
+
};
|
|
1028
|
+
await renderAndRespond(ctx, 'login', context, statusCode, dependencies.renderPage);
|
|
1029
|
+
}
|
|
1030
|
+
// ---------------------------------------------------------------------------
|
|
1031
|
+
// Helper: render error page
|
|
1032
|
+
// ---------------------------------------------------------------------------
|
|
1033
|
+
/**
|
|
1034
|
+
* Render a generic error page when the interaction cannot be loaded.
|
|
1035
|
+
* Used as a fallback when the interaction has expired or is invalid.
|
|
1036
|
+
*
|
|
1037
|
+
* @param ctx - Koa context
|
|
1038
|
+
* @param errorKey - Translation key for the error message
|
|
1039
|
+
*/
|
|
1040
|
+
async function renderErrorPage(ctx, errorKey) {
|
|
1041
|
+
try {
|
|
1042
|
+
const org = ctx.state.organization;
|
|
1043
|
+
const orgSlug = org?.slug ?? 'default';
|
|
1044
|
+
const locale = org
|
|
1045
|
+
? await resolveLocale(undefined, ctx.get('Accept-Language') || undefined, org.defaultLocale)
|
|
1046
|
+
: 'en';
|
|
1047
|
+
const t = getTranslationFunction(locale, orgSlug);
|
|
1048
|
+
const csrfToken = generateCsrfToken();
|
|
1049
|
+
const context = {
|
|
1050
|
+
branding: org
|
|
1051
|
+
? buildBrandingFromOrg(org)
|
|
1052
|
+
: {
|
|
1053
|
+
logoUrl: null,
|
|
1054
|
+
faviconUrl: null,
|
|
1055
|
+
primaryColor: '#3B82F6',
|
|
1056
|
+
companyName: 'Porta',
|
|
1057
|
+
customCss: null,
|
|
1058
|
+
},
|
|
1059
|
+
locale,
|
|
1060
|
+
t,
|
|
1061
|
+
csrfToken,
|
|
1062
|
+
orgSlug,
|
|
1063
|
+
errorMessage: t(errorKey),
|
|
1064
|
+
};
|
|
1065
|
+
await renderAndRespond(ctx, 'error', context, 400);
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
// Last resort: if even the error page fails, send plain text
|
|
1069
|
+
logger.error({ event: 'interaction-error-render-failed' }, 'Interaction error page failed');
|
|
1070
|
+
ctx.status = 500;
|
|
1071
|
+
ctx.body = 'An error occurred';
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
//# sourceMappingURL=interactions.js.map
|