@bajustone/fortress 1.0.0-rc.1
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 +1011 -0
- package/LICENSE +21 -0
- package/README.md +760 -0
- package/SECURITY.md +197 -0
- package/bin/fortress.ts +667 -0
- package/dist/auth-service-BcyQ2PuZ.d.cts +307 -0
- package/dist/auth-service-BkzZkWfH.d.ts +307 -0
- package/dist/config-B2366s_G.d.ts +665 -0
- package/dist/config-GtlVt6ZD.d.cts +665 -0
- package/dist/convert-routes-BLCQT57f.d.ts +72 -0
- package/dist/convert-routes-BdMvP2_X.d.cts +72 -0
- package/dist/crypto.cjs +61 -0
- package/dist/crypto.d.cts +36 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/crypto.js +35 -0
- package/dist/drift-BT1wtMDJ.d.cts +22 -0
- package/dist/drift-k9LiYpRP.d.ts +22 -0
- package/dist/drizzle/pg.cjs +481 -0
- package/dist/drizzle/pg.d.cts +15 -0
- package/dist/drizzle/pg.d.ts +15 -0
- package/dist/drizzle/pg.js +454 -0
- package/dist/drizzle.cjs +1442 -0
- package/dist/drizzle.d.cts +85 -0
- package/dist/drizzle.d.ts +85 -0
- package/dist/drizzle.js +1411 -0
- package/dist/express.cjs +1357 -0
- package/dist/express.d.cts +333 -0
- package/dist/express.d.ts +333 -0
- package/dist/express.js +1314 -0
- package/dist/fetcher.cjs +77 -0
- package/dist/fetcher.d.cts +7 -0
- package/dist/fetcher.d.ts +7 -0
- package/dist/fetcher.js +41 -0
- package/dist/fortress-BmSvFfqK.d.cts +1089 -0
- package/dist/fortress-uA-_cheR.d.ts +1089 -0
- package/dist/hono.cjs +1530 -0
- package/dist/hono.d.cts +514 -0
- package/dist/hono.d.ts +514 -0
- package/dist/hono.js +1483 -0
- package/dist/index-CxEkfCA0.d.cts +77 -0
- package/dist/index-CxEkfCA0.d.ts +77 -0
- package/dist/index-D054pcb-.d.cts +146 -0
- package/dist/index-jAMbbUAY.d.ts +146 -0
- package/dist/index.cjs +9046 -0
- package/dist/index.d.cts +717 -0
- package/dist/index.d.ts +717 -0
- package/dist/index.js +8935 -0
- package/dist/jwt.cjs +255 -0
- package/dist/jwt.d.cts +90 -0
- package/dist/jwt.d.ts +90 -0
- package/dist/jwt.js +227 -0
- package/dist/otel.cjs +108 -0
- package/dist/otel.d.cts +62 -0
- package/dist/otel.d.ts +62 -0
- package/dist/otel.js +73 -0
- package/dist/plugins/account-lockout.cjs +322 -0
- package/dist/plugins/account-lockout.d.cts +42 -0
- package/dist/plugins/account-lockout.d.ts +42 -0
- package/dist/plugins/account-lockout.js +295 -0
- package/dist/plugins/admin.cjs +2378 -0
- package/dist/plugins/admin.d.cts +72 -0
- package/dist/plugins/admin.d.ts +72 -0
- package/dist/plugins/admin.js +2351 -0
- package/dist/plugins/api-key.cjs +792 -0
- package/dist/plugins/api-key.d.cts +121 -0
- package/dist/plugins/api-key.d.ts +121 -0
- package/dist/plugins/api-key.js +765 -0
- package/dist/plugins/audit-log.cjs +493 -0
- package/dist/plugins/audit-log.d.cts +86 -0
- package/dist/plugins/audit-log.d.ts +86 -0
- package/dist/plugins/audit-log.js +468 -0
- package/dist/plugins/data-isolation.cjs +211 -0
- package/dist/plugins/data-isolation.d.cts +49 -0
- package/dist/plugins/data-isolation.d.ts +49 -0
- package/dist/plugins/data-isolation.js +186 -0
- package/dist/plugins/email-verification.cjs +273 -0
- package/dist/plugins/email-verification.d.cts +44 -0
- package/dist/plugins/email-verification.d.ts +44 -0
- package/dist/plugins/email-verification.js +246 -0
- package/dist/plugins/magic-link.cjs +231 -0
- package/dist/plugins/magic-link.d.cts +39 -0
- package/dist/plugins/magic-link.d.ts +39 -0
- package/dist/plugins/magic-link.js +204 -0
- package/dist/plugins/oauth.cjs +1696 -0
- package/dist/plugins/oauth.d.cts +406 -0
- package/dist/plugins/oauth.d.ts +406 -0
- package/dist/plugins/oauth.js +1669 -0
- package/dist/plugins/openapi.cjs +1185 -0
- package/dist/plugins/openapi.d.cts +6 -0
- package/dist/plugins/openapi.d.ts +6 -0
- package/dist/plugins/openapi.js +1158 -0
- package/dist/plugins/rate-limit/express.cjs +55 -0
- package/dist/plugins/rate-limit/express.d.cts +64 -0
- package/dist/plugins/rate-limit/express.d.ts +64 -0
- package/dist/plugins/rate-limit/express.js +30 -0
- package/dist/plugins/rate-limit/hono.cjs +51 -0
- package/dist/plugins/rate-limit/hono.d.cts +59 -0
- package/dist/plugins/rate-limit/hono.d.ts +59 -0
- package/dist/plugins/rate-limit/hono.js +26 -0
- package/dist/plugins/rate-limit/sveltekit.cjs +49 -0
- package/dist/plugins/rate-limit/sveltekit.d.cts +59 -0
- package/dist/plugins/rate-limit/sveltekit.d.ts +59 -0
- package/dist/plugins/rate-limit/sveltekit.js +24 -0
- package/dist/plugins/rate-limit.cjs +354 -0
- package/dist/plugins/rate-limit.d.cts +140 -0
- package/dist/plugins/rate-limit.d.ts +140 -0
- package/dist/plugins/rate-limit.js +325 -0
- package/dist/plugins/social-login.cjs +905 -0
- package/dist/plugins/social-login.d.cts +114 -0
- package/dist/plugins/social-login.d.ts +114 -0
- package/dist/plugins/social-login.js +880 -0
- package/dist/plugins/tenancy.cjs +780 -0
- package/dist/plugins/tenancy.d.cts +108 -0
- package/dist/plugins/tenancy.d.ts +108 -0
- package/dist/plugins/tenancy.js +753 -0
- package/dist/plugins/two-factor.cjs +555 -0
- package/dist/plugins/two-factor.d.cts +67 -0
- package/dist/plugins/two-factor.d.ts +67 -0
- package/dist/plugins/two-factor.js +526 -0
- package/dist/plugins/webauthn.cjs +786 -0
- package/dist/plugins/webauthn.d.cts +72 -0
- package/dist/plugins/webauthn.d.ts +72 -0
- package/dist/plugins/webauthn.js +766 -0
- package/dist/plugins/webhook.cjs +819 -0
- package/dist/plugins/webhook.d.cts +254 -0
- package/dist/plugins/webhook.d.ts +254 -0
- package/dist/plugins/webhook.js +789 -0
- package/dist/protect-D1v_0upK.d.cts +123 -0
- package/dist/protect-DsxV_-dJ.d.ts +123 -0
- package/dist/sveltekit.cjs +1258 -0
- package/dist/sveltekit.d.cts +420 -0
- package/dist/sveltekit.d.ts +420 -0
- package/dist/sveltekit.js +1207 -0
- package/dist/testing.cjs +4294 -0
- package/dist/testing.d.cts +197 -0
- package/dist/testing.d.ts +197 -0
- package/dist/testing.js +4264 -0
- package/dist/types-et0R8tsC.d.cts +217 -0
- package/dist/types-et0R8tsC.d.ts +217 -0
- package/docs/adapter-typed-helpers.md +192 -0
- package/docs/admin-recipes.md +227 -0
- package/docs/architecture.md +434 -0
- package/docs/ci/github-actions.yml +59 -0
- package/docs/ci.md +109 -0
- package/docs/compatibility.md +115 -0
- package/docs/deployment.md +305 -0
- package/docs/hardening.md +118 -0
- package/docs/host-owned-routes.md +154 -0
- package/docs/migrations/0001-schema-version.md +54 -0
- package/docs/migrations/0002-initial-schema.md +84 -0
- package/docs/migrations/upgrade-guide.md +160 -0
- package/docs/observability.md +394 -0
- package/docs/plugins/account-lockout.md +131 -0
- package/docs/plugins/admin.md +402 -0
- package/docs/plugins/api-key.md +327 -0
- package/docs/plugins/audit-log.md +261 -0
- package/docs/plugins/data-isolation.md +186 -0
- package/docs/plugins/email-verification.md +121 -0
- package/docs/plugins/magic-link.md +123 -0
- package/docs/plugins/oauth.md +544 -0
- package/docs/plugins/openapi.md +250 -0
- package/docs/plugins/rate-limit.md +223 -0
- package/docs/plugins/social-login.md +337 -0
- package/docs/plugins/tenancy.md +122 -0
- package/docs/plugins/two-factor.md +191 -0
- package/docs/plugins/webauthn.md +188 -0
- package/docs/plugins/webhook.md +242 -0
- package/docs/policy-as-code.md +156 -0
- package/docs/route-manifest.md +46 -0
- package/docs/security.md +261 -0
- package/docs/threat-model.md +161 -0
- package/examples/README.md +119 -0
- package/examples/express-app/index.ts +747 -0
- package/examples/hono-app/docker-compose.yml +14 -0
- package/examples/hono-app/index.ts +1009 -0
- package/examples/policy/fortress.policy.json +47 -0
- package/examples/sveltekit-app/README.md +125 -0
- package/examples/sveltekit-app/src/app.d.ts +16 -0
- package/examples/sveltekit-app/src/hooks.server.ts +23 -0
- package/examples/sveltekit-app/src/lib/server/fortress.ts +81 -0
- package/examples/sveltekit-app/src/routes/api/echo/[id]/+server.ts +32 -0
- package/examples/sveltekit-app/src/routes/api/fortress/[...path]/+server.ts +15 -0
- package/examples/sveltekit-app/src/routes/dashboard/+page.server.ts +20 -0
- package/examples/sveltekit-app/src/routes/login/+page.server.ts +48 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.server.ts +69 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.svelte +83 -0
- package/migrations/pg/0001_schema_version.down.sql +2 -0
- package/migrations/pg/0001_schema_version.sql +8 -0
- package/migrations/pg/0002_initial_schema.down.sql +36 -0
- package/migrations/pg/0002_initial_schema.sql +376 -0
- package/migrations/pg/0003_auth_continuation.down.sql +6 -0
- package/migrations/pg/0003_auth_continuation.sql +30 -0
- package/migrations/pg/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/pg/0004_tenant_default_unique.sql +14 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.down.sql +71 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.sql +71 -0
- package/migrations/pg/0006_canonical_email.down.sql +3 -0
- package/migrations/pg/0006_canonical_email.sql +8 -0
- package/migrations/pg/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/pg/0007_audit_chain_anchor.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.down.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.sql +8 -0
- package/migrations/pg/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/pg/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/pg/0010_bigint_append_only_ids.down.sql +2 -0
- package/migrations/pg/0010_bigint_append_only_ids.sql +23 -0
- package/migrations/sqlite/0001_schema_version.down.sql +2 -0
- package/migrations/sqlite/0001_schema_version.sql +8 -0
- package/migrations/sqlite/0002_initial_schema.down.sql +36 -0
- package/migrations/sqlite/0002_initial_schema.sql +376 -0
- package/migrations/sqlite/0003_auth_continuation.down.sql +27 -0
- package/migrations/sqlite/0003_auth_continuation.sql +45 -0
- package/migrations/sqlite/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/sqlite/0004_tenant_default_unique.sql +13 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.down.sql +10 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.sql +10 -0
- package/migrations/sqlite/0006_canonical_email.down.sql +3 -0
- package/migrations/sqlite/0006_canonical_email.sql +8 -0
- package/migrations/sqlite/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/sqlite/0007_audit_chain_anchor.sql +8 -0
- package/migrations/sqlite/0008_two_factor_hardening.down.sql +14 -0
- package/migrations/sqlite/0008_two_factor_hardening.sql +7 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.down.sql +1 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.sql +2 -0
- package/package.json +398 -0
- package/src/adapters/database/index.ts +63 -0
- package/src/adapters/database/types.ts +22 -0
- package/src/changelog-script.test.ts +21 -0
- package/src/cli.test.ts +79 -0
- package/src/core/auth/auth-admin.test.ts +247 -0
- package/src/core/auth/auth-endpoints.ts +558 -0
- package/src/core/auth/auth-observer.test.ts +191 -0
- package/src/core/auth/auth-service.ts +1464 -0
- package/src/core/auth/email.test.ts +17 -0
- package/src/core/auth/email.ts +11 -0
- package/src/core/auth/hooks.test.ts +251 -0
- package/src/core/auth/impersonation.test.ts +179 -0
- package/src/core/auth/jwt.test.ts +184 -0
- package/src/core/auth/jwt.ts +239 -0
- package/src/core/auth/login-timing.test.ts +77 -0
- package/src/core/auth/password-policy.test.ts +253 -0
- package/src/core/auth/password-policy.ts +220 -0
- package/src/core/auth/password.test.ts +42 -0
- package/src/core/auth/password.ts +62 -0
- package/src/core/auth/post-auth-gate.test.ts +258 -0
- package/src/core/auth/post-auth-gate.ts +228 -0
- package/src/core/auth/refresh-token.test.ts +86 -0
- package/src/core/auth/refresh-token.ts +105 -0
- package/src/core/auth/timing-safe.ts +23 -0
- package/src/core/config.ts +212 -0
- package/src/core/endpoint.ts +149 -0
- package/src/core/errors.ts +199 -0
- package/src/core/fetcher-interop.test.ts +106 -0
- package/src/core/fortress.test.ts +354 -0
- package/src/core/fortress.ts +550 -0
- package/src/core/http/call.test.ts +148 -0
- package/src/core/http/call.ts +149 -0
- package/src/core/http/cookie-serialize.test.ts +198 -0
- package/src/core/http/cookie-serialize.ts +107 -0
- package/src/core/http/csrf.test.ts +140 -0
- package/src/core/http/csrf.ts +173 -0
- package/src/core/http/dispatch.ts +652 -0
- package/src/core/http/error-response.test.ts +69 -0
- package/src/core/http/error-response.ts +93 -0
- package/src/core/http/fortress-rbac.test.ts +43 -0
- package/src/core/http/fortress-rbac.ts +127 -0
- package/src/core/http/handle-request.test.ts +441 -0
- package/src/core/http/handle-request.ts +354 -0
- package/src/core/http/match.test.ts +122 -0
- package/src/core/http/match.ts +126 -0
- package/src/core/http/outbound.test.ts +34 -0
- package/src/core/http/outbound.ts +105 -0
- package/src/core/http/plugin-middleware.ts +67 -0
- package/src/core/http/principal.test.ts +345 -0
- package/src/core/http/principal.ts +86 -0
- package/src/core/http/protect.test.ts +220 -0
- package/src/core/http/protect.ts +394 -0
- package/src/core/http/token-extraction.test.ts +59 -0
- package/src/core/http/token-extraction.ts +53 -0
- package/src/core/iam/explain.test.ts +177 -0
- package/src/core/iam/explain.ts +302 -0
- package/src/core/iam/iam-endpoints.ts +779 -0
- package/src/core/iam/iam-service.test.ts +829 -0
- package/src/core/iam/iam-service.ts +1137 -0
- package/src/core/iam/permission-cache.test.ts +115 -0
- package/src/core/iam/permission-cache.ts +71 -0
- package/src/core/iam/permission-check-observer.test.ts +90 -0
- package/src/core/iam/permission-evaluator.test.ts +291 -0
- package/src/core/iam/permission-evaluator.ts +198 -0
- package/src/core/iam/permission-sync.test.ts +166 -0
- package/src/core/iam/permission-sync.ts +192 -0
- package/src/core/iam/resource-sync.test.ts +70 -0
- package/src/core/iam/resource-sync.ts +182 -0
- package/src/core/iam/service-account-http.test.ts +529 -0
- package/src/core/iam/service-account.test.ts +282 -0
- package/src/core/internal-adapter.test.ts +389 -0
- package/src/core/internal-adapter.ts +435 -0
- package/src/core/json-schema.ts +50 -0
- package/src/core/manifest/__snapshots__/route-manifest.test.ts.snap +192 -0
- package/src/core/manifest/drift.ts +103 -0
- package/src/core/manifest/route-manifest.test.ts +142 -0
- package/src/core/manifest/route-manifest.ts +154 -0
- package/src/core/migrate.test.ts +72 -0
- package/src/core/migrations/engine.test.ts +308 -0
- package/src/core/migrations/engine.ts +548 -0
- package/src/core/migrations/migrations.ts +1771 -0
- package/src/core/migrations/pg-migration.integration-test.ts +353 -0
- package/src/core/migrations/upgrade-fixture.test.ts +378 -0
- package/src/core/observability/db-instrumentation.ts +205 -0
- package/src/core/observability/listener-list.test.ts +124 -0
- package/src/core/observability/listener-list.ts +73 -0
- package/src/core/observability/logger.test.ts +43 -0
- package/src/core/observability/logger.ts +49 -0
- package/src/core/observability/spans.test.ts +193 -0
- package/src/core/observability/types.ts +80 -0
- package/src/core/openapi-drift.test.ts +41 -0
- package/src/core/openapi.ts +47 -0
- package/src/core/plugin-methods-map.ts +75 -0
- package/src/core/plugin-runner.test.ts +233 -0
- package/src/core/plugin-runner.ts +276 -0
- package/src/core/plugin.ts +210 -0
- package/src/core/policy/apply.ts +272 -0
- package/src/core/policy/diff.ts +288 -0
- package/src/core/policy/loader.test.ts +63 -0
- package/src/core/policy/loader.ts +214 -0
- package/src/core/policy/policy.test.ts +232 -0
- package/src/core/policy/types.ts +105 -0
- package/src/core/schema-builder.test.ts +660 -0
- package/src/core/schema-builder.ts +881 -0
- package/src/core/standard-schema.ts +56 -0
- package/src/core/types.ts +258 -0
- package/src/core/validation.test.ts +195 -0
- package/src/core/validation.ts +192 -0
- package/src/drizzle/adapter.test.ts +425 -0
- package/src/drizzle/adapter.ts +474 -0
- package/src/drizzle/index.ts +31 -0
- package/src/drizzle/pg/adapter.integration-test.ts +456 -0
- package/src/drizzle/pg/index.ts +13 -0
- package/src/drizzle/pg/pg.integration-test.ts +1539 -0
- package/src/drizzle/pg/schema.ts +539 -0
- package/src/drizzle/pg-error-map.test.ts +218 -0
- package/src/drizzle/pg-error-map.ts +151 -0
- package/src/drizzle/schema.ts +544 -0
- package/src/drizzle/sqlite-serialization.test.ts +106 -0
- package/src/express/express-api-key-user-routes.test.ts +241 -0
- package/src/express/express.test.ts +442 -0
- package/src/express/handle.ts +191 -0
- package/src/express/index.ts +62 -0
- package/src/express/middleware.ts +551 -0
- package/src/express/protect.ts +154 -0
- package/src/express/validated.test.ts +86 -0
- package/src/express/validated.ts +99 -0
- package/src/fetcher/index.ts +81 -0
- package/src/hono/convert-routes.test.ts +220 -0
- package/src/hono/convert-routes.ts +196 -0
- package/src/hono/converters.test.ts +50 -0
- package/src/hono/converters.ts +43 -0
- package/src/hono/handle.ts +79 -0
- package/src/hono/helpers.ts +2 -0
- package/src/hono/hono-api-key-user-routes.test.ts +225 -0
- package/src/hono/hono-handlers.test.ts +861 -0
- package/src/hono/hono.test.ts +454 -0
- package/src/hono/index.ts +101 -0
- package/src/hono/middleware/auth.ts +236 -0
- package/src/hono/middleware/auth.types.test.ts +94 -0
- package/src/hono/middleware/csrf.test.ts +127 -0
- package/src/hono/middleware/csrf.ts +68 -0
- package/src/hono/middleware/error-handler.ts +12 -0
- package/src/hono/middleware/plugin-middleware.ts +35 -0
- package/src/hono/middleware/rbac.ts +131 -0
- package/src/hono/middleware/security-headers.test.ts +88 -0
- package/src/hono/middleware/security-headers.ts +68 -0
- package/src/hono/openapi.ts +237 -0
- package/src/hono/protect.ts +87 -0
- package/src/hono/validated.test.ts +123 -0
- package/src/hono/validated.ts +62 -0
- package/src/index.ts +309 -0
- package/src/integration.test.ts +718 -0
- package/src/internal/changelog.ts +56 -0
- package/src/otel/index.ts +158 -0
- package/src/otel/otel-types.test.ts +35 -0
- package/src/otel/otel.test.ts +235 -0
- package/src/plugins/account-lockout/account-lockout.test.ts +367 -0
- package/src/plugins/account-lockout/index.ts +267 -0
- package/src/plugins/admin/admin-api-key.test.ts +438 -0
- package/src/plugins/admin/index.ts +1612 -0
- package/src/plugins/api-key/api-key.test.ts +684 -0
- package/src/plugins/api-key/core.ts +336 -0
- package/src/plugins/api-key/index.ts +315 -0
- package/src/plugins/audit-log/audit-log.test.ts +749 -0
- package/src/plugins/audit-log/index.ts +680 -0
- package/src/plugins/data-isolation/data-isolation.test.ts +197 -0
- package/src/plugins/data-isolation/index.ts +157 -0
- package/src/plugins/email-verification/email-verification.test.ts +199 -0
- package/src/plugins/email-verification/index.ts +190 -0
- package/src/plugins/magic-link/index.ts +129 -0
- package/src/plugins/magic-link/magic-link.test.ts +156 -0
- package/src/plugins/oauth/id-token.ts +86 -0
- package/src/plugins/oauth/index.ts +2145 -0
- package/src/plugins/oauth/jwks.test.ts +32 -0
- package/src/plugins/oauth/jwks.ts +182 -0
- package/src/plugins/oauth/oauth.test.ts +2220 -0
- package/src/plugins/oauth/pkce.ts +58 -0
- package/src/plugins/openapi/index.ts +298 -0
- package/src/plugins/openapi/openapi.test.ts +425 -0
- package/src/plugins/openapi/spec-builder.ts +287 -0
- package/src/plugins/rate-limit/express.test.ts +89 -0
- package/src/plugins/rate-limit/express.ts +86 -0
- package/src/plugins/rate-limit/hono.test.ts +60 -0
- package/src/plugins/rate-limit/hono.ts +74 -0
- package/src/plugins/rate-limit/index.ts +383 -0
- package/src/plugins/rate-limit/memory-store.ts +61 -0
- package/src/plugins/rate-limit/rate-limit.test.ts +756 -0
- package/src/plugins/rate-limit/sveltekit.test.ts +58 -0
- package/src/plugins/rate-limit/sveltekit.ts +66 -0
- package/src/plugins/social-login/index.ts +715 -0
- package/src/plugins/social-login/providers/apple.ts +34 -0
- package/src/plugins/social-login/providers/discord.ts +26 -0
- package/src/plugins/social-login/providers/github.ts +54 -0
- package/src/plugins/social-login/providers/google.ts +23 -0
- package/src/plugins/social-login/providers/index.ts +22 -0
- package/src/plugins/social-login/providers/microsoft.ts +35 -0
- package/src/plugins/social-login/providers/oidc.ts +37 -0
- package/src/plugins/social-login/providers/providers.test.ts +211 -0
- package/src/plugins/social-login/social-login.test.ts +675 -0
- package/src/plugins/social-login/types.ts +80 -0
- package/src/plugins/tenancy/index.ts +544 -0
- package/src/plugins/tenancy/tenancy.test.ts +323 -0
- package/src/plugins/two-factor/index.ts +569 -0
- package/src/plugins/two-factor/two-factor.test.ts +235 -0
- package/src/plugins/webauthn/index.ts +554 -0
- package/src/plugins/webauthn/webauthn.test.ts +472 -0
- package/src/plugins/webhook/builtin-events.ts +29 -0
- package/src/plugins/webhook/delivery.test.ts +51 -0
- package/src/plugins/webhook/delivery.ts +154 -0
- package/src/plugins/webhook/hooks.ts +89 -0
- package/src/plugins/webhook/index.ts +445 -0
- package/src/plugins/webhook/queue/database.ts +67 -0
- package/src/plugins/webhook/queue/in-memory.test.ts +48 -0
- package/src/plugins/webhook/queue/in-memory.ts +71 -0
- package/src/plugins/webhook/queue/types.ts +51 -0
- package/src/plugins/webhook/registry.test.ts +48 -0
- package/src/plugins/webhook/registry.ts +64 -0
- package/src/plugins/webhook/signing.ts +45 -0
- package/src/plugins/webhook/ssrf.ts +198 -0
- package/src/plugins/webhook/types.ts +138 -0
- package/src/plugins/webhook/webhook.test.ts +324 -0
- package/src/sveltekit/actions.ts +233 -0
- package/src/sveltekit/catch-all.ts +43 -0
- package/src/sveltekit/cookies.ts +136 -0
- package/src/sveltekit/handle.ts +356 -0
- package/src/sveltekit/helpers.ts +69 -0
- package/src/sveltekit/index.ts +74 -0
- package/src/sveltekit/protect.ts +80 -0
- package/src/sveltekit/sveltekit-types.test.ts +31 -0
- package/src/sveltekit/sveltekit.test.ts +799 -0
- package/src/sveltekit/types.ts +134 -0
- package/src/sveltekit/validated.test.ts +117 -0
- package/src/sveltekit/validated.ts +78 -0
- package/src/testing/adapter-conformance.test.ts +408 -0
- package/src/testing/checks.test.ts +124 -0
- package/src/testing/checks.ts +304 -0
- package/src/testing/index.ts +107 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn / Passkeys plugin for fortress.
|
|
3
|
+
*
|
|
4
|
+
* Implements passkey registration, passwordless authentication, and a
|
|
5
|
+
* second-factor mode using `@simplewebauthn/server`. Persists credentials
|
|
6
|
+
* and challenges via the fortress database adapter and exposes the standard
|
|
7
|
+
* begin/finish endpoints when mounted on a framework adapter.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AuthenticationResponseJSON,
|
|
14
|
+
AuthenticatorTransportFuture,
|
|
15
|
+
PublicKeyCredentialCreationOptionsJSON,
|
|
16
|
+
PublicKeyCredentialRequestOptionsJSON,
|
|
17
|
+
RegistrationResponseJSON,
|
|
18
|
+
} from '@simplewebauthn/server';
|
|
19
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
20
|
+
import type { FortressPlugin, PluginRouteContext } from '../../core/plugin';
|
|
21
|
+
import type { AuthResult, FortressUser, RequestMeta } from '../../core/types';
|
|
22
|
+
import {
|
|
23
|
+
generateAuthenticationOptions as generateAuthOptions,
|
|
24
|
+
generateRegistrationOptions as generateRegOptions,
|
|
25
|
+
verifyAuthenticationResponse,
|
|
26
|
+
verifyRegistrationResponse,
|
|
27
|
+
} from '@simplewebauthn/server';
|
|
28
|
+
import { Errors } from '../../core/errors';
|
|
29
|
+
import { bool, endpoint, id, obj, record, str } from '../../core/schema-builder';
|
|
30
|
+
|
|
31
|
+
// ── Config ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface WebAuthnConfig {
|
|
34
|
+
/** Human-readable relying party name shown to users (e.g., "My App") */
|
|
35
|
+
rpName: string;
|
|
36
|
+
/** Relying party identifier -- typically the domain (e.g., "example.com") */
|
|
37
|
+
rpID: string;
|
|
38
|
+
/** Expected origin(s) for the credential (e.g., "https://example.com") */
|
|
39
|
+
origin: string | string[];
|
|
40
|
+
/** Attestation preference. Default: 'none'. */
|
|
41
|
+
attestation?: 'none' | 'indirect' | 'direct' | 'enterprise';
|
|
42
|
+
/** Authenticator selection criteria */
|
|
43
|
+
authenticatorSelection?: {
|
|
44
|
+
authenticatorAttachment?: 'platform' | 'cross-platform';
|
|
45
|
+
residentKey?: 'discouraged' | 'preferred' | 'required';
|
|
46
|
+
userVerification?: 'discouraged' | 'preferred' | 'required';
|
|
47
|
+
};
|
|
48
|
+
/** Timeout for WebAuthn ceremonies in milliseconds. Default: 60000. */
|
|
49
|
+
timeout?: number;
|
|
50
|
+
/** Challenge TTL in seconds. Default: 300 (5 minutes). */
|
|
51
|
+
challengeTTLSeconds?: number;
|
|
52
|
+
/** Issue tokens on authentication verify (passwordless). Default: true. */
|
|
53
|
+
supportPasswordless?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Method Types ────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export interface WebAuthnMethods {
|
|
59
|
+
generateRegistrationOptions: (
|
|
60
|
+
input: Record<string, unknown>,
|
|
61
|
+
ctx: PluginRouteContext,
|
|
62
|
+
) => Promise<{ options: PublicKeyCredentialCreationOptionsJSON }>;
|
|
63
|
+
verifyRegistration: (
|
|
64
|
+
input: { response: RegistrationResponseJSON },
|
|
65
|
+
ctx: PluginRouteContext,
|
|
66
|
+
) => Promise<{
|
|
67
|
+
verified: boolean;
|
|
68
|
+
credentialId: string;
|
|
69
|
+
credentialDeviceType: string;
|
|
70
|
+
credentialBackedUp: boolean;
|
|
71
|
+
}>;
|
|
72
|
+
generateAuthenticationOptions: (input: { userId?: string }) => Promise<{ options: PublicKeyCredentialRequestOptionsJSON }>;
|
|
73
|
+
verifyAuthentication: (
|
|
74
|
+
input: { response: AuthenticationResponseJSON },
|
|
75
|
+
meta?: RequestMeta,
|
|
76
|
+
) => Promise<AuthResult>;
|
|
77
|
+
completeAuthentication: (
|
|
78
|
+
continuationToken: string,
|
|
79
|
+
response: AuthenticationResponseJSON,
|
|
80
|
+
meta?: RequestMeta,
|
|
81
|
+
) => Promise<AuthResult>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Internal Record Types ───────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
interface CredentialRecord {
|
|
87
|
+
id: string;
|
|
88
|
+
userId: string;
|
|
89
|
+
credentialId: string;
|
|
90
|
+
publicKey: string; // base64url-encoded
|
|
91
|
+
counter: number;
|
|
92
|
+
deviceType: string;
|
|
93
|
+
backedUp: boolean;
|
|
94
|
+
transports: string | null; // JSON array
|
|
95
|
+
createdAt: Date;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface ChallengeRecord {
|
|
99
|
+
id: string;
|
|
100
|
+
challenge: string;
|
|
101
|
+
userId: string | null;
|
|
102
|
+
expiresAt: Date;
|
|
103
|
+
createdAt: Date;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const RE_PLUS = /\+/g;
|
|
109
|
+
const RE_SLASH = /\//g;
|
|
110
|
+
const RE_TRAILING_EQ = /=+$/;
|
|
111
|
+
|
|
112
|
+
function base64urlToUint8Array(base64url: string): Uint8Array<ArrayBuffer> {
|
|
113
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
114
|
+
const pad = base64.length % 4;
|
|
115
|
+
const padded = pad ? base64 + '='.repeat(4 - pad) : base64;
|
|
116
|
+
const binary = atob(padded);
|
|
117
|
+
const bytes = new Uint8Array(binary.length);
|
|
118
|
+
for (let i = 0; i < binary.length; i++) {
|
|
119
|
+
bytes[i] = binary.charCodeAt(i);
|
|
120
|
+
}
|
|
121
|
+
return bytes;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function uint8ArrayToBase64url(bytes: Uint8Array): string {
|
|
125
|
+
let binary = '';
|
|
126
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
127
|
+
binary += String.fromCharCode(bytes[i]);
|
|
128
|
+
}
|
|
129
|
+
return btoa(binary).replace(RE_PLUS, '-').replace(RE_SLASH, '_').replace(RE_TRAILING_EQ, '');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Routes ──────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
const webauthnRoutes = {
|
|
135
|
+
generateRegistrationOptions: endpoint('POST', '/webauthn/register/options')
|
|
136
|
+
.summary('Generate WebAuthn registration options')
|
|
137
|
+
.description('Generate public key credential creation options for registering a new passkey against the authenticated caller.')
|
|
138
|
+
.tags('WebAuthn')
|
|
139
|
+
.security('bearer')
|
|
140
|
+
.body(obj({}))
|
|
141
|
+
.response(200, 'Registration options', obj({
|
|
142
|
+
options: record('PublicKeyCredentialCreationOptions JSON'),
|
|
143
|
+
}, 'options'))
|
|
144
|
+
.response(400, 'Bad request')
|
|
145
|
+
.response(401, 'Not authenticated')
|
|
146
|
+
.response(404, 'User not found')
|
|
147
|
+
.handler('generateRegistrationOptions')
|
|
148
|
+
.build(),
|
|
149
|
+
|
|
150
|
+
verifyRegistration: endpoint('POST', '/webauthn/register/verify')
|
|
151
|
+
.summary('Verify WebAuthn registration')
|
|
152
|
+
.description('Verify the registration response from the authenticator and store the new credential for the authenticated caller.')
|
|
153
|
+
.tags('WebAuthn')
|
|
154
|
+
.security('bearer')
|
|
155
|
+
.body(obj({
|
|
156
|
+
response: record('RegistrationResponseJSON from navigator.credentials.create()'),
|
|
157
|
+
}, 'response'))
|
|
158
|
+
.response(200, 'Registration verified', obj({
|
|
159
|
+
verified: bool('Whether registration was successful'),
|
|
160
|
+
credentialId: str('Base64url-encoded credential ID'),
|
|
161
|
+
credentialDeviceType: str('singleDevice or multiDevice'),
|
|
162
|
+
credentialBackedUp: bool('Whether credential is backed up (synced passkey)'),
|
|
163
|
+
}, 'verified'))
|
|
164
|
+
.response(400, 'Verification failed')
|
|
165
|
+
.handler('verifyRegistration')
|
|
166
|
+
.build(),
|
|
167
|
+
|
|
168
|
+
generateAuthenticationOptions: endpoint('POST', '/webauthn/authenticate/options')
|
|
169
|
+
.summary('Generate WebAuthn authentication options')
|
|
170
|
+
.description('Generate public key credential request options. Optionally pass userId for non-discoverable credential flow.')
|
|
171
|
+
.tags('WebAuthn')
|
|
172
|
+
.security('none')
|
|
173
|
+
.body(obj({ userId: id('Optional user ID') }))
|
|
174
|
+
.response(200, 'Authentication options', obj({
|
|
175
|
+
options: record('PublicKeyCredentialRequestOptions JSON'),
|
|
176
|
+
}, 'options'))
|
|
177
|
+
.handler('generateAuthenticationOptions')
|
|
178
|
+
.build(),
|
|
179
|
+
|
|
180
|
+
verifyAuthentication: endpoint('POST', '/webauthn/authenticate/verify')
|
|
181
|
+
.summary('Verify WebAuthn authentication')
|
|
182
|
+
.description('Verify the authentication assertion. Returns tokens if passwordless mode is enabled.')
|
|
183
|
+
.tags('WebAuthn')
|
|
184
|
+
.security('none')
|
|
185
|
+
.body(obj({
|
|
186
|
+
response: record('AuthenticationResponseJSON from navigator.credentials.get()'),
|
|
187
|
+
}, 'response'))
|
|
188
|
+
.response(200, 'Authentication verified', obj({
|
|
189
|
+
verified: bool('Whether authentication was successful'),
|
|
190
|
+
userId: id('Authenticated user ID'),
|
|
191
|
+
accessToken: str('JWT access token (if passwordless)'),
|
|
192
|
+
refreshToken: str('Refresh token (if passwordless)'),
|
|
193
|
+
}, 'verified', 'userId'))
|
|
194
|
+
.response(401, 'Authentication failed')
|
|
195
|
+
.handler('verifyAuthentication')
|
|
196
|
+
.build(),
|
|
197
|
+
} as const;
|
|
198
|
+
|
|
199
|
+
// ── Plugin Factory ──────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* WebAuthn / Passkeys plugin factory. Returns a {@link FortressPlugin} that
|
|
203
|
+
* implements passkey registration, passwordless authentication, and a
|
|
204
|
+
* second-factor mode using `@simplewebauthn/server`.
|
|
205
|
+
*/
|
|
206
|
+
export function webauthn(config: WebAuthnConfig): FortressPlugin & { readonly name: 'webauthn' } {
|
|
207
|
+
const rpName = config.rpName;
|
|
208
|
+
const rpID = config.rpID;
|
|
209
|
+
const origin = config.origin;
|
|
210
|
+
const attestation = config.attestation ?? 'none';
|
|
211
|
+
const timeout = config.timeout ?? 60_000;
|
|
212
|
+
const challengeTTLSeconds = config.challengeTTLSeconds ?? 300;
|
|
213
|
+
const supportPasswordless = config.supportPasswordless ?? true;
|
|
214
|
+
const authenticatorSelection = {
|
|
215
|
+
residentKey: config.authenticatorSelection?.residentKey ?? 'preferred' as const,
|
|
216
|
+
userVerification: config.authenticatorSelection?.userVerification ?? 'preferred' as const,
|
|
217
|
+
...(config.authenticatorSelection?.authenticatorAttachment
|
|
218
|
+
? { authenticatorAttachment: config.authenticatorSelection.authenticatorAttachment }
|
|
219
|
+
: {}),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
async function verifyAuthenticationProof(
|
|
223
|
+
db: DatabaseAdapter,
|
|
224
|
+
response: AuthenticationResponseJSON,
|
|
225
|
+
): Promise<string> {
|
|
226
|
+
const credentialRecord = await db.findOne<CredentialRecord>({
|
|
227
|
+
model: 'webauthn_credential',
|
|
228
|
+
where: [{ field: 'credentialId', operator: '=', value: response.id }],
|
|
229
|
+
});
|
|
230
|
+
if (!credentialRecord)
|
|
231
|
+
throw Errors.unauthorized('Unknown credential');
|
|
232
|
+
|
|
233
|
+
let responseChallenge: string;
|
|
234
|
+
try {
|
|
235
|
+
const clientData = JSON.parse(
|
|
236
|
+
new TextDecoder().decode(base64urlToUint8Array(response.response.clientDataJSON)),
|
|
237
|
+
) as { challenge?: unknown };
|
|
238
|
+
if (typeof clientData.challenge !== 'string' || clientData.challenge.length === 0)
|
|
239
|
+
throw new Error('missing challenge');
|
|
240
|
+
responseChallenge = clientData.challenge;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
throw Errors.unauthorized('Invalid WebAuthn client data');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The challenge value uniquely identifies the ceremony. Looking up by
|
|
247
|
+
// userId (especially NULL for discoverable credentials) is ambiguous when
|
|
248
|
+
// multiple logins are in flight and could select another user's row.
|
|
249
|
+
const challenge = await db.findOne<ChallengeRecord>({
|
|
250
|
+
model: 'webauthn_challenge',
|
|
251
|
+
where: [{ field: 'challenge', operator: '=', value: responseChallenge }],
|
|
252
|
+
});
|
|
253
|
+
if (!challenge || (challenge.userId !== null && challenge.userId !== credentialRecord.userId))
|
|
254
|
+
throw Errors.badRequest('No pending authentication challenge');
|
|
255
|
+
|
|
256
|
+
if (new Date(challenge.expiresAt) < new Date()) {
|
|
257
|
+
await db.delete({
|
|
258
|
+
model: 'webauthn_challenge',
|
|
259
|
+
where: [{ field: 'id', operator: '=', value: challenge.id }],
|
|
260
|
+
});
|
|
261
|
+
throw Errors.badRequest('Challenge expired');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const verification = await verifyAuthenticationResponse({
|
|
265
|
+
response,
|
|
266
|
+
expectedChallenge: challenge.challenge,
|
|
267
|
+
expectedOrigin: origin,
|
|
268
|
+
expectedRPID: rpID,
|
|
269
|
+
credential: {
|
|
270
|
+
id: credentialRecord.credentialId,
|
|
271
|
+
publicKey: base64urlToUint8Array(credentialRecord.publicKey),
|
|
272
|
+
counter: credentialRecord.counter,
|
|
273
|
+
transports: credentialRecord.transports
|
|
274
|
+
? JSON.parse(credentialRecord.transports) as AuthenticatorTransportFuture[]
|
|
275
|
+
: undefined,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
if (!verification.verified)
|
|
279
|
+
throw Errors.unauthorized('Authentication verification failed');
|
|
280
|
+
|
|
281
|
+
const { newCounter } = verification.authenticationInfo;
|
|
282
|
+
if (newCounter > 0 && credentialRecord.counter > 0 && newCounter <= credentialRecord.counter)
|
|
283
|
+
throw Errors.unauthorized('Authenticator counter validation failed');
|
|
284
|
+
|
|
285
|
+
await db.update({
|
|
286
|
+
model: 'webauthn_credential',
|
|
287
|
+
where: [{ field: 'id', operator: '=', value: credentialRecord.id }],
|
|
288
|
+
data: { counter: newCounter },
|
|
289
|
+
});
|
|
290
|
+
await db.delete({
|
|
291
|
+
model: 'webauthn_challenge',
|
|
292
|
+
where: [{ field: 'id', operator: '=', value: challenge.id }],
|
|
293
|
+
});
|
|
294
|
+
return credentialRecord.userId;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
name: 'webauthn',
|
|
299
|
+
|
|
300
|
+
models: [
|
|
301
|
+
{
|
|
302
|
+
name: 'webauthn_credential',
|
|
303
|
+
fields: {
|
|
304
|
+
id: { type: 'number', required: true },
|
|
305
|
+
userId: { type: 'number', required: true, references: { model: 'user', field: 'id' } },
|
|
306
|
+
credentialId: { type: 'string', required: true, unique: true },
|
|
307
|
+
publicKey: { type: 'string', required: true },
|
|
308
|
+
counter: { type: 'number', required: true },
|
|
309
|
+
deviceType: { type: 'string', required: true },
|
|
310
|
+
backedUp: { type: 'boolean', required: true },
|
|
311
|
+
transports: { type: 'string' },
|
|
312
|
+
createdAt: { type: 'date', required: true },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'webauthn_challenge',
|
|
317
|
+
fields: {
|
|
318
|
+
id: { type: 'number', required: true },
|
|
319
|
+
challenge: { type: 'string', required: true, unique: true },
|
|
320
|
+
userId: { type: 'number' },
|
|
321
|
+
expiresAt: { type: 'date', required: true },
|
|
322
|
+
createdAt: { type: 'date', required: true },
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
|
|
327
|
+
routes: webauthnRoutes,
|
|
328
|
+
|
|
329
|
+
hooks: supportPasswordless
|
|
330
|
+
? undefined
|
|
331
|
+
: {
|
|
332
|
+
postAuthGate: {
|
|
333
|
+
reason: 'webauthn',
|
|
334
|
+
async evaluate(ctx) {
|
|
335
|
+
const credential = await ctx.db.findOne<CredentialRecord>({
|
|
336
|
+
model: 'webauthn_credential',
|
|
337
|
+
where: [{ field: 'userId', operator: '=', value: ctx.user.id }],
|
|
338
|
+
});
|
|
339
|
+
return credential ? { pluginData: { requiresWebAuthn: true } } : undefined;
|
|
340
|
+
},
|
|
341
|
+
async verify(ctx, completion) {
|
|
342
|
+
if (!completion || typeof completion !== 'object' || !('response' in completion))
|
|
343
|
+
throw Errors.unauthorized('Invalid WebAuthn completion');
|
|
344
|
+
const userId = await verifyAuthenticationProof(
|
|
345
|
+
ctx.db,
|
|
346
|
+
(completion as { response: AuthenticationResponseJSON }).response,
|
|
347
|
+
);
|
|
348
|
+
if (userId !== ctx.user.id)
|
|
349
|
+
throw Errors.unauthorized('WebAuthn credential belongs to another user');
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
methods: ctx => ({
|
|
355
|
+
async generateRegistrationOptions(
|
|
356
|
+
_input: Record<string, unknown>,
|
|
357
|
+
routeCtx: PluginRouteContext,
|
|
358
|
+
): Promise<{ options: PublicKeyCredentialCreationOptionsJSON }> {
|
|
359
|
+
const userId = routeCtx?.userId;
|
|
360
|
+
if (userId == null)
|
|
361
|
+
throw Errors.unauthorized('User not authenticated');
|
|
362
|
+
|
|
363
|
+
const user = await ctx.db.findOne<FortressUser>({
|
|
364
|
+
model: 'user',
|
|
365
|
+
where: [{ field: 'id', operator: '=', value: userId }],
|
|
366
|
+
});
|
|
367
|
+
if (!user)
|
|
368
|
+
throw Errors.notFound('User not found');
|
|
369
|
+
|
|
370
|
+
// Find existing credentials for excludeCredentials
|
|
371
|
+
const existing = await ctx.db.findMany<CredentialRecord>({
|
|
372
|
+
model: 'webauthn_credential',
|
|
373
|
+
where: [{ field: 'userId', operator: '=', value: userId }],
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Clean up expired challenges for this user
|
|
377
|
+
await ctx.db.delete({
|
|
378
|
+
model: 'webauthn_challenge',
|
|
379
|
+
where: [{ field: 'userId', operator: '=', value: userId }],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const options = await generateRegOptions({
|
|
383
|
+
rpName,
|
|
384
|
+
rpID,
|
|
385
|
+
userName: user.email,
|
|
386
|
+
userDisplayName: user.name,
|
|
387
|
+
userID: new TextEncoder().encode(String(userId)),
|
|
388
|
+
attestationType: attestation === 'indirect' ? 'none' : attestation as 'none' | 'direct' | 'enterprise',
|
|
389
|
+
excludeCredentials: existing.map(cred => ({
|
|
390
|
+
id: cred.credentialId,
|
|
391
|
+
transports: cred.transports
|
|
392
|
+
? JSON.parse(cred.transports) as AuthenticatorTransportFuture[]
|
|
393
|
+
: undefined,
|
|
394
|
+
})),
|
|
395
|
+
authenticatorSelection,
|
|
396
|
+
timeout,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Store challenge
|
|
400
|
+
await ctx.db.create({
|
|
401
|
+
model: 'webauthn_challenge',
|
|
402
|
+
data: {
|
|
403
|
+
challenge: options.challenge,
|
|
404
|
+
userId,
|
|
405
|
+
expiresAt: new Date(Date.now() + challengeTTLSeconds * 1000),
|
|
406
|
+
createdAt: new Date(),
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return { options };
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
async verifyRegistration(
|
|
414
|
+
input: { response: RegistrationResponseJSON },
|
|
415
|
+
routeCtx: PluginRouteContext,
|
|
416
|
+
): Promise<{
|
|
417
|
+
verified: boolean;
|
|
418
|
+
credentialId: string;
|
|
419
|
+
credentialDeviceType: string;
|
|
420
|
+
credentialBackedUp: boolean;
|
|
421
|
+
}> {
|
|
422
|
+
const userId = routeCtx?.userId;
|
|
423
|
+
if (userId == null)
|
|
424
|
+
throw Errors.unauthorized('User not authenticated');
|
|
425
|
+
const { response } = input;
|
|
426
|
+
|
|
427
|
+
// Find the pending challenge
|
|
428
|
+
const challengeRecord = await ctx.db.findOne<ChallengeRecord>({
|
|
429
|
+
model: 'webauthn_challenge',
|
|
430
|
+
where: [{ field: 'userId', operator: '=', value: userId }],
|
|
431
|
+
});
|
|
432
|
+
if (!challengeRecord)
|
|
433
|
+
throw Errors.badRequest('No pending registration challenge');
|
|
434
|
+
|
|
435
|
+
if (new Date(challengeRecord.expiresAt) < new Date()) {
|
|
436
|
+
await ctx.db.delete({
|
|
437
|
+
model: 'webauthn_challenge',
|
|
438
|
+
where: [{ field: 'id', operator: '=', value: challengeRecord.id }],
|
|
439
|
+
});
|
|
440
|
+
throw Errors.badRequest('Challenge expired');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const verification = await verifyRegistrationResponse({
|
|
444
|
+
response,
|
|
445
|
+
expectedChallenge: challengeRecord.challenge,
|
|
446
|
+
expectedOrigin: origin,
|
|
447
|
+
expectedRPID: rpID,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
451
|
+
throw Errors.badRequest('Registration verification failed');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
|
455
|
+
|
|
456
|
+
// Store the credential
|
|
457
|
+
await ctx.db.create({
|
|
458
|
+
model: 'webauthn_credential',
|
|
459
|
+
data: {
|
|
460
|
+
userId,
|
|
461
|
+
credentialId: credential.id,
|
|
462
|
+
publicKey: uint8ArrayToBase64url(credential.publicKey),
|
|
463
|
+
counter: credential.counter,
|
|
464
|
+
deviceType: credentialDeviceType,
|
|
465
|
+
backedUp: credentialBackedUp,
|
|
466
|
+
transports: credential.transports ? JSON.stringify(credential.transports) : null,
|
|
467
|
+
createdAt: new Date(),
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Delete used challenge
|
|
472
|
+
await ctx.db.delete({
|
|
473
|
+
model: 'webauthn_challenge',
|
|
474
|
+
where: [{ field: 'id', operator: '=', value: challengeRecord.id }],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
verified: true,
|
|
479
|
+
credentialId: credential.id,
|
|
480
|
+
credentialDeviceType,
|
|
481
|
+
credentialBackedUp,
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
async generateAuthenticationOptions(input: { userId?: string }): Promise<{ options: PublicKeyCredentialRequestOptionsJSON }> {
|
|
486
|
+
const { userId } = input;
|
|
487
|
+
|
|
488
|
+
let allowCredentials: { id: string; transports?: AuthenticatorTransportFuture[] }[] | undefined;
|
|
489
|
+
|
|
490
|
+
if (userId) {
|
|
491
|
+
// Clean up old challenges for this user
|
|
492
|
+
await ctx.db.delete({
|
|
493
|
+
model: 'webauthn_challenge',
|
|
494
|
+
where: [{ field: 'userId', operator: '=', value: userId }],
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const credentials = await ctx.db.findMany<CredentialRecord>({
|
|
498
|
+
model: 'webauthn_credential',
|
|
499
|
+
where: [{ field: 'userId', operator: '=', value: userId }],
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
allowCredentials = credentials.map(cred => ({
|
|
503
|
+
id: cred.credentialId,
|
|
504
|
+
transports: cred.transports
|
|
505
|
+
? JSON.parse(cred.transports) as AuthenticatorTransportFuture[]
|
|
506
|
+
: undefined,
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const options = await generateAuthOptions({
|
|
511
|
+
rpID,
|
|
512
|
+
allowCredentials,
|
|
513
|
+
userVerification: authenticatorSelection.userVerification,
|
|
514
|
+
timeout,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Store challenge
|
|
518
|
+
await ctx.db.create({
|
|
519
|
+
model: 'webauthn_challenge',
|
|
520
|
+
data: {
|
|
521
|
+
challenge: options.challenge,
|
|
522
|
+
userId: userId ?? null,
|
|
523
|
+
expiresAt: new Date(Date.now() + challengeTTLSeconds * 1000),
|
|
524
|
+
createdAt: new Date(),
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
return { options };
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
async completeAuthentication(
|
|
532
|
+
continuationToken: string,
|
|
533
|
+
response: AuthenticationResponseJSON,
|
|
534
|
+
meta?: RequestMeta,
|
|
535
|
+
): Promise<AuthResult> {
|
|
536
|
+
if (!ctx.auth)
|
|
537
|
+
throw Errors.badRequest('Auth service is unavailable');
|
|
538
|
+
return ctx.auth.completePendingAuth(continuationToken, { response }, meta);
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
async verifyAuthentication(
|
|
542
|
+
input: { response: AuthenticationResponseJSON },
|
|
543
|
+
meta?: RequestMeta,
|
|
544
|
+
): Promise<AuthResult> {
|
|
545
|
+
const userId = await verifyAuthenticationProof(ctx.db, input.response);
|
|
546
|
+
if (!supportPasswordless)
|
|
547
|
+
throw Errors.badRequest('Use completeAuthentication for second-factor WebAuthn');
|
|
548
|
+
if (!ctx.auth)
|
|
549
|
+
throw Errors.badRequest('Auth service is unavailable');
|
|
550
|
+
return ctx.auth.completePluginAuth(userId, 'webauthn', meta);
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
553
|
+
};
|
|
554
|
+
}
|