@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,544 @@
|
|
|
1
|
+
# OAuth Plugin
|
|
2
|
+
|
|
3
|
+
OAuth 2.0 authorization server with PKCE support and scope-to-IAM permission mapping.
|
|
4
|
+
|
|
5
|
+
Implements RFC 6749 (OAuth 2.0), RFC 7636 (PKCE), RFC 7662 (Token Introspection), RFC 7009 (Token Revocation), and partial RFC 8414 (OIDC Discovery). Supports authorization code and client credentials grant types.
|
|
6
|
+
|
|
7
|
+
The unique differentiator is `scopePermissionMap`, which bridges OAuth scopes to Fortress IAM permissions so that token-based access control and role-based access control share a single permission model.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createFortress } from "@bajustone/fortress";
|
|
13
|
+
import { oauth } from "@bajustone/fortress/plugins/oauth";
|
|
14
|
+
|
|
15
|
+
const fortress = createFortress({
|
|
16
|
+
jwt: { key: "your-secret-minimum-32-bytes-long!" },
|
|
17
|
+
database: db,
|
|
18
|
+
plugins: [
|
|
19
|
+
oauth({
|
|
20
|
+
issuerUrl: "https://auth.example.com",
|
|
21
|
+
scopePermissionMap: {
|
|
22
|
+
"read:posts": { resource: "post", action: "read" },
|
|
23
|
+
"write:posts": { resource: "post", action: "create" },
|
|
24
|
+
"read:users": { resource: "user", action: "list" },
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
All fields in `OAuthConfig` are optional.
|
|
34
|
+
|
|
35
|
+
| Option | Type | Default | Description |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `authCodeExpirySeconds` | `number` | `600` (10 min) | How long an authorization code is valid. |
|
|
38
|
+
| `pendingFlowExpirySeconds` | `number` | `600` (10 min) | How long a pending flow (identity broker) survives before expiry. |
|
|
39
|
+
| `accessTokenExpirySeconds` | `number` | `3600` (1 hour) | Lifetime of issued access tokens. |
|
|
40
|
+
| `scopePermissionMap` | `Record<string, { resource: string; action: string }>` | `undefined` | Maps OAuth scope strings to Fortress IAM resource+action pairs. |
|
|
41
|
+
| `issuerUrl` | `string` | `"https://localhost"` | Base URL of the authorization server. Used in the OIDC discovery document and endpoint URLs. |
|
|
42
|
+
|
|
43
|
+
### scopePermissionMap
|
|
44
|
+
|
|
45
|
+
This is the bridge between OAuth and Fortress IAM. When a token carries scopes like `"read:posts write:posts"`, calling `resolveTokenPermissions(token)` returns the corresponding IAM permissions:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const fortress = createFortress({
|
|
49
|
+
// ...
|
|
50
|
+
plugins: [
|
|
51
|
+
oauth({
|
|
52
|
+
scopePermissionMap: {
|
|
53
|
+
"read:posts": { resource: "post", action: "read" },
|
|
54
|
+
"write:posts": { resource: "post", action: "create" },
|
|
55
|
+
"admin:users": { resource: "user", action: "delete" },
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Later, in a request handler:
|
|
62
|
+
const permissions = await fortress.plugins.oauth.resolveTokenPermissions(token);
|
|
63
|
+
// => [{ resource: "post", action: "read" }, { resource: "post", action: "create" }]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Scopes not present in the map are silently ignored. Tokens with no scope return an empty array.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
All methods are available on `fortress.plugins.oauth`.
|
|
71
|
+
|
|
72
|
+
### Client Registration
|
|
73
|
+
|
|
74
|
+
Register an OAuth client. The returned `clientSecret` is shown once and stored as a hash.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const { clientId, clientSecret } = await fortress.plugins.oauth.createClient({
|
|
78
|
+
name: "My Web App",
|
|
79
|
+
redirectUris: ["https://app.example.com/callback"],
|
|
80
|
+
grantTypes: ["authorization_code"],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Store clientId and clientSecret securely in the consuming application.
|
|
84
|
+
// clientSecret cannot be retrieved again.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For service-to-service clients that do not involve a user:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const { clientId, clientSecret } = await fortress.plugins.oauth.createClient({
|
|
91
|
+
name: "Background Worker",
|
|
92
|
+
redirectUris: [],
|
|
93
|
+
grantTypes: ["client_credentials"],
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A client can support both grant types:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const { clientId, clientSecret } = await fortress.plugins.oauth.createClient({
|
|
101
|
+
name: "Hybrid App",
|
|
102
|
+
redirectUris: ["https://app.example.com/callback"],
|
|
103
|
+
grantTypes: ["authorization_code", "client_credentials"],
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Authorization Code Flow
|
|
108
|
+
|
|
109
|
+
The standard three-step flow: redirect the user to your authorization UI, generate a code after they approve, then exchange the code for tokens.
|
|
110
|
+
|
|
111
|
+
**Step 1: Generate an authorization code** (after the user authenticates and consents)
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const { code } = await fortress.plugins.oauth.createAuthorizationCode({
|
|
115
|
+
clientId: "the-client-id",
|
|
116
|
+
userId: authenticatedUser.id,
|
|
117
|
+
redirectUri: "https://app.example.com/callback",
|
|
118
|
+
scope: "read:posts write:posts",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Redirect the user back to the client:
|
|
122
|
+
// https://app.example.com/callback?code=<code>&state=<state>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Step 2: Exchange the code for an access token** (the client calls this)
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const tokens = await fortress.plugins.oauth.exchangeCode({
|
|
129
|
+
code: "the-authorization-code",
|
|
130
|
+
clientId: "the-client-id",
|
|
131
|
+
clientSecret: "the-client-secret",
|
|
132
|
+
redirectUri: "https://app.example.com/callback",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// tokens = {
|
|
136
|
+
// accessToken: "...",
|
|
137
|
+
// tokenType: "Bearer",
|
|
138
|
+
// expiresIn: 3600,
|
|
139
|
+
// scope: "read:posts write:posts",
|
|
140
|
+
// }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Authorization codes are single-use. Attempting to exchange a code twice throws an error.
|
|
144
|
+
|
|
145
|
+
### PKCE Support
|
|
146
|
+
|
|
147
|
+
PKCE (Proof Key for Code Exchange, RFC 7636) prevents authorization code interception attacks. Only S256 is supported.
|
|
148
|
+
|
|
149
|
+
The plugin exports `generateCodeVerifier` and `generateCodeChallenge` helpers:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import {
|
|
153
|
+
generateCodeChallenge,
|
|
154
|
+
generateCodeVerifier,
|
|
155
|
+
} from "@bajustone/fortress/plugins/oauth";
|
|
156
|
+
|
|
157
|
+
// Client generates a verifier and challenge before starting the flow
|
|
158
|
+
const codeVerifier = generateCodeVerifier();
|
|
159
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
160
|
+
|
|
161
|
+
// Pass the challenge when creating the authorization code
|
|
162
|
+
const { code } = await fortress.plugins.oauth.createAuthorizationCode({
|
|
163
|
+
clientId: "the-client-id",
|
|
164
|
+
userId: authenticatedUser.id,
|
|
165
|
+
redirectUri: "https://app.example.com/callback",
|
|
166
|
+
codeChallenge,
|
|
167
|
+
codeChallengeMethod: "S256",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// When exchanging, provide the original verifier
|
|
171
|
+
const tokens = await fortress.plugins.oauth.exchangeCode({
|
|
172
|
+
code,
|
|
173
|
+
clientId: "the-client-id",
|
|
174
|
+
clientSecret: "the-client-secret",
|
|
175
|
+
redirectUri: "https://app.example.com/callback",
|
|
176
|
+
codeVerifier,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If a code was created with a `codeChallenge`, the `codeVerifier` is mandatory at exchange time. A wrong verifier is rejected with `"Invalid code_verifier"`.
|
|
181
|
+
|
|
182
|
+
### Client Credentials Grant
|
|
183
|
+
|
|
184
|
+
For service-to-service authentication where no user is involved:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const tokens = await fortress.plugins.oauth.clientCredentialsGrant({
|
|
188
|
+
clientId: "the-client-id",
|
|
189
|
+
clientSecret: "the-client-secret",
|
|
190
|
+
scope: "read:posts",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// tokens = {
|
|
194
|
+
// accessToken: "...",
|
|
195
|
+
// tokenType: "Bearer",
|
|
196
|
+
// expiresIn: 3600,
|
|
197
|
+
// }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The client must have `"client_credentials"` in its `grantTypes` list, otherwise the call is rejected.
|
|
201
|
+
|
|
202
|
+
### Token Introspection
|
|
203
|
+
|
|
204
|
+
Check whether an access token is active and retrieve its metadata (RFC 7662):
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const info = await fortress.plugins.oauth.introspectToken(accessToken);
|
|
208
|
+
|
|
209
|
+
if (info.active) {
|
|
210
|
+
console.log(info.clientId); // which client issued this token
|
|
211
|
+
console.log(info.userId); // associated user (undefined for client_credentials)
|
|
212
|
+
console.log(info.scope); // space-separated scopes
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Token Revocation
|
|
217
|
+
|
|
218
|
+
Immediately invalidate an access token (RFC 7009):
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
await fortress.plugins.oauth.revokeToken(accessToken);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Revoking a nonexistent or already-revoked token is a no-op (no error thrown), per the RFC.
|
|
225
|
+
|
|
226
|
+
### Pending Flows (Identity Broker Pattern)
|
|
227
|
+
|
|
228
|
+
When the user arrives at your `/authorize` endpoint but is not yet authenticated, you can park the OAuth flow, send the user through login, then resume:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// 1. User hits /authorize but is not logged in -- save the flow
|
|
232
|
+
const { flowId } = await fortress.plugins.oauth.createPendingFlow({
|
|
233
|
+
clientId: query.client_id,
|
|
234
|
+
redirectUri: query.redirect_uri,
|
|
235
|
+
scope: query.scope,
|
|
236
|
+
state: query.state,
|
|
237
|
+
codeChallenge: query.code_challenge,
|
|
238
|
+
codeChallengeMethod: query.code_challenge_method,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// 2. Redirect to login, passing flowId (e.g. /login?flow=<flowId>)
|
|
242
|
+
|
|
243
|
+
// 3. After login, resume the flow
|
|
244
|
+
const flow = await fortress.plugins.oauth.resumePendingFlow(flowId);
|
|
245
|
+
|
|
246
|
+
// 4. Now issue the authorization code with the authenticated user
|
|
247
|
+
const { code } = await fortress.plugins.oauth.createAuthorizationCode({
|
|
248
|
+
clientId: flow.clientId,
|
|
249
|
+
userId: authenticatedUser.id,
|
|
250
|
+
redirectUri: flow.redirectUri,
|
|
251
|
+
scope: flow.scope ?? undefined,
|
|
252
|
+
codeChallenge: flow.codeChallenge ?? undefined,
|
|
253
|
+
codeChallengeMethod: flow.codeChallengeMethod ?? undefined,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// 5. Redirect back to the client
|
|
257
|
+
// https://app.example.com/callback?code=<code>&state=<flow.state>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Pending flows are single-use and expire after `pendingFlowExpirySeconds` (default 10 minutes).
|
|
261
|
+
|
|
262
|
+
### SPA-friendly Consent Flow (Pattern B)
|
|
263
|
+
|
|
264
|
+
For host apps that already render their own login UI (e.g. SvelteKit/Next/React) the plugin ships a turn-key version of the broker flow above. Fortress runs the OAuth state machine; your app owns the screens.
|
|
265
|
+
|
|
266
|
+
Enable it via four config knobs:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
oauth({
|
|
270
|
+
enableAuthorizeEndpoint: true,
|
|
271
|
+
enableConsentApi: true,
|
|
272
|
+
loginUrl: 'https://app.example.com/signin',
|
|
273
|
+
consentUrl: 'https://app.example.com/oauth/consent',
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The choreography:
|
|
278
|
+
|
|
279
|
+
1. **OAuth client** sends the browser to `GET /oauth/authorize?client_id=...&redirect_uri=...&response_type=code&state=...&code_challenge=...`. Fortress validates everything against the registered client, creates an `oauth_pending_flow` row, and 302s to either `${loginUrl}?flow=<id>` (no session) or `${consentUrl}?flow=<id>` (logged in).
|
|
280
|
+
2. **Sign-in page** (your app, e.g. `/signin`) reads `?flow=<id>`, runs the existing login form, and on success redirects back to `${API}/oauth/authorize?...` with the original query — or simpler, to `${consentUrl}?flow=<id>` directly now that the session cookie is set.
|
|
281
|
+
3. **Consent page** (your app, e.g. `/oauth/consent`) calls `GET /oauth/flows/<id>` (cookie-authed) for `{ flowId, client: { clientId, name }, redirectUri, scopes, state }`, renders the screen, and on submit posts to `POST /oauth/flows/<id>/approve` or `/deny`. Both return `{ redirectUrl }`; the page navigates the browser there.
|
|
282
|
+
|
|
283
|
+
Fortress never serves HTML. The `code_challenge` / `code_challenge_method` are kept server-side and never returned by `GET /oauth/flows/<id>`, so the SPA can't tamper with PKCE.
|
|
284
|
+
|
|
285
|
+
**Cross-origin setups.** The flow above works whether the host app and the Fortress API share an origin or not, but the cookie + CORS wiring differs:
|
|
286
|
+
|
|
287
|
+
- **Same origin or sibling subdomains** (e.g. `api.example.com` + `app.example.com` under `example.com`): set `cookies.domain: '.example.com'` and `cookies.sameSite: 'lax'`. The SvelteKit consent page can read the flow with a server-side `+page.server.ts` `load` that forwards the incoming `cookie` header to the API. No browser CORS needed.
|
|
288
|
+
- **Truly different origins** (e.g. `tdmp-api.com` + `tdmp-web.com`): set `cookies.sameSite: 'none'` and `cookies.secure: true`, drop the `__Host-` prefix (`__Host-` cookies forbid `Domain`, but cross-site cookies need `SameSite=None`). The consent page must fetch `/oauth/flows/:id` from the **browser** with `credentials: 'include'` (a `+page.ts` universal load, or `onMount`) — SvelteKit's server can't see the API origin's cookie. Configure CORS on `/oauth/flows/*` and `/auth/*` to allow the web origin with `credentials: true`.
|
|
289
|
+
|
|
290
|
+
In both cases the 302s out of `/oauth/authorize` are top-level navigations and aren't subject to CORS.
|
|
291
|
+
|
|
292
|
+
Methods you can call directly (in addition to the HTTP endpoints):
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// Non-destructive read — used by the consent page on every render.
|
|
296
|
+
const flow = await fortress.plugins.oauth.getPendingFlow(flowId);
|
|
297
|
+
|
|
298
|
+
// Same as the HTTP handlers; transport-agnostic.
|
|
299
|
+
const { redirectUrl } = await fortress.plugins.oauth.handleApproveFlow(flowId, { userId });
|
|
300
|
+
await fortress.plugins.oauth.handleDenyFlow(flowId);
|
|
301
|
+
|
|
302
|
+
// Operational key rotation. The retired public key remains in JWKS for
|
|
303
|
+
// signingKeyGraceSeconds (defaults to idTokenExpirySeconds), then is pruned.
|
|
304
|
+
const { kid } = await fortress.plugins.oauth.rotateSigningKey();
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Scope-to-IAM Permission Mapping
|
|
308
|
+
|
|
309
|
+
Resolve an access token's scopes into Fortress IAM permissions:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const permissions = await fortress.plugins.oauth.resolveTokenPermissions(token);
|
|
313
|
+
// => [{ resource: "post", action: "read" }, { resource: "post", action: "create" }]
|
|
314
|
+
|
|
315
|
+
// Use with fortress.iam to check access
|
|
316
|
+
for (const perm of permissions) {
|
|
317
|
+
const allowed = await fortress.iam.checkPermission(
|
|
318
|
+
{ type: 'USER', id: userId },
|
|
319
|
+
perm.resource,
|
|
320
|
+
perm.action,
|
|
321
|
+
);
|
|
322
|
+
// ...
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Returns an empty array if the token has no scope, is inactive, or if no `scopePermissionMap` was configured.
|
|
327
|
+
|
|
328
|
+
### UserInfo
|
|
329
|
+
|
|
330
|
+
Retrieve the user associated with an access token (OpenID Connect userinfo):
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const user = await fortress.plugins.oauth.getUserInfo(accessToken);
|
|
334
|
+
// => { id: 1, email: "alice@example.com", name: "Alice", ... } or null
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Returns `null` for invalid/expired tokens or client_credentials tokens (which have no user).
|
|
338
|
+
|
|
339
|
+
## HTTP Endpoints
|
|
340
|
+
|
|
341
|
+
Mount the OAuth endpoints (along with all other Fortress routes) with a
|
|
342
|
+
single call:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { Hono } from "hono";
|
|
346
|
+
import { mountFortress } from "@bajustone/fortress/hono";
|
|
347
|
+
|
|
348
|
+
const app = new Hono();
|
|
349
|
+
mountFortress(app, fortress);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
This registers the following endpoints. An optional `prefix` can be passed: `mountFortress(app, fortress, { prefix: "/api" })`.
|
|
353
|
+
|
|
354
|
+
### GET /oauth/authorize *(opt-in)*
|
|
355
|
+
|
|
356
|
+
Front door for the auth-code flow. Mounted only when `enableAuthorizeEndpoint: true`.
|
|
357
|
+
|
|
358
|
+
Validates `client_id`, `redirect_uri`, `response_type=code`, `state`, and (optional) `code_challenge[_method]` against the registered client, persists the request as a pending flow, then returns `302 Location: ${loginUrl}?flow=<id>` (no session) or `${consentUrl}?flow=<id>` (authenticated).
|
|
359
|
+
|
|
360
|
+
Returns `400` for unknown client, mismatched redirect URI, or unsupported PKCE method.
|
|
361
|
+
|
|
362
|
+
### GET /oauth/flows/:flowId *(opt-in)*
|
|
363
|
+
|
|
364
|
+
Flow metadata for the consent page. Mounted only when `enableConsentApi: true`. Requires a bearer/cookie session.
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"flowId": 17,
|
|
369
|
+
"client": { "clientId": "...", "name": "Brand X" },
|
|
370
|
+
"redirectUri": "https://x.com/callback",
|
|
371
|
+
"scopes": ["read:posts", "write:posts"],
|
|
372
|
+
"state": "opaque-csrf-value"
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Returns `404` if the flow is unknown or expired.
|
|
377
|
+
|
|
378
|
+
### POST /oauth/flows/:flowId/approve *(opt-in)*
|
|
379
|
+
|
|
380
|
+
Issue an authorization code on behalf of the authenticated user, consume the pending flow, and return `{ redirectUrl }` (the OAuth client's `redirect_uri` with `?code=...&state=...` appended). Requires a bearer/cookie session.
|
|
381
|
+
|
|
382
|
+
### POST /oauth/flows/:flowId/deny *(opt-in)*
|
|
383
|
+
|
|
384
|
+
Consume the pending flow and return `{ redirectUrl }` with `?error=access_denied&state=...`.
|
|
385
|
+
|
|
386
|
+
### POST /oauth/token
|
|
387
|
+
|
|
388
|
+
Exchange an authorization code or client credentials for an access token.
|
|
389
|
+
|
|
390
|
+
**Content-Type:** `application/x-www-form-urlencoded`
|
|
391
|
+
|
|
392
|
+
**Client authentication:** HTTP Basic (`Authorization: Basic base64(clientId:clientSecret)`) or body params (`client_id` + `client_secret`).
|
|
393
|
+
|
|
394
|
+
Authorization code grant:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
curl -X POST https://auth.example.com/oauth/token \
|
|
398
|
+
-u "$CLIENT_ID:$CLIENT_SECRET" \
|
|
399
|
+
-d "grant_type=authorization_code" \
|
|
400
|
+
-d "code=the-auth-code" \
|
|
401
|
+
-d "redirect_uri=https://app.example.com/callback" \
|
|
402
|
+
-d "code_verifier=the-pkce-verifier"
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Client credentials grant:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
curl -X POST https://auth.example.com/oauth/token \
|
|
409
|
+
-u "$CLIENT_ID:$CLIENT_SECRET" \
|
|
410
|
+
-d "grant_type=client_credentials" \
|
|
411
|
+
-d "scope=read:posts"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Response:**
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"access_token": "...",
|
|
419
|
+
"token_type": "Bearer",
|
|
420
|
+
"expires_in": 3600,
|
|
421
|
+
"scope": "read:posts"
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### POST /oauth/introspect
|
|
426
|
+
|
|
427
|
+
Validate an access token (RFC 7662). Requires client authentication via HTTP Basic.
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
curl -X POST https://auth.example.com/oauth/introspect \
|
|
431
|
+
-u "$CLIENT_ID:$CLIENT_SECRET" \
|
|
432
|
+
-d "token=the-access-token"
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Response (active):**
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
{
|
|
439
|
+
"active": true,
|
|
440
|
+
"client_id": "...",
|
|
441
|
+
"sub": "42",
|
|
442
|
+
"scope": "read:posts",
|
|
443
|
+
"token_type": "Bearer"
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Response (inactive/expired/revoked):**
|
|
448
|
+
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"active": false
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### POST /oauth/revoke
|
|
456
|
+
|
|
457
|
+
Revoke an access token (RFC 7009). Always returns HTTP 200, even if the token does not exist.
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
curl -X POST https://auth.example.com/oauth/revoke \
|
|
461
|
+
-d "token=the-access-token"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### GET /oauth/userinfo
|
|
465
|
+
|
|
466
|
+
Get scope-authorized identity claims for a valid user-bound access token. Requires a Bearer token. `email`/`email_verified` are returned only with the `email` scope; `name`/`preferred_username` only with `profile`. Tokens carrying unrelated API scopes receive `sub` but no identity PII.
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
curl https://auth.example.com/oauth/userinfo \
|
|
470
|
+
-H "Authorization: Bearer the-access-token"
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Response:**
|
|
474
|
+
|
|
475
|
+
```json
|
|
476
|
+
{
|
|
477
|
+
"sub": "42",
|
|
478
|
+
"email": "alice@example.com",
|
|
479
|
+
"name": "Alice"
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Returns HTTP 401 if the token is invalid, expired, or belongs to a client_credentials grant (no user).
|
|
484
|
+
|
|
485
|
+
### GET /oauth/.well-known/openid-configuration
|
|
486
|
+
|
|
487
|
+
OIDC discovery document (RFC 8414). No authentication required.
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
curl https://auth.example.com/oauth/.well-known/openid-configuration
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**Response:**
|
|
494
|
+
|
|
495
|
+
```json
|
|
496
|
+
{
|
|
497
|
+
"issuer": "https://auth.example.com",
|
|
498
|
+
"authorization_endpoint": "https://auth.example.com/oauth/authorize",
|
|
499
|
+
"token_endpoint": "https://auth.example.com/oauth/token",
|
|
500
|
+
"introspection_endpoint": "https://auth.example.com/oauth/introspect",
|
|
501
|
+
"revocation_endpoint": "https://auth.example.com/oauth/revoke",
|
|
502
|
+
"userinfo_endpoint": "https://auth.example.com/oauth/userinfo",
|
|
503
|
+
"response_types_supported": ["code"],
|
|
504
|
+
"grant_types_supported": ["authorization_code", "client_credentials"],
|
|
505
|
+
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
|
|
506
|
+
"code_challenge_methods_supported": ["S256"],
|
|
507
|
+
"subject_types_supported": ["public"]
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## API Reference
|
|
512
|
+
|
|
513
|
+
All methods are accessed via `fortress.plugins.oauth`.
|
|
514
|
+
|
|
515
|
+
| Method | Description |
|
|
516
|
+
|---|---|
|
|
517
|
+
| `createClient(data)` | Register an OAuth client. Returns `{ clientId, clientSecret }`. |
|
|
518
|
+
| `createAuthorizationCode(params)` | Generate an auth code for a user+client. Returns `{ code }`. |
|
|
519
|
+
| `exchangeCode(params)` | Exchange an auth code for an access token. Returns `{ accessToken, tokenType, expiresIn, scope? }`. |
|
|
520
|
+
| `clientCredentialsGrant(params)` | Issue a token via client credentials. Returns `{ accessToken, tokenType, expiresIn }`. |
|
|
521
|
+
| `introspectToken(token)` | Check if a token is active. Returns `{ active, clientId?, userId?, scope? }`. |
|
|
522
|
+
| `revokeToken(token)` | Revoke an access token. |
|
|
523
|
+
| `createPendingFlow(params)` | Park an OAuth flow for unauthenticated users. Returns `{ flowId }`. |
|
|
524
|
+
| `resumePendingFlow(flowId)` | Resume and consume a pending flow. Returns `PendingFlowRecord`. |
|
|
525
|
+
| `getUserInfo(token)` | Get the user for a token. Returns `FortressUser \| null`. |
|
|
526
|
+
| `resolveTokenPermissions(token)` | Map a token's scopes to IAM permissions via `scopePermissionMap`. Returns `{ resource, action }[]`. |
|
|
527
|
+
| `handleTokenRequest(body, clientAuth?)` | HTTP handler for POST /oauth/token. |
|
|
528
|
+
| `handleIntrospectRequest(body, clientAuth)` | HTTP handler for POST /oauth/introspect. |
|
|
529
|
+
| `handleRevokeRequest(body, clientAuth)` | HTTP handler for POST /oauth/revoke. |
|
|
530
|
+
| `handleUserInfoRequest(bearerToken)` | HTTP handler for GET /oauth/userinfo. |
|
|
531
|
+
| `handleDiscovery()` | HTTP handler for GET /oauth/.well-known/openid-configuration. |
|
|
532
|
+
|
|
533
|
+
## Database Models
|
|
534
|
+
|
|
535
|
+
The plugin creates four tables automatically via the Fortress model system:
|
|
536
|
+
|
|
537
|
+
- `oauth_client` -- registered OAuth clients (clientId, secretHash, redirectUris, grantTypes)
|
|
538
|
+
- `oauth_authorization_code` -- authorization codes (hashed, single-use, expiry, PKCE fields)
|
|
539
|
+
- `oauth_access_token` -- issued access tokens (hashed, expiry, scope, optional userId)
|
|
540
|
+
- `oauth_pending_flow` -- parked authorization flows for the identity broker pattern
|
|
541
|
+
|
|
542
|
+
## Working Example
|
|
543
|
+
|
|
544
|
+
See `examples/hono-app/index.ts` for a complete Hono application that mounts the OAuth plugin alongside authentication, RBAC, two-factor, and audit logging.
|