@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,472 @@
|
|
|
1
|
+
import type { Fortress } from '../../core/fortress';
|
|
2
|
+
import type { PluginRouteContext } from '../../core/plugin';
|
|
3
|
+
import type { WebAuthnMethods } from './index';
|
|
4
|
+
import { Buffer } from 'node:buffer';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { createFortress } from '../../core/fortress';
|
|
7
|
+
import { createTestAdapter } from '../../testing';
|
|
8
|
+
import { webauthn } from './index';
|
|
9
|
+
|
|
10
|
+
// Minimal PluginRouteContext for tests that invoke plugin methods directly.
|
|
11
|
+
// The dispatcher constructs the real ctx for HTTP calls; unit tests stub
|
|
12
|
+
// just the userId (and a placeholder Request) since the handlers only read
|
|
13
|
+
// routeCtx.userId.
|
|
14
|
+
function httpCtx(uid: string | undefined): PluginRouteContext {
|
|
15
|
+
return {
|
|
16
|
+
userId: uid,
|
|
17
|
+
claims: undefined,
|
|
18
|
+
meta: undefined,
|
|
19
|
+
request: new Request('http://localhost/webauthn/register'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SECRET = 'webauthn-test-secret-at-least-32chars';
|
|
24
|
+
const MOCK_CHALLENGE = 'dGVzdC1jaGFsbGVuZ2U'; // base64url of "test-challenge"
|
|
25
|
+
const MOCK_CREDENTIAL_ID = 'Y3JlZGVudGlhbC1pZA'; // base64url
|
|
26
|
+
const MOCK_PUBLIC_KEY = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
27
|
+
const MOCK_CLIENT_DATA_JSON = Buffer.from(JSON.stringify({
|
|
28
|
+
type: 'webauthn.get',
|
|
29
|
+
challenge: MOCK_CHALLENGE,
|
|
30
|
+
origin: 'http://localhost:3000',
|
|
31
|
+
})).toString('base64url');
|
|
32
|
+
|
|
33
|
+
// Mock @simplewebauthn/server
|
|
34
|
+
vi.mock('@simplewebauthn/server', () => ({
|
|
35
|
+
generateRegistrationOptions: vi.fn(async () => ({
|
|
36
|
+
challenge: MOCK_CHALLENGE,
|
|
37
|
+
rp: { name: 'Test', id: 'localhost' },
|
|
38
|
+
user: { id: 'MQ', name: 'alice@example.com', displayName: 'Alice' },
|
|
39
|
+
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
|
|
40
|
+
timeout: 60000,
|
|
41
|
+
attestation: 'none',
|
|
42
|
+
excludeCredentials: [],
|
|
43
|
+
authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred' },
|
|
44
|
+
})),
|
|
45
|
+
verifyRegistrationResponse: vi.fn(async () => ({
|
|
46
|
+
verified: true,
|
|
47
|
+
registrationInfo: {
|
|
48
|
+
fmt: 'none',
|
|
49
|
+
aaguid: '00000000-0000-0000-0000-000000000000',
|
|
50
|
+
credential: {
|
|
51
|
+
id: MOCK_CREDENTIAL_ID,
|
|
52
|
+
publicKey: MOCK_PUBLIC_KEY,
|
|
53
|
+
counter: 0,
|
|
54
|
+
transports: ['internal'],
|
|
55
|
+
},
|
|
56
|
+
credentialType: 'public-key',
|
|
57
|
+
credentialDeviceType: 'multiDevice',
|
|
58
|
+
credentialBackedUp: true,
|
|
59
|
+
userVerified: true,
|
|
60
|
+
origin: 'http://localhost:3000',
|
|
61
|
+
},
|
|
62
|
+
})),
|
|
63
|
+
generateAuthenticationOptions: vi.fn(async () => ({
|
|
64
|
+
challenge: MOCK_CHALLENGE,
|
|
65
|
+
rpId: 'localhost',
|
|
66
|
+
timeout: 60000,
|
|
67
|
+
userVerification: 'preferred',
|
|
68
|
+
allowCredentials: [],
|
|
69
|
+
})),
|
|
70
|
+
verifyAuthenticationResponse: vi.fn(async () => ({
|
|
71
|
+
verified: true,
|
|
72
|
+
authenticationInfo: {
|
|
73
|
+
credentialID: MOCK_CREDENTIAL_ID,
|
|
74
|
+
newCounter: 1,
|
|
75
|
+
userVerified: true,
|
|
76
|
+
credentialDeviceType: 'multiDevice',
|
|
77
|
+
credentialBackedUp: true,
|
|
78
|
+
origin: 'http://localhost:3000',
|
|
79
|
+
rpID: 'localhost',
|
|
80
|
+
},
|
|
81
|
+
})),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const simplewebauthn = await import('@simplewebauthn/server') as unknown as {
|
|
85
|
+
[K in keyof Awaited<typeof import('@simplewebauthn/server')>]:
|
|
86
|
+
import('vitest').Mock;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
describe('webauthn plugin', () => {
|
|
90
|
+
let fortress: Fortress<any>;
|
|
91
|
+
let methods: WebAuthnMethods;
|
|
92
|
+
let userId: string;
|
|
93
|
+
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
vi.clearAllMocks();
|
|
96
|
+
|
|
97
|
+
fortress = createFortress({
|
|
98
|
+
jwt: { key: SECRET },
|
|
99
|
+
database: createTestAdapter(),
|
|
100
|
+
plugins: [webauthn({ rpName: 'Test', rpID: 'localhost', origin: 'http://localhost:3000' })],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
methods = fortress.plugins.webauthn as unknown as WebAuthnMethods;
|
|
104
|
+
|
|
105
|
+
const user = await fortress.auth.createUser({
|
|
106
|
+
email: 'alice@example.com',
|
|
107
|
+
name: 'Alice',
|
|
108
|
+
password: 'password-123456',
|
|
109
|
+
});
|
|
110
|
+
userId = user.id;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('generateRegistrationOptions', () => {
|
|
114
|
+
it('returns registration options', async () => {
|
|
115
|
+
const result = await methods.generateRegistrationOptions({}, httpCtx(userId));
|
|
116
|
+
|
|
117
|
+
expect(result.options).toBeDefined();
|
|
118
|
+
expect(result.options.challenge).toBe(MOCK_CHALLENGE);
|
|
119
|
+
expect(simplewebauthn.generateRegistrationOptions).toHaveBeenCalledOnce();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('throws if user not found', async () => {
|
|
123
|
+
await expect(methods.generateRegistrationOptions({}, httpCtx('9999')))
|
|
124
|
+
.rejects
|
|
125
|
+
.toThrow('User not found');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('throws UNAUTHORIZED when ctx.userId is missing', async () => {
|
|
129
|
+
await expect(methods.generateRegistrationOptions({}, httpCtx(undefined)))
|
|
130
|
+
.rejects
|
|
131
|
+
.toThrow('User not authenticated');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('passes excludeCredentials for existing credentials', async () => {
|
|
135
|
+
// Register a credential first
|
|
136
|
+
await methods.generateRegistrationOptions({}, httpCtx(userId));
|
|
137
|
+
await methods.verifyRegistration(
|
|
138
|
+
{ response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any },
|
|
139
|
+
httpCtx(userId),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Generate options again - should exclude the existing credential
|
|
143
|
+
(simplewebauthn.generateRegistrationOptions).mockClear();
|
|
144
|
+
await methods.generateRegistrationOptions({}, httpCtx(userId));
|
|
145
|
+
|
|
146
|
+
const call = (simplewebauthn.generateRegistrationOptions).mock.calls[0][0];
|
|
147
|
+
expect(call.excludeCredentials).toHaveLength(1);
|
|
148
|
+
expect(call.excludeCredentials![0].id).toBe(MOCK_CREDENTIAL_ID);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('verifyRegistration', () => {
|
|
153
|
+
it('stores credential on successful verification', async () => {
|
|
154
|
+
await methods.generateRegistrationOptions({}, httpCtx(userId));
|
|
155
|
+
|
|
156
|
+
const result = await methods.verifyRegistration(
|
|
157
|
+
{ response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any },
|
|
158
|
+
httpCtx(userId),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(result.verified).toBe(true);
|
|
162
|
+
expect(result.credentialId).toBe(MOCK_CREDENTIAL_ID);
|
|
163
|
+
expect(result.credentialDeviceType).toBe('multiDevice');
|
|
164
|
+
expect(result.credentialBackedUp).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('throws when no pending challenge', async () => {
|
|
168
|
+
await expect(methods.verifyRegistration(
|
|
169
|
+
{ response: {} as any },
|
|
170
|
+
httpCtx(userId),
|
|
171
|
+
)).rejects.toThrow('No pending registration challenge');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('throws UNAUTHORIZED when ctx.userId is missing', async () => {
|
|
175
|
+
await expect(methods.verifyRegistration(
|
|
176
|
+
{ response: {} as any },
|
|
177
|
+
httpCtx(undefined),
|
|
178
|
+
)).rejects.toThrow('User not authenticated');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('throws on failed verification', async () => {
|
|
182
|
+
await methods.generateRegistrationOptions({}, httpCtx(userId));
|
|
183
|
+
|
|
184
|
+
(simplewebauthn.verifyRegistrationResponse).mockResolvedValueOnce({
|
|
185
|
+
verified: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await expect(methods.verifyRegistration(
|
|
189
|
+
{ response: {} as any },
|
|
190
|
+
httpCtx(userId),
|
|
191
|
+
)).rejects.toThrow('Registration verification failed');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('generateAuthenticationOptions', () => {
|
|
196
|
+
it('returns options with allowCredentials when userId provided', async () => {
|
|
197
|
+
// Register a credential first
|
|
198
|
+
await methods.generateRegistrationOptions({}, httpCtx(userId));
|
|
199
|
+
await methods.verifyRegistration(
|
|
200
|
+
{ response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any },
|
|
201
|
+
httpCtx(userId),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
(simplewebauthn.generateAuthenticationOptions).mockClear();
|
|
205
|
+
await methods.generateAuthenticationOptions({ userId });
|
|
206
|
+
|
|
207
|
+
const call = (simplewebauthn.generateAuthenticationOptions).mock.calls[0][0];
|
|
208
|
+
expect(call.allowCredentials).toHaveLength(1);
|
|
209
|
+
expect(call.allowCredentials![0].id).toBe(MOCK_CREDENTIAL_ID);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns options without allowCredentials when no userId (discoverable)', async () => {
|
|
213
|
+
(simplewebauthn.generateAuthenticationOptions).mockClear();
|
|
214
|
+
await methods.generateAuthenticationOptions({});
|
|
215
|
+
|
|
216
|
+
const call = (simplewebauthn.generateAuthenticationOptions).mock.calls[0][0];
|
|
217
|
+
expect(call.allowCredentials).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('verifyAuthentication', () => {
|
|
222
|
+
async function registerAndPrepareAuth(): Promise<void> {
|
|
223
|
+
// Register credential
|
|
224
|
+
await methods.generateRegistrationOptions({}, httpCtx(userId));
|
|
225
|
+
await methods.verifyRegistration(
|
|
226
|
+
{ response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any },
|
|
227
|
+
httpCtx(userId),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Generate auth options
|
|
231
|
+
await methods.generateAuthenticationOptions({ userId });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
it('verifies and returns a full auth result', async () => {
|
|
235
|
+
await registerAndPrepareAuth();
|
|
236
|
+
|
|
237
|
+
const result = await methods.verifyAuthentication({
|
|
238
|
+
response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(result.status).toBe('success');
|
|
242
|
+
expect(result.user.id).toBe(userId);
|
|
243
|
+
expect(result.status === 'success' ? result.method : null).toBe('webauthn');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('issues access token when passwordless', async () => {
|
|
247
|
+
await registerAndPrepareAuth();
|
|
248
|
+
|
|
249
|
+
const result = await methods.verifyAuthentication({
|
|
250
|
+
response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (result.status !== 'success')
|
|
254
|
+
throw new Error('Expected successful passwordless login');
|
|
255
|
+
expect(result.accessToken).toBeTruthy();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('runs configured gates before passwordless token issuance', async () => {
|
|
259
|
+
const database = createTestAdapter();
|
|
260
|
+
const gated = createFortress({
|
|
261
|
+
jwt: { key: SECRET },
|
|
262
|
+
database,
|
|
263
|
+
plugins: [
|
|
264
|
+
webauthn({ rpName: 'Test', rpID: 'localhost', origin: 'http://localhost:3000' }),
|
|
265
|
+
{
|
|
266
|
+
name: 'factor',
|
|
267
|
+
hooks: {
|
|
268
|
+
postAuthGate: {
|
|
269
|
+
reason: 'two-factor',
|
|
270
|
+
evaluate: async () => ({ pluginData: { requires2FA: true } }),
|
|
271
|
+
verify: async () => {},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
const gatedMethods = gated.plugins.webauthn as unknown as WebAuthnMethods;
|
|
278
|
+
const user = await gated.auth.createUser({
|
|
279
|
+
email: 'passwordless-gated@example.com',
|
|
280
|
+
name: 'Passwordless Gated',
|
|
281
|
+
password: 'password-123456',
|
|
282
|
+
});
|
|
283
|
+
await gatedMethods.generateRegistrationOptions({}, httpCtx(user.id));
|
|
284
|
+
await gatedMethods.verifyRegistration(
|
|
285
|
+
{ response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any },
|
|
286
|
+
httpCtx(user.id),
|
|
287
|
+
);
|
|
288
|
+
await gatedMethods.generateAuthenticationOptions({ userId: user.id });
|
|
289
|
+
|
|
290
|
+
const result = await gatedMethods.verifyAuthentication({
|
|
291
|
+
response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any,
|
|
292
|
+
});
|
|
293
|
+
expect(result.status).toBe('pending');
|
|
294
|
+
expect(result.status === 'pending' ? result.pending?.reason : undefined).toBe('two-factor');
|
|
295
|
+
expect(await database.count({ model: 'refresh_token' })).toBe(0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('throws on unknown credential', async () => {
|
|
299
|
+
await methods.generateAuthenticationOptions({});
|
|
300
|
+
|
|
301
|
+
await expect(methods.verifyAuthentication({
|
|
302
|
+
response: { id: 'unknown-id', rawId: 'unknown-id', response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any,
|
|
303
|
+
})).rejects.toThrow('Unknown credential');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('throws on failed verification', async () => {
|
|
307
|
+
await registerAndPrepareAuth();
|
|
308
|
+
|
|
309
|
+
(simplewebauthn.verifyAuthenticationResponse).mockResolvedValueOnce({
|
|
310
|
+
verified: false,
|
|
311
|
+
authenticationInfo: {
|
|
312
|
+
credentialID: MOCK_CREDENTIAL_ID,
|
|
313
|
+
newCounter: 1,
|
|
314
|
+
userVerified: false,
|
|
315
|
+
credentialDeviceType: 'multiDevice',
|
|
316
|
+
credentialBackedUp: true,
|
|
317
|
+
origin: 'http://localhost:3000',
|
|
318
|
+
rpID: 'localhost',
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await expect(methods.verifyAuthentication({
|
|
323
|
+
response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any,
|
|
324
|
+
})).rejects.toThrow('Authentication verification failed');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('detects counter rollback (clone detection)', async () => {
|
|
328
|
+
await registerAndPrepareAuth();
|
|
329
|
+
|
|
330
|
+
// First authentication: counter goes to 5
|
|
331
|
+
(simplewebauthn.verifyAuthenticationResponse).mockResolvedValueOnce({
|
|
332
|
+
verified: true,
|
|
333
|
+
authenticationInfo: {
|
|
334
|
+
credentialID: MOCK_CREDENTIAL_ID,
|
|
335
|
+
newCounter: 5,
|
|
336
|
+
userVerified: true,
|
|
337
|
+
credentialDeviceType: 'multiDevice',
|
|
338
|
+
credentialBackedUp: true,
|
|
339
|
+
origin: 'http://localhost:3000',
|
|
340
|
+
rpID: 'localhost',
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await methods.verifyAuthentication({
|
|
345
|
+
response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Second auth: counter rolls back to 3 (clone!)
|
|
349
|
+
await methods.generateAuthenticationOptions({ userId });
|
|
350
|
+
|
|
351
|
+
(simplewebauthn.verifyAuthenticationResponse).mockResolvedValueOnce({
|
|
352
|
+
verified: true,
|
|
353
|
+
authenticationInfo: {
|
|
354
|
+
credentialID: MOCK_CREDENTIAL_ID,
|
|
355
|
+
newCounter: 3,
|
|
356
|
+
userVerified: true,
|
|
357
|
+
credentialDeviceType: 'multiDevice',
|
|
358
|
+
credentialBackedUp: true,
|
|
359
|
+
origin: 'http://localhost:3000',
|
|
360
|
+
rpID: 'localhost',
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await expect(methods.verifyAuthentication({
|
|
365
|
+
response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any,
|
|
366
|
+
})).rejects.toThrow('counter validation failed');
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe('post-auth gate (second-factor mode)', () => {
|
|
371
|
+
it('holds token issuance when supportPasswordless is false', async () => {
|
|
372
|
+
const database = createTestAdapter();
|
|
373
|
+
const f = createFortress({
|
|
374
|
+
jwt: { key: SECRET },
|
|
375
|
+
database,
|
|
376
|
+
plugins: [webauthn({ rpName: 'Test', rpID: 'localhost', origin: 'http://localhost:3000', supportPasswordless: false })],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const m = f.plugins.webauthn as unknown as WebAuthnMethods;
|
|
380
|
+
|
|
381
|
+
const user = await f.auth.createUser({
|
|
382
|
+
email: 'bob@example.com',
|
|
383
|
+
name: 'Bob',
|
|
384
|
+
password: 'password-123456',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Register a credential
|
|
388
|
+
await m.generateRegistrationOptions({}, httpCtx(user.id));
|
|
389
|
+
await m.verifyRegistration(
|
|
390
|
+
{ response: { id: MOCK_CREDENTIAL_ID, rawId: MOCK_CREDENTIAL_ID, response: { clientDataJSON: MOCK_CLIENT_DATA_JSON }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any },
|
|
391
|
+
httpCtx(user.id),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Login should be intercepted
|
|
395
|
+
const result = await f.auth.login('bob@example.com', 'password-123456');
|
|
396
|
+
expect('accessToken' in result).toBe(false);
|
|
397
|
+
expect(result.pluginData?.requiresWebAuthn).toBe(true);
|
|
398
|
+
expect(result.status === 'pending' ? result.pending?.reason : undefined).toBe('webauthn');
|
|
399
|
+
expect(await database.count({ model: 'refresh_token' })).toBe(0);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('allows normal login when no credentials registered', async () => {
|
|
403
|
+
const f = createFortress({
|
|
404
|
+
jwt: { key: SECRET },
|
|
405
|
+
database: createTestAdapter(),
|
|
406
|
+
plugins: [webauthn({ rpName: 'Test', rpID: 'localhost', origin: 'http://localhost:3000', supportPasswordless: false })],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await f.auth.createUser({
|
|
410
|
+
email: 'bob@example.com',
|
|
411
|
+
name: 'Bob',
|
|
412
|
+
password: 'password-123456',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const result = await f.auth.login('bob@example.com', 'password-123456');
|
|
416
|
+
if (result.status !== 'success')
|
|
417
|
+
throw new Error('Expected successful login');
|
|
418
|
+
expect(result.accessToken).toBeTruthy();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('http dispatch (privilege-escalation regression)', () => {
|
|
423
|
+
// Sanity guard: a caller hitting POST /webauthn/register/options with a
|
|
424
|
+
// valid bearer token for user A must register a credential against user A,
|
|
425
|
+
// even if the request body tries to smuggle user B's id. Verified by
|
|
426
|
+
// inspecting which userId ends up stamped on the stored challenge row.
|
|
427
|
+
it('uses the authenticated caller id, ignoring any body userId', async () => {
|
|
428
|
+
// Create two users; alice is the attacker, bob is the victim.
|
|
429
|
+
const alice = await fortress.auth.createUser({
|
|
430
|
+
email: 'attacker@example.com',
|
|
431
|
+
name: 'Alice',
|
|
432
|
+
password: 'password-123456',
|
|
433
|
+
});
|
|
434
|
+
const bob = await fortress.auth.createUser({
|
|
435
|
+
email: 'victim@example.com',
|
|
436
|
+
name: 'Bob',
|
|
437
|
+
password: 'password-123456',
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const login = await fortress.auth.login('attacker@example.com', 'password-123456');
|
|
441
|
+
if (login.status !== 'success')
|
|
442
|
+
throw new Error('expected login success');
|
|
443
|
+
|
|
444
|
+
// Attempt: call /webauthn/register/options with alice's bearer token but
|
|
445
|
+
// a body claiming to register for bob. The dispatcher should ignore the
|
|
446
|
+
// body field entirely (it's not even in the schema now) and stamp the
|
|
447
|
+
// challenge against alice.
|
|
448
|
+
const res = await fortress.handleRequest(new Request('http://localhost/webauthn/register/options', {
|
|
449
|
+
method: 'POST',
|
|
450
|
+
headers: {
|
|
451
|
+
'authorization': `Bearer ${login.accessToken}`,
|
|
452
|
+
'content-type': 'application/json',
|
|
453
|
+
},
|
|
454
|
+
body: JSON.stringify({ userId: bob.id }),
|
|
455
|
+
}));
|
|
456
|
+
expect(res.status).toBe(200);
|
|
457
|
+
|
|
458
|
+
// The stored challenge row must belong to alice, never bob.
|
|
459
|
+
const adapter = fortress.config.database;
|
|
460
|
+
const aliceChallenge = await adapter.findOne<{ userId: string | null }>({
|
|
461
|
+
model: 'webauthn_challenge',
|
|
462
|
+
where: [{ field: 'userId', operator: '=', value: alice.id }],
|
|
463
|
+
});
|
|
464
|
+
const bobChallenge = await adapter.findOne<{ userId: string | null }>({
|
|
465
|
+
model: 'webauthn_challenge',
|
|
466
|
+
where: [{ field: 'userId', operator: '=', value: bob.id }],
|
|
467
|
+
});
|
|
468
|
+
expect(aliceChallenge).not.toBeNull();
|
|
469
|
+
expect(bobChallenge).toBeNull();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in auth events. These go through the same registry + emit path as
|
|
3
|
+
* user-defined events; the plugin merges them in by default and consumers can
|
|
4
|
+
* exclude individual ones via `builtinEvents({ exclude })`.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { WebhookEventDeclaration } from './types';
|
|
10
|
+
|
|
11
|
+
const BUILTIN: readonly WebhookEventDeclaration[] = [
|
|
12
|
+
{ name: 'auth.login.success', source: 'afterLogin', description: 'A user authenticated successfully.' },
|
|
13
|
+
{ name: 'auth.login.failure', source: 'onLoginFailure', description: 'A login attempt failed.' },
|
|
14
|
+
{ name: 'auth.logout', source: 'beforeLogout', description: 'A user logged out.' },
|
|
15
|
+
{ name: 'auth.user.registered', source: 'afterRegister', description: 'A new user was registered.' },
|
|
16
|
+
{ name: 'auth.token.refreshed', source: 'afterTokenRefresh', description: 'An access token was refreshed.' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** Names of the built-in auth events (for collision checks). */
|
|
20
|
+
export const BUILTIN_EVENT_NAMES: ReadonlySet<string> = new Set(BUILTIN.map(e => e.name));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The built-in auth event declarations. Pass `{ exclude: [...] }` to drop
|
|
24
|
+
* individual ones (their auto-emit hook then becomes a no-op).
|
|
25
|
+
*/
|
|
26
|
+
export function builtinEvents(opts: { exclude?: string[] } = {}): WebhookEventDeclaration[] {
|
|
27
|
+
const all = BUILTIN.map(e => ({ ...e }));
|
|
28
|
+
return opts.exclude ? all.filter(e => !opts.exclude!.includes(e.name)) : all;
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ssrfSafeFetch } from './delivery';
|
|
3
|
+
|
|
4
|
+
// The SSRF guard MUST live inside the delivery transport: validation and the
|
|
5
|
+
// pinned connection use the same resolved IP, so there is no rebinding window.
|
|
6
|
+
// These assert the guard runs through the real `FetchFn` (the path fetcher
|
|
7
|
+
// uses), rejecting before any connection is made.
|
|
8
|
+
describe('ssrfSafeFetch — SSRF guard in the delivery transport', () => {
|
|
9
|
+
const fetchFn = ssrfSafeFetch();
|
|
10
|
+
|
|
11
|
+
it('rejects non-https targets', async () => {
|
|
12
|
+
await expect(
|
|
13
|
+
fetchFn(new Request('http://example.com/hook', { method: 'POST' })),
|
|
14
|
+
).rejects.toThrow(/https/i);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('rejects the cloud metadata IP', async () => {
|
|
18
|
+
await expect(
|
|
19
|
+
fetchFn(new Request('https://169.254.169.254/hook', { method: 'POST' })),
|
|
20
|
+
).rejects.toThrow(/private/i);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects loopback', async () => {
|
|
24
|
+
await expect(
|
|
25
|
+
fetchFn(new Request('https://127.0.0.1/hook', { method: 'POST' })),
|
|
26
|
+
).rejects.toThrow(/private/i);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('rejects IPv4-mapped IPv6 metadata targets', async () => {
|
|
30
|
+
await expect(
|
|
31
|
+
fetchFn(new Request('https://[::ffff:169.254.169.254]/hook', { method: 'POST' })),
|
|
32
|
+
).rejects.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('rejects NAT64 well-known-prefix (64:ff9b::/96) targets that embed private IPv4', async () => {
|
|
36
|
+
// 64:ff9b::a9fe:a9fe == NAT64 form of 169.254.169.254 (cloud metadata)
|
|
37
|
+
await expect(
|
|
38
|
+
fetchFn(new Request('https://[64:ff9b::a9fe:a9fe]/hook', { method: 'POST' })),
|
|
39
|
+
).rejects.toThrow(/private/i);
|
|
40
|
+
// 64:ff9b::7f00:1 == NAT64 form of 127.0.0.1 (loopback)
|
|
41
|
+
await expect(
|
|
42
|
+
fetchFn(new Request('https://[64:ff9b::7f00:1]/hook', { method: 'POST' })),
|
|
43
|
+
).rejects.toThrow(/private/i);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('rejects localhost by name', async () => {
|
|
47
|
+
await expect(
|
|
48
|
+
fetchFn(new Request('https://localhost/hook', { method: 'POST' })),
|
|
49
|
+
).rejects.toThrow(/not allowed/i);
|
|
50
|
+
});
|
|
51
|
+
});
|