@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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email verification plugin for fortress.
|
|
3
|
+
*
|
|
4
|
+
* Issues hashed, time-limited verification tokens, exposes
|
|
5
|
+
* `requestVerification` and `verifyEmail` methods on the fortress instance,
|
|
6
|
+
* and adds the corresponding HTTP routes when mounted via a framework adapter.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
12
|
+
import type { FortressPlugin } from '../../core/plugin';
|
|
13
|
+
import type { AuthResult, FortressUser, RequestMeta } from '../../core/types';
|
|
14
|
+
import { generateRefreshToken, hashToken } from '../../core/auth/refresh-token';
|
|
15
|
+
import { Errors } from '../../core/errors';
|
|
16
|
+
|
|
17
|
+
export interface EmailVerificationConfig {
|
|
18
|
+
/** Token expiry in seconds (default: 86400 = 24h) */
|
|
19
|
+
tokenExpirySeconds?: number;
|
|
20
|
+
/** Block login for unverified users (default: true) */
|
|
21
|
+
requireVerification?: boolean;
|
|
22
|
+
/** Called when a verification token is created */
|
|
23
|
+
onSendVerification?: (email: string, token: string, userId: string) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface VerificationTokenRecord {
|
|
27
|
+
id: string;
|
|
28
|
+
userId: string;
|
|
29
|
+
token: string;
|
|
30
|
+
email: string;
|
|
31
|
+
expiresAt: Date;
|
|
32
|
+
usedAt: Date | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EmailVerificationMethods {
|
|
36
|
+
sendVerification: (userId: string, email?: string) => Promise<{ sent: true }>;
|
|
37
|
+
verify: (rawToken: string) => Promise<{ userId: string; email: string }>;
|
|
38
|
+
completeVerification: (
|
|
39
|
+
continuationToken: string,
|
|
40
|
+
verificationToken: string,
|
|
41
|
+
meta?: RequestMeta,
|
|
42
|
+
) => Promise<AuthResult>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Email verification plugin factory. Returns a {@link FortressPlugin} that
|
|
46
|
+
* issues hashed verification tokens, exposes `requestVerification` /
|
|
47
|
+
* `verifyEmail` methods, and (when mounted) the corresponding HTTP routes.
|
|
48
|
+
*/
|
|
49
|
+
export function emailVerification(config: EmailVerificationConfig = {}): FortressPlugin & { readonly name: 'email-verification' } {
|
|
50
|
+
const tokenExpirySeconds = config.tokenExpirySeconds ?? 86400;
|
|
51
|
+
const requireVerification = config.requireVerification ?? true;
|
|
52
|
+
|
|
53
|
+
async function consumeVerificationToken(
|
|
54
|
+
db: DatabaseAdapter,
|
|
55
|
+
rawToken: string,
|
|
56
|
+
expectedUserId?: string,
|
|
57
|
+
): Promise<VerificationTokenRecord> {
|
|
58
|
+
const hash = await hashToken(rawToken);
|
|
59
|
+
const usedAt = new Date();
|
|
60
|
+
const record = await db.update<VerificationTokenRecord>({
|
|
61
|
+
model: 'email_verification_token',
|
|
62
|
+
where: [
|
|
63
|
+
{ field: 'token', operator: '=', value: hash },
|
|
64
|
+
{ field: 'usedAt', operator: 'isNull', value: null },
|
|
65
|
+
{ field: 'expiresAt', operator: 'gt', value: usedAt },
|
|
66
|
+
...(expectedUserId ? [{ field: 'userId', operator: '=' as const, value: expectedUserId }] : []),
|
|
67
|
+
],
|
|
68
|
+
data: { usedAt },
|
|
69
|
+
});
|
|
70
|
+
if (!record)
|
|
71
|
+
throw Errors.notFound('Invalid or expired verification token');
|
|
72
|
+
|
|
73
|
+
await db.update({
|
|
74
|
+
model: 'user',
|
|
75
|
+
where: [{ field: 'id', operator: '=', value: record.userId }],
|
|
76
|
+
data: { email: record.email, emailVerified: true },
|
|
77
|
+
});
|
|
78
|
+
return record;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
name: 'email-verification',
|
|
83
|
+
|
|
84
|
+
models: [{
|
|
85
|
+
name: 'email_verification_token',
|
|
86
|
+
fields: {
|
|
87
|
+
id: { type: 'number', required: true },
|
|
88
|
+
userId: { type: 'number', required: true, references: { model: 'user', field: 'id' } },
|
|
89
|
+
token: { type: 'string', required: true },
|
|
90
|
+
email: { type: 'string', required: true },
|
|
91
|
+
expiresAt: { type: 'date', required: true },
|
|
92
|
+
usedAt: { type: 'date' },
|
|
93
|
+
createdAt: { type: 'date', required: true },
|
|
94
|
+
},
|
|
95
|
+
}],
|
|
96
|
+
|
|
97
|
+
hooks: {
|
|
98
|
+
postAuthGate: {
|
|
99
|
+
reason: 'email-verification',
|
|
100
|
+
async evaluate(ctx) {
|
|
101
|
+
if (!requireVerification || ctx.user.emailVerified)
|
|
102
|
+
return;
|
|
103
|
+
|
|
104
|
+
const tokens = await ctx.db.findMany<VerificationTokenRecord>({
|
|
105
|
+
model: 'email_verification_token',
|
|
106
|
+
where: [{ field: 'userId', operator: '=', value: ctx.user.id }],
|
|
107
|
+
});
|
|
108
|
+
return tokens.some(token => token.usedAt !== null)
|
|
109
|
+
? undefined
|
|
110
|
+
: { pluginData: { requiresEmailVerification: true } };
|
|
111
|
+
},
|
|
112
|
+
async verify(ctx, completion) {
|
|
113
|
+
if (typeof completion !== 'string')
|
|
114
|
+
throw Errors.unauthorized('Invalid verification token');
|
|
115
|
+
await consumeVerificationToken(ctx.db, completion, ctx.user.id);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async afterRegister(ctx, user) {
|
|
120
|
+
const { raw, hash } = await generateRefreshToken();
|
|
121
|
+
const expiresAt = new Date(Date.now() + tokenExpirySeconds * 1000);
|
|
122
|
+
|
|
123
|
+
await ctx.db.create({
|
|
124
|
+
model: 'email_verification_token',
|
|
125
|
+
data: {
|
|
126
|
+
userId: user.id,
|
|
127
|
+
token: hash,
|
|
128
|
+
email: user.email,
|
|
129
|
+
expiresAt,
|
|
130
|
+
usedAt: null,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (config.onSendVerification) {
|
|
135
|
+
await config.onSendVerification(user.email, raw, user.id);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
methods: ctx => ({
|
|
141
|
+
async sendVerification(userId: string, email?: string): Promise<{ sent: true }> {
|
|
142
|
+
const user = await ctx.db.findOne<FortressUser>({
|
|
143
|
+
model: 'user',
|
|
144
|
+
where: [{ field: 'id', operator: '=', value: userId }],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!user)
|
|
148
|
+
throw Errors.notFound('User not found');
|
|
149
|
+
|
|
150
|
+
const targetEmail = email ?? user.email;
|
|
151
|
+
const { raw, hash } = await generateRefreshToken();
|
|
152
|
+
const expiresAt = new Date(Date.now() + tokenExpirySeconds * 1000);
|
|
153
|
+
|
|
154
|
+
await ctx.db.create({
|
|
155
|
+
model: 'email_verification_token',
|
|
156
|
+
data: {
|
|
157
|
+
userId,
|
|
158
|
+
token: hash,
|
|
159
|
+
email: targetEmail,
|
|
160
|
+
expiresAt,
|
|
161
|
+
usedAt: null,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (config.onSendVerification) {
|
|
166
|
+
await config.onSendVerification(targetEmail, raw, userId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Raw verification credentials are delivered only through the callback;
|
|
170
|
+
// returning them to the caller defeats out-of-band verification.
|
|
171
|
+
return { sent: true };
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async verify(rawToken: string): Promise<{ userId: string; email: string }> {
|
|
175
|
+
const record = await ctx.db.transaction(tx => consumeVerificationToken(tx, rawToken));
|
|
176
|
+
return { userId: record.userId, email: record.email };
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
async completeVerification(
|
|
180
|
+
continuationToken: string,
|
|
181
|
+
verificationToken: string,
|
|
182
|
+
meta?: RequestMeta,
|
|
183
|
+
): Promise<AuthResult> {
|
|
184
|
+
if (!ctx.auth)
|
|
185
|
+
throw Errors.badRequest('Auth service is unavailable');
|
|
186
|
+
return ctx.auth.completePendingAuth(continuationToken, verificationToken, meta);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passwordless magic-link plugin for fortress.
|
|
3
|
+
*
|
|
4
|
+
* Issues short-lived hashed tokens delivered out-of-band (typically by email)
|
|
5
|
+
* and exchanges a valid token for a fortress access/refresh token pair.
|
|
6
|
+
* Useful as a passwordless sign-in flow or as a recovery mechanism.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { FortressPlugin } from '../../core/plugin';
|
|
12
|
+
import type { AuthResult, FortressUser, RequestMeta } from '../../core/types';
|
|
13
|
+
import { generateRefreshToken, hashToken } from '../../core/auth/refresh-token';
|
|
14
|
+
import { Errors } from '../../core/errors';
|
|
15
|
+
|
|
16
|
+
export interface MagicLinkConfig {
|
|
17
|
+
/** Token expiry in seconds. Default: 600 (10 minutes). */
|
|
18
|
+
tokenExpirySeconds?: number;
|
|
19
|
+
/** Called when a magic link token is created -- send the link to the user */
|
|
20
|
+
onSendMagicLink?: (email: string, token: string) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MagicLinkMethods {
|
|
24
|
+
sendMagicLink: (email: string) => Promise<{ sent: true }>;
|
|
25
|
+
verify: (rawToken: string, meta?: RequestMeta) => Promise<AuthResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MagicLinkTokenRecord {
|
|
29
|
+
id: string;
|
|
30
|
+
email: string;
|
|
31
|
+
token: string;
|
|
32
|
+
expiresAt: Date;
|
|
33
|
+
usedAt: Date | null;
|
|
34
|
+
createdAt: Date;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Magic link plugin factory. Returns a {@link FortressPlugin} that issues
|
|
39
|
+
* short-lived hashed tokens for passwordless sign-in. Tokens are typically
|
|
40
|
+
* delivered out-of-band (email) and exchanged for fortress access/refresh
|
|
41
|
+
* tokens via the verify endpoint.
|
|
42
|
+
*/
|
|
43
|
+
export function magicLink(config: MagicLinkConfig = {}): FortressPlugin & { readonly name: 'magic-link' } {
|
|
44
|
+
const tokenExpirySeconds = config.tokenExpirySeconds ?? 600;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: 'magic-link',
|
|
48
|
+
|
|
49
|
+
models: [{
|
|
50
|
+
name: 'magic_link_token',
|
|
51
|
+
fields: {
|
|
52
|
+
id: { type: 'number', required: true },
|
|
53
|
+
email: { type: 'string', required: true },
|
|
54
|
+
token: { type: 'string', required: true },
|
|
55
|
+
expiresAt: { type: 'date', required: true },
|
|
56
|
+
usedAt: { type: 'date' },
|
|
57
|
+
createdAt: { type: 'date', required: true },
|
|
58
|
+
},
|
|
59
|
+
}],
|
|
60
|
+
|
|
61
|
+
methods: ctx => ({
|
|
62
|
+
async sendMagicLink(email: string): Promise<{ sent: true }> {
|
|
63
|
+
const { raw, hash } = await generateRefreshToken();
|
|
64
|
+
const expiresAt = new Date(Date.now() + tokenExpirySeconds * 1000);
|
|
65
|
+
|
|
66
|
+
await ctx.db.create({
|
|
67
|
+
model: 'magic_link_token',
|
|
68
|
+
data: {
|
|
69
|
+
email,
|
|
70
|
+
token: hash,
|
|
71
|
+
expiresAt,
|
|
72
|
+
usedAt: null,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (config.onSendMagicLink) {
|
|
77
|
+
await config.onSendMagicLink(email, raw);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { sent: true };
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async verify(rawToken: string, meta?: RequestMeta): Promise<AuthResult> {
|
|
84
|
+
const hash = await hashToken(rawToken);
|
|
85
|
+
const usedAt = new Date();
|
|
86
|
+
const record = await ctx.db.transaction(tx => tx.update<MagicLinkTokenRecord>({
|
|
87
|
+
model: 'magic_link_token',
|
|
88
|
+
where: [
|
|
89
|
+
{ field: 'token', operator: '=', value: hash },
|
|
90
|
+
{ field: 'usedAt', operator: 'isNull', value: null },
|
|
91
|
+
{ field: 'expiresAt', operator: 'gt', value: usedAt },
|
|
92
|
+
],
|
|
93
|
+
data: { usedAt },
|
|
94
|
+
}));
|
|
95
|
+
if (!record)
|
|
96
|
+
throw Errors.notFound('Invalid or expired magic link token');
|
|
97
|
+
|
|
98
|
+
// Find or create user by email (JIT provisioning)
|
|
99
|
+
let user = await ctx.db.findOne<FortressUser>({
|
|
100
|
+
model: 'user',
|
|
101
|
+
where: [{ field: 'email', operator: '=', value: record.email }],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!user) {
|
|
105
|
+
user = await ctx.auth!.createUser({
|
|
106
|
+
email: record.email,
|
|
107
|
+
name: record.email.split('@')[0],
|
|
108
|
+
}) as FortressUser;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Possession of the single-use link proves control of this address.
|
|
112
|
+
// Mark both JIT-created and existing matching accounts verified before
|
|
113
|
+
// running post-auth gates, otherwise email-verification blocks the
|
|
114
|
+
// very login that established ownership.
|
|
115
|
+
if (!user.emailVerified) {
|
|
116
|
+
await ctx.db.update({
|
|
117
|
+
model: 'user',
|
|
118
|
+
where: [{ field: 'id', operator: '=', value: user.id }],
|
|
119
|
+
data: { emailVerified: true },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!ctx.auth)
|
|
124
|
+
throw Errors.badRequest('Auth service is unavailable');
|
|
125
|
+
return ctx.auth.completePluginAuth(user.id, 'magic-link', meta);
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Fortress } from '../../core/fortress';
|
|
2
|
+
import type { AuthResult } from '../../core/types';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { createFortress } from '../../core/fortress';
|
|
5
|
+
import { createTestAdapter } from '../../testing';
|
|
6
|
+
import { magicLink } from './index';
|
|
7
|
+
|
|
8
|
+
const SECRET = 'magic-link-test-secret-32chars!!';
|
|
9
|
+
|
|
10
|
+
describe('magic-link plugin', () => {
|
|
11
|
+
let fortress: Fortress<any>;
|
|
12
|
+
let capturedToken: string | null;
|
|
13
|
+
let capturedEmail: string | null;
|
|
14
|
+
const onSend = vi.fn(async (email: string, token: string) => {
|
|
15
|
+
capturedEmail = email;
|
|
16
|
+
capturedToken = token;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
capturedToken = null;
|
|
21
|
+
capturedEmail = null;
|
|
22
|
+
onSend.mockClear();
|
|
23
|
+
|
|
24
|
+
fortress = createFortress({
|
|
25
|
+
jwt: { key: SECRET },
|
|
26
|
+
database: createTestAdapter(),
|
|
27
|
+
plugins: [magicLink({ onSendMagicLink: onSend })],
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('sendMagicLink', () => {
|
|
32
|
+
it('creates a token record and calls onSendMagicLink', async () => {
|
|
33
|
+
const send = fortress.plugins['magic-link'].sendMagicLink as (email: string) => Promise<{ sent: true }>;
|
|
34
|
+
const result = await send('alice@example.com');
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({ sent: true });
|
|
37
|
+
expect(onSend).toHaveBeenCalledOnce();
|
|
38
|
+
expect(capturedEmail).toBe('alice@example.com');
|
|
39
|
+
expect(capturedToken).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('verify', () => {
|
|
44
|
+
it('returns user info and access token for existing user', async () => {
|
|
45
|
+
// Pre-create the user
|
|
46
|
+
await fortress.auth.createUser({
|
|
47
|
+
email: 'bob@example.com',
|
|
48
|
+
name: 'Bob',
|
|
49
|
+
password: 'password-123456',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const send = fortress.plugins['magic-link'].sendMagicLink as (email: string) => Promise<{ sent: true }>;
|
|
53
|
+
await send('bob@example.com');
|
|
54
|
+
|
|
55
|
+
const verify = fortress.plugins['magic-link'].verify as (token: string) => Promise<AuthResult>;
|
|
56
|
+
const result = await verify(capturedToken!);
|
|
57
|
+
|
|
58
|
+
expect(result.user.email).toBe('bob@example.com');
|
|
59
|
+
expect(result.user.id).toBeDefined();
|
|
60
|
+
expect(result.status === 'success' ? result.accessToken : null).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('verifies through the core HTTP endpoint and attaches auth cookies', async () => {
|
|
64
|
+
const send = fortress.plugins['magic-link'].sendMagicLink as (email: string) => Promise<{ sent: true }>;
|
|
65
|
+
await send('bob@example.com');
|
|
66
|
+
|
|
67
|
+
const response = await fortress.handleRequest(new Request('http://localhost/auth/magic-link/verify', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ token: capturedToken }),
|
|
71
|
+
}));
|
|
72
|
+
expect(response.status).toBe(200);
|
|
73
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
74
|
+
status: 'success',
|
|
75
|
+
method: 'magic-link',
|
|
76
|
+
user: { email: 'bob@example.com' },
|
|
77
|
+
});
|
|
78
|
+
expect(response.headers.getSetCookie().length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('runs configured post-auth gates before issuing tokens', async () => {
|
|
82
|
+
const database = createTestAdapter();
|
|
83
|
+
const gated = createFortress({
|
|
84
|
+
jwt: { key: SECRET },
|
|
85
|
+
database,
|
|
86
|
+
plugins: [
|
|
87
|
+
magicLink({ onSendMagicLink: onSend }),
|
|
88
|
+
{
|
|
89
|
+
name: 'factor',
|
|
90
|
+
hooks: {
|
|
91
|
+
postAuthGate: {
|
|
92
|
+
reason: 'two-factor',
|
|
93
|
+
evaluate: async () => ({ pluginData: { requires2FA: true } }),
|
|
94
|
+
verify: async () => {},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
const send = gated.plugins['magic-link'].sendMagicLink as (email: string) => Promise<{ sent: true }>;
|
|
101
|
+
const verify = gated.plugins['magic-link'].verify as (token: string) => Promise<AuthResult>;
|
|
102
|
+
await send('gated@example.com');
|
|
103
|
+
|
|
104
|
+
const result = await verify(capturedToken!);
|
|
105
|
+
expect(result.status).toBe('pending');
|
|
106
|
+
expect(result.status === 'pending' ? result.pending?.reason : undefined).toBe('two-factor');
|
|
107
|
+
expect(await database.count({ model: 'refresh_token' })).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('auto-creates user for unknown email (JIT provisioning)', async () => {
|
|
111
|
+
const send = fortress.plugins['magic-link'].sendMagicLink as (email: string) => Promise<{ sent: true }>;
|
|
112
|
+
await send('newuser@example.com');
|
|
113
|
+
|
|
114
|
+
const verify = fortress.plugins['magic-link'].verify as (token: string) => Promise<AuthResult>;
|
|
115
|
+
const result = await verify(capturedToken!);
|
|
116
|
+
|
|
117
|
+
expect(result.user.email).toBe('newuser@example.com');
|
|
118
|
+
expect(result.user.id).toBeDefined();
|
|
119
|
+
expect(result.status === 'success' ? result.accessToken : null).toBeTruthy();
|
|
120
|
+
|
|
121
|
+
// Confirm the user was actually created
|
|
122
|
+
const user = await fortress.auth.me(result.user.id);
|
|
123
|
+
expect(user.email).toBe('newuser@example.com');
|
|
124
|
+
expect(user.name).toBe('newuser');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('rejects expired tokens', async () => {
|
|
128
|
+
const expiredFortress = createFortress({
|
|
129
|
+
jwt: { key: SECRET },
|
|
130
|
+
database: createTestAdapter(),
|
|
131
|
+
plugins: [magicLink({ tokenExpirySeconds: -1, onSendMagicLink: onSend })],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const send = expiredFortress.plugins['magic-link'].sendMagicLink as (email: string) => Promise<{ sent: true }>;
|
|
135
|
+
await send('expired@example.com');
|
|
136
|
+
|
|
137
|
+
const verify = expiredFortress.plugins['magic-link'].verify as (token: string) => Promise<unknown>;
|
|
138
|
+
await expect(verify(capturedToken!)).rejects.toThrow('Invalid or expired magic link token');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects already-used tokens', async () => {
|
|
142
|
+
const send = fortress.plugins['magic-link'].sendMagicLink as (email: string) => Promise<{ sent: true }>;
|
|
143
|
+
await send('carol@example.com');
|
|
144
|
+
|
|
145
|
+
const verify = fortress.plugins['magic-link'].verify as (token: string) => Promise<unknown>;
|
|
146
|
+
await verify(capturedToken!);
|
|
147
|
+
|
|
148
|
+
await expect(verify(capturedToken!)).rejects.toThrow('Invalid or expired magic link token');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rejects invalid tokens', async () => {
|
|
152
|
+
const verify = fortress.plugins['magic-link'].verify as (token: string) => Promise<unknown>;
|
|
153
|
+
await expect(verify('bogus-token')).rejects.toThrow('Invalid or expired magic link token');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Core 1.0 id_token issuer.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Build the standard claims (`iss`, `sub`, `aud`, `iat`, `exp`, plus
|
|
6
|
+
* conditional `nonce`, `auth_time`).
|
|
7
|
+
* - Add scope-gated identity claims (`email`, `email_verified`, `name`,
|
|
8
|
+
* `preferred_username`) that match what the userinfo endpoint emits, so
|
|
9
|
+
* RPs that decode the id_token directly see the same view as RPs that
|
|
10
|
+
* call userinfo.
|
|
11
|
+
* - Sign with the active RS256 key and tag the JWS header with `kid`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { FortressUser } from '../../core/types';
|
|
17
|
+
import type { ActiveSigningKey } from './jwks';
|
|
18
|
+
import { SignJWT } from 'jose';
|
|
19
|
+
|
|
20
|
+
/** Inputs to {@link issueIdToken}. All times are in seconds. */
|
|
21
|
+
export interface IdTokenParams {
|
|
22
|
+
user: FortressUser;
|
|
23
|
+
clientId: string;
|
|
24
|
+
/** OIDC Core \u00a72: same value as the AS issuer URL in discovery. */
|
|
25
|
+
issuerUrl: string;
|
|
26
|
+
/** Lifetime in seconds (default 1h). */
|
|
27
|
+
ttlSeconds?: number;
|
|
28
|
+
/** Echoed verbatim if the authorize request supplied one (\u00a73.1.3.7). */
|
|
29
|
+
nonce?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Unix seconds when the user authenticated (\u00a72). Required when
|
|
32
|
+
* `max_age` was used. Omit on refresh when the original authentication
|
|
33
|
+
* time was not persisted; never substitute the refresh time.
|
|
34
|
+
*/
|
|
35
|
+
authTimeSeconds?: number;
|
|
36
|
+
/** Space-separated scope; gates the identity claims. */
|
|
37
|
+
scope: string | null;
|
|
38
|
+
/** Pre-resolved active signing key from {@link getActiveSigningKey}. */
|
|
39
|
+
signingKey: ActiveSigningKey;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build and sign an OIDC id_token. Returns the compact JWS string.
|
|
44
|
+
*
|
|
45
|
+
* Claim coverage (OIDC Core \u00a72 + \u00a75.1):
|
|
46
|
+
* - `iss`, `sub`, `aud`, `iat`, `exp` always
|
|
47
|
+
* - `auth_time` and `nonce` when supplied
|
|
48
|
+
* - `email`, `email_verified` only when scope contains `email`
|
|
49
|
+
* - `name`, `preferred_username` when scope contains `profile`
|
|
50
|
+
* - `updated_at` always (mirrors userinfo)
|
|
51
|
+
*/
|
|
52
|
+
export async function issueIdToken(params: IdTokenParams): Promise<string> {
|
|
53
|
+
const ttl = params.ttlSeconds ?? 3600;
|
|
54
|
+
const now = Math.floor(Date.now() / 1000);
|
|
55
|
+
const scopes = params.scope ? params.scope.split(' ').filter(Boolean) : [];
|
|
56
|
+
const exposeEmail = scopes.includes('email');
|
|
57
|
+
const exposeProfile = scopes.includes('profile');
|
|
58
|
+
|
|
59
|
+
// Standard + scope-gated identity claims.
|
|
60
|
+
const claims: Record<string, unknown> = {};
|
|
61
|
+
if (params.authTimeSeconds !== undefined)
|
|
62
|
+
claims.auth_time = params.authTimeSeconds;
|
|
63
|
+
if (params.nonce)
|
|
64
|
+
claims.nonce = params.nonce;
|
|
65
|
+
if (exposeEmail) {
|
|
66
|
+
claims.email = params.user.email;
|
|
67
|
+
if (typeof params.user.emailVerified === 'boolean')
|
|
68
|
+
claims.email_verified = params.user.emailVerified;
|
|
69
|
+
}
|
|
70
|
+
if (exposeProfile) {
|
|
71
|
+
claims.name = params.user.name;
|
|
72
|
+
claims.preferred_username = params.user.email;
|
|
73
|
+
}
|
|
74
|
+
if (params.user.updatedAt) {
|
|
75
|
+
claims.updated_at = Math.floor(new Date(params.user.updatedAt).getTime() / 1000);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return new SignJWT(claims)
|
|
79
|
+
.setProtectedHeader({ alg: 'RS256', kid: params.signingKey.kid, typ: 'JWT' })
|
|
80
|
+
.setIssuer(params.issuerUrl)
|
|
81
|
+
.setSubject(String(params.user.id))
|
|
82
|
+
.setAudience(params.clientId)
|
|
83
|
+
.setIssuedAt(now)
|
|
84
|
+
.setExpirationTime(now + ttl)
|
|
85
|
+
.sign(params.signingKey.privateKey);
|
|
86
|
+
}
|