diff --git a/.changeset/adapter-usage-docs.md b/.changeset/adapter-usage-docs.md deleted file mode 100644 index fd1d22f..0000000 --- a/.changeset/adapter-usage-docs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Correct the adapter usage docs. The README Quick Start now passes every required option (`authServerUrl`, `cookieSecret`, `serviceSecret`, `audience`) and calls `requireAuth({ cookieSecret })`, so the copy-paste example runs. The Environment Variables table, which listed variables the adapter never reads (including unrelated database settings), is replaced with a configuration section stating that all settings are passed as options. The `requireAuth` JSDoc no longer claims the guard performs token refresh or documents a positional signature it does not accept, and its duplicated options interface is removed. diff --git a/.changeset/collapse-user-sub.md b/.changeset/collapse-user-sub.md deleted file mode 100644 index 909b3cb..0000000 --- a/.changeset/collapse-user-sub.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@seamless-auth/express": minor -"@seamless-auth/core": minor ---- - -Breaking: remove the duplicate `sub` field from `SeamlessAuthUser`. - -`requireAuth` populated both `id` and `sub` on `req.user` from the same access token `sub` claim. -Only `id` remains, which is also the identifier exposed by `getSeamlessUser`, so both user sources -now agree on one field name. - -Adopters must replace `req.user.sub` with `req.user.id`. Any defensive `user.sub ?? user.id` -coalescing can be reduced to `user.id`. The `sub` claim inside JWT payloads is unchanged. - -`getSeamlessUser` also gains a real return type. It previously returned `any` by default, which is -what made that coalescing look necessary. It now returns the exported `SeamlessUser` interface -(`id`, `email`, `phone`, `roles`, plus optional `lastLogin` and `activeOrganizationId`), matching -the auth API's `GET /users/me` response. The generic parameter is unchanged for callers that pass -their own type. diff --git a/.changeset/cookie-clearing-and-csrf.md b/.changeset/cookie-clearing-and-csrf.md deleted file mode 100644 index 970ca28..0000000 --- a/.changeset/cookie-clearing-and-csrf.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@seamless-auth/express": minor ---- - -Fix two security issues in the Express adapter. - -Cookie clearing now mirrors the cookie set path. `clearSessionCookie` and -`clearAllCookies` previously emitted a clearing header with no `Secure` or -`SameSite`, which browsers reject in a cross-site response. In the default -cross-site deployment that meant logout returned 204 and revoked the session -upstream while the signed cookie survived in the browser and stayed valid for -every route guarded by `requireAuth` until its own TTL expired. - -BREAKING: `GET /auth/logout` and `GET /auth/magic-link` are removed. Both were -state-changing routes reachable as simple cross-site requests, so an `` -on any page could revoke all of a user's sessions or trigger magic-link emails. -Use `DELETE /auth/logout/all` in place of `GET /auth/logout`, and the new -`POST /auth/magic-link` in place of `GET /auth/magic-link`. diff --git a/.changeset/cookie-requirement-routing.md b/.changeset/cookie-requirement-routing.md deleted file mode 100644 index b1b05c3..0000000 --- a/.changeset/cookie-requirement-routing.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Register `/users/update`, `/users/credentials`, `/sessions`, and `/admin/credential-count` in the core cookie requirements table. Without these entries the ensureCookies middleware never populated `req.cookiePayload`, so `/users/update` and `/users/credentials` returned 401 and `/sessions` and `/admin/credential-count` failed to forward the access token upstream. diff --git a/.changeset/cookie-secure-option.md b/.changeset/cookie-secure-option.md deleted file mode 100644 index 76ad167..0000000 --- a/.changeset/cookie-secure-option.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Drive cookie `Secure` and `SameSite` from explicit adapter options instead of ambient `NODE_ENV`. - -Session cookies previously only got `Secure` and `SameSite=None` when `process.env.NODE_ENV === "production"`, so a production deploy that forgot to set `NODE_ENV` shipped session cookies over plaintext HTTP with a weaker CSRF posture. - -`createSeamlessAuthServer` and `createEnsureCookiesMiddleware` now accept: - -- `cookieSecure?: boolean`, defaulting to `true` -- `cookieSameSite?: "lax" | "none" | "strict"`, defaulting to `none` when secure and `lax` otherwise - -Cookies are now secure by default in every environment. Set `cookieSecure: false` for local HTTP development. If you relied on the old behavior to develop over plain HTTP without setting `NODE_ENV=production`, add `cookieSecure: false` to your local configuration. diff --git a/.changeset/core-secret-validation.md b/.changeset/core-secret-validation.md deleted file mode 100644 index 3f62505..0000000 --- a/.changeset/core-secret-validation.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Move secret strength validation into the core and apply it to every entry point that accepts a secret. - -The 32 character minimum on `cookieSecret` and `serviceSecret` previously only guarded -`createSeamlessAuthServer` and `createEnsureCookiesMiddleware` in the Express adapter, so an adopter -calling a core function or `requireAuth` directly got no protection. - -`@seamless-auth/core` now owns the check and exports `MIN_SECRET_LENGTH`, `assertSecretStrength`, -and `assertSecrets`. It runs in `ensureCookies`, `refreshAccessToken`, `getSeamlessUser`, and -`createServiceToken`. The Express adapter re-uses the core implementation and adds it to -`requireAuth`, which previously only checked that `cookieSecret` was present. - -`verifyCookieJwt` and `verifyRefreshCookie` are deliberately unchanged. They are low-level -primitives with a documented "return `null` on failure" contract, and every code path in these -packages that reaches them validates the secret first. - -Adopters passing a secret shorter than 32 characters to any of these functions will now get a thrown -error naming the option. Generate replacements with a CSPRNG, for example `openssl rand -base64 48`. diff --git a/.changeset/encode-upstream-params.md b/.changeset/encode-upstream-params.md deleted file mode 100644 index 4f7570d..0000000 --- a/.changeset/encode-upstream-params.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Encode user-derived path segments before interpolating them into upstream auth server URLs. - -Admin handlers, session handlers, and the Express `/magic-link/verify/:token` route interpolated route params directly into the upstream URL. A param carrying an encoded `?`, `#`, `;`, or `%2F` was decoded into the URL raw, so it could append or override upstream query params or reshape the upstream path. - -Every user-derived segment now goes through `encodeURIComponent`, matching the organization and OAuth routes. A param that previously reshaped the upstream request is now confined to a single path segment, which upstream rejects as an unknown id. diff --git a/.changeset/error-middleware-status.md b/.changeset/error-middleware-status.md deleted file mode 100644 index 756bd4f..0000000 --- a/.changeset/error-middleware-status.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Stop rewriting client errors to 500 in the router error middleware. `express.json()` is mounted on the same router, so body-parser failures land in the catch-all. Those errors carry their own status (400 for `entity.parse.failed`, 413 for `entity.too.large`), and the middleware discarded it: a malformed JSON body answered `500 {"error":"internal_error"}` instead of 400. The middleware now honors a 4xx status from `status` or `statusCode` and answers with a generic body (`bad_request`, or `payload_too_large` for 413). Genuine 5xx failures are unchanged and still answer `500 {"error":"internal_error"}`. - -Client errors are also no longer written to `console.error`. Every malformed request produced an error-level log line, so an unauthenticated caller could generate unbounded error log volume. Only 5xx failures log now. - -The error object is still never serialized to the client. That matters here because `entity.parse.failed` errors carry a `body` property holding the raw payload, so echoing the parser error would reflect request content back. diff --git a/.changeset/fix-users-credentials-delete.md b/.changeset/fix-users-credentials-delete.md deleted file mode 100644 index ba309b9..0000000 --- a/.changeset/fix-users-credentials-delete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Fix `DELETE /users/credentials` proxying to the auth API as a `POST`. The adapter now forwards the request as a `DELETE`, matching the API's `deleteCredential` contract (the previous default hit `updateCredential` instead). diff --git a/.changeset/get-seamless-user-service-token.md b/.changeset/get-seamless-user-service-token.md deleted file mode 100644 index f905b57..0000000 --- a/.changeset/get-seamless-user-service-token.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Forward the service token from `getSeamlessUser`, so the client IP it sends is honored again. - -`GetSeamlessUserOptions` did not declare `serviceAuthorization` and the core `authFetch` call never passed it. The Express adapter still computed the service token and passed it, but an `as GetSeamlessUserOptions` cast on the option literal discarded it without a type error. Every `getSeamlessUser` call therefore sent `x-seamless-client-ip` with no accompanying service token, and the auth server ignores the forwarded IP unless a valid service token rides with it. Rate limiting, lockout, and anomaly detection attributed those requests to the adapter's egress IP instead of the end user's. This restores the behavior added in 0.7.0. - -`GetSeamlessUserOptions.authorization` is now optional, which matches what the adapter already passed: it resolves the user's access token from `req.cookiePayload` or `req.user`, both of which are unset when `getSeamlessUser` is called outside the auth router or the `requireAuth` guard. The required type was only satisfied by the same cast that hid the dropped service token. diff --git a/.changeset/magic-link-verify-gate.md b/.changeset/magic-link-verify-gate.md deleted file mode 100644 index 8658e98..0000000 --- a/.changeset/magic-link-verify-gate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@seamless-auth/core": patch ---- - -Stop the cookie gate from rejecting cross-device magic-link verification. `/magic-link/verify/:token` was prefix-matched by the `/magic-link` pre-auth cookie requirement, so a link opened on a device without the pre-auth or refresh cookie returned `400 Missing required cookie`. The token in the verify URL is the credential, so that route is now explicitly ungated while `/magic-link` (request) and `/magic-link/check` (poll) keep requiring the pre-auth cookie. diff --git a/.changeset/minor-correctness.md b/.changeset/minor-correctness.md deleted file mode 100644 index 82d3c70..0000000 --- a/.changeset/minor-correctness.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Fix assorted correctness bugs: - -- Magic link polling no longer returns a body with its 204 response. Express strips bodies on 204, so the message was never delivered. The 204 status is unchanged. -- `getSeamlessUser` no longer throws when the auth server returns a 200 with an empty body. It resolves to null instead. -- `/internal/auth-events/grouped` now forwards query params to the auth server, matching the summary and timeseries routes. Grouping and filter params were previously ignored. -- The bootstrap admin invite handler now surfaces string-shaped upstream errors instead of falling back to `bootstrap_failed`, and no longer throws when the request has no parsed body. diff --git a/.changeset/named-export-create-server.md b/.changeset/named-export-create-server.md deleted file mode 100644 index 73daf56..0000000 --- a/.changeset/named-export-create-server.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Export `createSeamlessAuthServer` by name. The symbol was imported into the package entry point and used only for the default export, so `import { createSeamlessAuthServer } from "@seamless-auth/express"` resolved to `undefined` and threw `TypeError: createSeamlessAuthServer is not a function`. That is the form used by the README Quick Start, every other README example, and the Express template scaffold, so the documented setup path did not run. The default export is unchanged and still works. - -The adapter README also documented a `getSeamlessUser` signature that no longer exists. It showed `getSeamlessUser(req, authServerUrl, cookieName?)`, while the real signature takes the same options object as `createSeamlessAuthServer`. Following the documented call reached the core secret check and threw on a missing `cookieSecret`. - -Both packages now carry a smoke test that imports every documented symbol by name from the built `dist`, so a named export that goes missing fails the suite instead of shipping. diff --git a/.changeset/oauth-provider-admin-routes.md b/.changeset/oauth-provider-admin-routes.md deleted file mode 100644 index e6bb16c..0000000 --- a/.changeset/oauth-provider-admin-routes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Proxy the new OAuth provider admin routes to the auth API: `GET`/`POST /system-config/oauth-providers` and `PATCH`/`DELETE /system-config/oauth-providers/:id`, all gated on the access identity. Register `/system-config/oauth-providers` in the core cookie requirements table so the ensureCookies middleware populates `req.cookiePayload` for both the collection and the id-scoped routes; without it the proxy never attaches the access token and the routes fail closed. diff --git a/.changeset/olive-schools-jam.md b/.changeset/olive-schools-jam.md deleted file mode 100644 index e384e01..0000000 --- a/.changeset/olive-schools-jam.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Correct published package metadata. Both packages now declare `engines.node` matching the Node 24 repo baseline, point `repository.url` at the repo root with a `directory` field, and declare a `bugs` URL. The express package gains the `homepage` field the core package already had. diff --git a/.changeset/origin-csrf-guard.md b/.changeset/origin-csrf-guard.md deleted file mode 100644 index b8c968d..0000000 --- a/.changeset/origin-csrf-guard.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@seamless-auth/express": minor ---- - -Add a router-level CSRF guard that rejects cross-site state-changing requests when the adapter issues `SameSite=None` cookies. - -The guard enforces `Sec-Fetch-Site` by default with no configuration: a non-safe request (anything other than GET, HEAD, or OPTIONS) is rejected with 403 when `Sec-Fetch-Site` is `cross-site`. Page JavaScript cannot forge that header and same-origin SPA calls send `same-origin` or `same-site`, so legitimate traffic passes untouched. When `Sec-Fetch-Site` is absent (older browsers), the request `Origin` is matched against the new opt-in `allowedOrigins` option; if `allowedOrigins` is unset the request passes, so nobody regresses. Server-to-server callers that send neither header pass, and a literal `null` origin is treated as cross-site. The guard only activates when the effective `sameSite` is `none`. diff --git a/.changeset/otp-generate-csrf.md b/.changeset/otp-generate-csrf.md deleted file mode 100644 index 2dfe5fb..0000000 --- a/.changeset/otp-generate-csrf.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@seamless-auth/express": minor ---- - -Serve the OTP generate routes over POST instead of GET. - -`GET /auth/otp/generate-phone-otp`, `-email-otp`, and their `-login-` variants were state-changing routes (each sends an SMS or email) reachable as a simple cross-site request, so an `` on any page could trigger unbounded OTP messages to a signed-in user. This is the same vector already closed for `/auth/magic-link`. - -BREAKING: the four `GET /auth/otp/generate-*` routes are removed and replaced with POST. Pair this with `@seamless-auth/react` 0.5.0 or later, which requests them over POST. An older SDK that still issues GET will get a 404. diff --git a/.changeset/path-encoding-containment.md b/.changeset/path-encoding-containment.md deleted file mode 100644 index 6b04ea2..0000000 --- a/.changeset/path-encoding-containment.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Close two path-encoding containment gaps. - -The Express console proxy relied on `new URL` normalizing `..` segments to keep requests inside the mounted subtree, but WHATWG `URL` does not decode `%2f` or `%5c`, so `/console/..%2fadmin/users` passed the prefix check and was forwarded upstream verbatim where a decoding upstream could escape the console subtree. The proxy now rejects any subpath containing an encoded path separator with a 400. - -The core `verifyMagicLinkHandler` interpolated its token into the upstream path without `encodeURIComponent`, unlike every sibling handler. A caller wiring it to a route param could send a traversal- or query-shaped token that reshaped the upstream request while carrying the caller's service authorization. The token is now encoded to a single path segment. diff --git a/.changeset/post-release-followups.md b/.changeset/post-release-followups.md deleted file mode 100644 index bfd170a..0000000 --- a/.changeset/post-release-followups.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@seamless-auth/core": minor -"@seamless-auth/express": minor ---- - -Post-release follow-up cleanups from the pre-release audit. - -- Bound the refresh-result cache in core. Entries were keyed by the rotating refresh cookie and never revisited, so the map grew without limit and retained tokens for the process lifetime. It now sweeps expired entries (throttled) and caps total size. -- Memoize the JWKS key set per auth-server URL in `verifySignedAuthResponse`. It was rebuilt on every call, so jose's key cache and refetch cooldown never engaged and every verification made an extra request to `/.well-known/jwks.json`. -- `SeamlessAuthUser.email` is now optional and `phone` is `string | null`, matching the cookie payload and the upstream `/users/me` shape. This is a type-level change: consumers that treated `phone` as a non-null `string` will need to handle `null`. -- Export `redactSensitiveText` from core and use it to mask tokens and secrets before the Express router logs an unhandled error. -- Reorder the `/magic-link/check` cookie requirement so it is no longer shadowed by `/magic-link`, throw a clear error when a route parameter is missing instead of forwarding the literal string `"undefined"`, correct the `Missing cookieSecret` message that named a removed environment variable, and drop a redundant terminal `.end()` after `res.json(...)`. diff --git a/.changeset/pre-release-doc-sweep.md b/.changeset/pre-release-doc-sweep.md deleted file mode 100644 index 5530c9f..0000000 --- a/.changeset/pre-release-doc-sweep.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Pre-release documentation and metadata corrections. The `requireRole` JSDoc example no longer calls `requireAuth()` with no arguments (which does not compile and throws), its malformed code fence is closed, and it now shares a constructed guard. The README Quick Start startup log matches its listen port, the `createSeamlessAuthServer` options block lists the `resolveClientIp` option, and the end-to-end flow references the real `webAuthn/login/finish` route. Both packages now declare `keywords` for npm discoverability. diff --git a/.changeset/refresh-audience.md b/.changeset/refresh-audience.md deleted file mode 100644 index b669277..0000000 --- a/.changeset/refresh-audience.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Fix the silent-refresh service token so it carries the M2M contract issuer and audience (`iss: seamless-portal-api`, `aud: seamless-auth`) instead of the adopter-configured issuer and the auth server URL. The auth API validates the forwarded service token with a fixed issuer and audience, so the previous values caused the token to be rejected and the real client IP to be dropped on refresh, breaking IP-based rate limiting and anomaly detection. diff --git a/.changeset/remove-dead-issuer-option.md b/.changeset/remove-dead-issuer-option.md deleted file mode 100644 index 3d19c96..0000000 --- a/.changeset/remove-dead-issuer-option.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@seamless-auth/express": minor ---- - -**Breaking:** remove the `issuer` option from `SeamlessAuthServerOptions`. Delete the `issuer` line from your `createSeamlessAuthServer(...)` call; no other change is needed. - -The option was required but write-only. Since the silent-refresh path moved to the fixed M2M contract constants (`iss: seamless-portal-api`, `aud: seamless-auth`), the adopter-supplied value reached nothing, so removing it changes no runtime behavior. `audience` is unaffected and stays required: it is enforced when verifying signed auth-server responses. - -The adapter README also documents a deployment constraint that produced an opaque failure: `authServerUrl` must exactly match the auth server's `ISSUER` environment variable, because signed responses are verified with `iss === authServerUrl`. Pointing the adapter at an internal address while the auth server issues its public URL fails every login with only a generic verification error. diff --git a/.changeset/remove-option-casts.md b/.changeset/remove-option-casts.md deleted file mode 100644 index 9a77ba0..0000000 --- a/.changeset/remove-option-casts.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Remove the redundant `as any` / option-object casts across the Express adapter so the compiler checks each option literal against its handler interface. These casts were the construct that previously let a mistyped option (`serviceAuthorization`) be silently dropped. No public API change. One internal cleanup with a visible edge case: the internal metrics handlers now reduce an array-valued query parameter to its first value rather than letting it reach the upstream comma-joined, which the scalar handler contract never supported. diff --git a/.changeset/require-express-5.md b/.changeset/require-express-5.md deleted file mode 100644 index 3205d66..0000000 --- a/.changeset/require-express-5.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@seamless-auth/express": minor ---- - -Require Express 5. - -BREAKING: the `express` and `@types/express` peer ranges are now `>=5.0.0`, up from `>=4.18.0` and `>=4.17.0`. Adopters still on Express 4 need to upgrade their application before taking this release. - -Under Express 4 a rejected handler promise was never routed anywhere, so an upstream failure (a network error reaching the auth server, for example) left the request hanging until the client timed out. Express 5 forwards rejected handler promises to error middleware, which makes those failures terminate properly. - -The router now also registers its own error middleware. The Express built-in handler answers with an HTML stack trace, including absolute server paths, whenever `NODE_ENV` is not `production`. Route errors now return `500` with a JSON `{ "error": "internal_error" }` body and are logged server side instead. diff --git a/.changeset/secret-hardening.md b/.changeset/secret-hardening.md deleted file mode 100644 index bad1994..0000000 --- a/.changeset/secret-hardening.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Validate secret strength at startup, warn on the dev JWKS key id, and stop logging the cookie payload. - -`cookieSecret` and `serviceSecret` were only checked for presence, so a short secret could be brute -forced offline and used to forge cookie sessions and service tokens. Both are now required to be at -least 32 characters. `createSeamlessAuthServer` and `createEnsureCookiesMiddleware` throw with a -clear message when a secret is missing or too short. - -This is a behavior change at startup: a deployment running with a weak secret will now fail fast -instead of starting. Generate replacements with a CSPRNG, for example `openssl rand -base64 48`. - -`jwksKid` still defaults to `dev-main`, but the adapter now logs a warning when the default is used -so an unconfigured key id is visible in production logs. - -`createSeamlessAuthServer` no longer passes `req.cookiePayload` to `console.warn` when a request is -missing its session subject. The payload could contain `token`, `sub`, and `roles`. diff --git a/.changeset/service-token-and-client-ip.md b/.changeset/service-token-and-client-ip.md deleted file mode 100644 index 6660442..0000000 --- a/.changeset/service-token-and-client-ip.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@seamless-auth/core": minor -"@seamless-auth/express": minor ---- - -Send a genuine machine-to-machine service token on proxied routes, and derive the forwarded client IP from a trusted hop. - -`authFetch` no longer falls back to `authorization` when no `serviceAuthorization` is given, so the browser user's access token is never placed in the `x-seamless-service-token` header. The user's identity now travels in `Authorization` only. `serviceAuthorization` is accepted by every core handler that already accepted `forwardedClientIp`. - -The Express adapter mints a real HS256 service token for proxied routes, signed with the configured `serviceSecret` and carrying the fixed `iss`/`aud` the auth server requires. The auth server only honors `x-seamless-client-ip` when a valid service token accompanies it, so client IP forwarding previously no-opped: IP-keyed rate limiters and audit records attributed proxied requests to the adapter's egress IP instead of the end user's. Tokens are reused for 45 seconds rather than signed per request. - -The forwarded client IP is now validated as a real IP address, and is dropped when Express `trust proxy` is set to blanket `true`, since `req.ip` is then taken from a client-supplied `X-Forwarded-For`. Set `trust proxy` to an explicit hop count or subnet. A new `resolveClientIp` option lets adopters derive the address themselves when their topology needs it. diff --git a/.changeset/verify-audience-binding.md b/.changeset/verify-audience-binding.md deleted file mode 100644 index a35f2f2..0000000 --- a/.changeset/verify-audience-binding.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@seamless-auth/core": patch -"@seamless-auth/express": patch ---- - -Bind the configured `audience` when verifying signed auth responses. `verifySignedAuthResponse` now enforces the `aud` claim in `jwtVerify`, and the login, finishLogin, finishRegister, OAuth, OTP, magic-link, and switch-organization handlers thread `SeamlessAuthServerOptions.audience` through to it. Previously only the issuer was checked, so on a multi-relying-party auth server a token minted by the same issuer for a different application would pass verification and be accepted as this app's session. diff --git a/.changeset/warn-on-missing-external-delivery.md b/.changeset/warn-on-missing-external-delivery.md deleted file mode 100644 index c84594a..0000000 --- a/.changeset/warn-on-missing-external-delivery.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@seamless-auth/express": patch ---- - -Warn when external delivery is requested but the auth server returns no delivery payload. The four auth-message routes (OTP email, OTP SMS, magic-link email, bootstrap invite email) previously fell through to a plain success response in that case, so a `serviceSecret` that does not match the auth server's `API_SERVICE_TOKEN` produced a successful-looking response with no message sent and nothing logged. The delivery branch shared by those routes is now a single `applyExternalDelivery` helper that logs a warning on a missing payload. Response bodies and status codes are unchanged. The messaging section of the README now documents `serviceSecret` as a prerequisite for auth-message delivery. diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index c269d91..108e3fe 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,92 @@ # @seamless-auth/core +## 0.9.0 + +### Minor Changes + +- de96f29: Breaking: remove the duplicate `sub` field from `SeamlessAuthUser`. + + `requireAuth` populated both `id` and `sub` on `req.user` from the same access token `sub` claim. + Only `id` remains, which is also the identifier exposed by `getSeamlessUser`, so both user sources + now agree on one field name. + + Adopters must replace `req.user.sub` with `req.user.id`. Any defensive `user.sub ?? user.id` + coalescing can be reduced to `user.id`. The `sub` claim inside JWT payloads is unchanged. + + `getSeamlessUser` also gains a real return type. It previously returned `any` by default, which is + what made that coalescing look necessary. It now returns the exported `SeamlessUser` interface + (`id`, `email`, `phone`, `roles`, plus optional `lastLogin` and `activeOrganizationId`), matching + the auth API's `GET /users/me` response. The generic parameter is unchanged for callers that pass + their own type. + +- d3e9274: Post-release follow-up cleanups from the pre-release audit. + + - Bound the refresh-result cache in core. Entries were keyed by the rotating refresh cookie and never revisited, so the map grew without limit and retained tokens for the process lifetime. It now sweeps expired entries (throttled) and caps total size. + - Memoize the JWKS key set per auth-server URL in `verifySignedAuthResponse`. It was rebuilt on every call, so jose's key cache and refetch cooldown never engaged and every verification made an extra request to `/.well-known/jwks.json`. + - `SeamlessAuthUser.email` is now optional and `phone` is `string | null`, matching the cookie payload and the upstream `/users/me` shape. This is a type-level change: consumers that treated `phone` as a non-null `string` will need to handle `null`. + - Export `redactSensitiveText` from core and use it to mask tokens and secrets before the Express router logs an unhandled error. + - Reorder the `/magic-link/check` cookie requirement so it is no longer shadowed by `/magic-link`, throw a clear error when a route parameter is missing instead of forwarding the literal string `"undefined"`, correct the `Missing cookieSecret` message that named a removed environment variable, and drop a redundant terminal `.end()` after `res.json(...)`. + +- 2627da4: Send a genuine machine-to-machine service token on proxied routes, and derive the forwarded client IP from a trusted hop. + + `authFetch` no longer falls back to `authorization` when no `serviceAuthorization` is given, so the browser user's access token is never placed in the `x-seamless-service-token` header. The user's identity now travels in `Authorization` only. `serviceAuthorization` is accepted by every core handler that already accepted `forwardedClientIp`. + + The Express adapter mints a real HS256 service token for proxied routes, signed with the configured `serviceSecret` and carrying the fixed `iss`/`aud` the auth server requires. The auth server only honors `x-seamless-client-ip` when a valid service token accompanies it, so client IP forwarding previously no-opped: IP-keyed rate limiters and audit records attributed proxied requests to the adapter's egress IP instead of the end user's. Tokens are reused for 45 seconds rather than signed per request. + + The forwarded client IP is now validated as a real IP address, and is dropped when Express `trust proxy` is set to blanket `true`, since `req.ip` is then taken from a client-supplied `X-Forwarded-For`. Set `trust proxy` to an explicit hop count or subnet. A new `resolveClientIp` option lets adopters derive the address themselves when their topology needs it. + +### Patch Changes + +- 9bae2bf: Register `/users/update`, `/users/credentials`, `/sessions`, and `/admin/credential-count` in the core cookie requirements table. Without these entries the ensureCookies middleware never populated `req.cookiePayload`, so `/users/update` and `/users/credentials` returned 401 and `/sessions` and `/admin/credential-count` failed to forward the access token upstream. +- 0672bd8: Move secret strength validation into the core and apply it to every entry point that accepts a secret. + + The 32 character minimum on `cookieSecret` and `serviceSecret` previously only guarded + `createSeamlessAuthServer` and `createEnsureCookiesMiddleware` in the Express adapter, so an adopter + calling a core function or `requireAuth` directly got no protection. + + `@seamless-auth/core` now owns the check and exports `MIN_SECRET_LENGTH`, `assertSecretStrength`, + and `assertSecrets`. It runs in `ensureCookies`, `refreshAccessToken`, `getSeamlessUser`, and + `createServiceToken`. The Express adapter re-uses the core implementation and adds it to + `requireAuth`, which previously only checked that `cookieSecret` was present. + + `verifyCookieJwt` and `verifyRefreshCookie` are deliberately unchanged. They are low-level + primitives with a documented "return `null` on failure" contract, and every code path in these + packages that reaches them validates the secret first. + + Adopters passing a secret shorter than 32 characters to any of these functions will now get a thrown + error naming the option. Generate replacements with a CSPRNG, for example `openssl rand -base64 48`. + +- cb84eb4: Encode user-derived path segments before interpolating them into upstream auth server URLs. + + Admin handlers, session handlers, and the Express `/magic-link/verify/:token` route interpolated route params directly into the upstream URL. A param carrying an encoded `?`, `#`, `;`, or `%2F` was decoded into the URL raw, so it could append or override upstream query params or reshape the upstream path. + + Every user-derived segment now goes through `encodeURIComponent`, matching the organization and OAuth routes. A param that previously reshaped the upstream request is now confined to a single path segment, which upstream rejects as an unknown id. + +- 4748a6b: Forward the service token from `getSeamlessUser`, so the client IP it sends is honored again. + + `GetSeamlessUserOptions` did not declare `serviceAuthorization` and the core `authFetch` call never passed it. The Express adapter still computed the service token and passed it, but an `as GetSeamlessUserOptions` cast on the option literal discarded it without a type error. Every `getSeamlessUser` call therefore sent `x-seamless-client-ip` with no accompanying service token, and the auth server ignores the forwarded IP unless a valid service token rides with it. Rate limiting, lockout, and anomaly detection attributed those requests to the adapter's egress IP instead of the end user's. This restores the behavior added in 0.7.0. + + `GetSeamlessUserOptions.authorization` is now optional, which matches what the adapter already passed: it resolves the user's access token from `req.cookiePayload` or `req.user`, both of which are unset when `getSeamlessUser` is called outside the auth router or the `requireAuth` guard. The required type was only satisfied by the same cast that hid the dropped service token. + +- c2746aa: Stop the cookie gate from rejecting cross-device magic-link verification. `/magic-link/verify/:token` was prefix-matched by the `/magic-link` pre-auth cookie requirement, so a link opened on a device without the pre-auth or refresh cookie returned `400 Missing required cookie`. The token in the verify URL is the credential, so that route is now explicitly ungated while `/magic-link` (request) and `/magic-link/check` (poll) keep requiring the pre-auth cookie. +- e9bd7a1: Fix assorted correctness bugs: + + - Magic link polling no longer returns a body with its 204 response. Express strips bodies on 204, so the message was never delivered. The 204 status is unchanged. + - `getSeamlessUser` no longer throws when the auth server returns a 200 with an empty body. It resolves to null instead. + - `/internal/auth-events/grouped` now forwards query params to the auth server, matching the summary and timeseries routes. Grouping and filter params were previously ignored. + - The bootstrap admin invite handler now surfaces string-shaped upstream errors instead of falling back to `bootstrap_failed`, and no longer throws when the request has no parsed body. + +- 3296263: Proxy the new OAuth provider admin routes to the auth API: `GET`/`POST /system-config/oauth-providers` and `PATCH`/`DELETE /system-config/oauth-providers/:id`, all gated on the access identity. Register `/system-config/oauth-providers` in the core cookie requirements table so the ensureCookies middleware populates `req.cookiePayload` for both the collection and the id-scoped routes; without it the proxy never attaches the access token and the routes fail closed. +- c53ab04: Correct published package metadata. Both packages now declare `engines.node` matching the Node 24 repo baseline, point `repository.url` at the repo root with a `directory` field, and declare a `bugs` URL. The express package gains the `homepage` field the core package already had. +- 44f98d0: Close two path-encoding containment gaps. + + The Express console proxy relied on `new URL` normalizing `..` segments to keep requests inside the mounted subtree, but WHATWG `URL` does not decode `%2f` or `%5c`, so `/console/..%2fadmin/users` passed the prefix check and was forwarded upstream verbatim where a decoding upstream could escape the console subtree. The proxy now rejects any subpath containing an encoded path separator with a 400. + + The core `verifyMagicLinkHandler` interpolated its token into the upstream path without `encodeURIComponent`, unlike every sibling handler. A caller wiring it to a route param could send a traversal- or query-shaped token that reshaped the upstream request while carrying the caller's service authorization. The token is now encoded to a single path segment. + +- 49e31f9: Pre-release documentation and metadata corrections. The `requireRole` JSDoc example no longer calls `requireAuth()` with no arguments (which does not compile and throws), its malformed code fence is closed, and it now shares a constructed guard. The README Quick Start startup log matches its listen port, the `createSeamlessAuthServer` options block lists the `resolveClientIp` option, and the end-to-end flow references the real `webAuthn/login/finish` route. Both packages now declare `keywords` for npm discoverability. +- c7f6a98: Bind the configured `audience` when verifying signed auth responses. `verifySignedAuthResponse` now enforces the `aud` claim in `jwtVerify`, and the login, finishLogin, finishRegister, OAuth, OTP, magic-link, and switch-organization handlers thread `SeamlessAuthServerOptions.audience` through to it. Previously only the issuer was checked, so on a multi-relying-party auth server a token minted by the same issuer for a different application would pass verification and be accepted as this app's session. + ## 0.7.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 2e67706..0b45fdf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@seamless-auth/core", - "version": "0.7.0", + "version": "0.9.0", "description": "Framework-agnostic core authentication logic for SeamlessAuth", "keywords": [ "authentication", diff --git a/packages/express/CHANGELOG.md b/packages/express/CHANGELOG.md index f6ccd51..9c97544 100644 --- a/packages/express/CHANGELOG.md +++ b/packages/express/CHANGELOG.md @@ -1,5 +1,189 @@ # @seamless-auth/express +## 0.9.0 + +### Minor Changes + +- de96f29: Breaking: remove the duplicate `sub` field from `SeamlessAuthUser`. + + `requireAuth` populated both `id` and `sub` on `req.user` from the same access token `sub` claim. + Only `id` remains, which is also the identifier exposed by `getSeamlessUser`, so both user sources + now agree on one field name. + + Adopters must replace `req.user.sub` with `req.user.id`. Any defensive `user.sub ?? user.id` + coalescing can be reduced to `user.id`. The `sub` claim inside JWT payloads is unchanged. + + `getSeamlessUser` also gains a real return type. It previously returned `any` by default, which is + what made that coalescing look necessary. It now returns the exported `SeamlessUser` interface + (`id`, `email`, `phone`, `roles`, plus optional `lastLogin` and `activeOrganizationId`), matching + the auth API's `GET /users/me` response. The generic parameter is unchanged for callers that pass + their own type. + +- 9bc928d: Fix two security issues in the Express adapter. + + Cookie clearing now mirrors the cookie set path. `clearSessionCookie` and + `clearAllCookies` previously emitted a clearing header with no `Secure` or + `SameSite`, which browsers reject in a cross-site response. In the default + cross-site deployment that meant logout returned 204 and revoked the session + upstream while the signed cookie survived in the browser and stayed valid for + every route guarded by `requireAuth` until its own TTL expired. + + BREAKING: `GET /auth/logout` and `GET /auth/magic-link` are removed. Both were + state-changing routes reachable as simple cross-site requests, so an `` + on any page could revoke all of a user's sessions or trigger magic-link emails. + Use `DELETE /auth/logout/all` in place of `GET /auth/logout`, and the new + `POST /auth/magic-link` in place of `GET /auth/magic-link`. + +- 9d25cca: Add a router-level CSRF guard that rejects cross-site state-changing requests when the adapter issues `SameSite=None` cookies. + + The guard enforces `Sec-Fetch-Site` by default with no configuration: a non-safe request (anything other than GET, HEAD, or OPTIONS) is rejected with 403 when `Sec-Fetch-Site` is `cross-site`. Page JavaScript cannot forge that header and same-origin SPA calls send `same-origin` or `same-site`, so legitimate traffic passes untouched. When `Sec-Fetch-Site` is absent (older browsers), the request `Origin` is matched against the new opt-in `allowedOrigins` option; if `allowedOrigins` is unset the request passes, so nobody regresses. Server-to-server callers that send neither header pass, and a literal `null` origin is treated as cross-site. The guard only activates when the effective `sameSite` is `none`. + +- 827b4ed: Serve the OTP generate routes over POST instead of GET. + + `GET /auth/otp/generate-phone-otp`, `-email-otp`, and their `-login-` variants were state-changing routes (each sends an SMS or email) reachable as a simple cross-site request, so an `` on any page could trigger unbounded OTP messages to a signed-in user. This is the same vector already closed for `/auth/magic-link`. + + BREAKING: the four `GET /auth/otp/generate-*` routes are removed and replaced with POST. Pair this with `@seamless-auth/react` 0.5.0 or later, which requests them over POST. An older SDK that still issues GET will get a 404. + +- d3e9274: Post-release follow-up cleanups from the pre-release audit. + + - Bound the refresh-result cache in core. Entries were keyed by the rotating refresh cookie and never revisited, so the map grew without limit and retained tokens for the process lifetime. It now sweeps expired entries (throttled) and caps total size. + - Memoize the JWKS key set per auth-server URL in `verifySignedAuthResponse`. It was rebuilt on every call, so jose's key cache and refetch cooldown never engaged and every verification made an extra request to `/.well-known/jwks.json`. + - `SeamlessAuthUser.email` is now optional and `phone` is `string | null`, matching the cookie payload and the upstream `/users/me` shape. This is a type-level change: consumers that treated `phone` as a non-null `string` will need to handle `null`. + - Export `redactSensitiveText` from core and use it to mask tokens and secrets before the Express router logs an unhandled error. + - Reorder the `/magic-link/check` cookie requirement so it is no longer shadowed by `/magic-link`, throw a clear error when a route parameter is missing instead of forwarding the literal string `"undefined"`, correct the `Missing cookieSecret` message that named a removed environment variable, and drop a redundant terminal `.end()` after `res.json(...)`. + +- 072d65a: **Breaking:** remove the `issuer` option from `SeamlessAuthServerOptions`. Delete the `issuer` line from your `createSeamlessAuthServer(...)` call; no other change is needed. + + The option was required but write-only. Since the silent-refresh path moved to the fixed M2M contract constants (`iss: seamless-portal-api`, `aud: seamless-auth`), the adopter-supplied value reached nothing, so removing it changes no runtime behavior. `audience` is unaffected and stays required: it is enforced when verifying signed auth-server responses. + + The adapter README also documents a deployment constraint that produced an opaque failure: `authServerUrl` must exactly match the auth server's `ISSUER` environment variable, because signed responses are verified with `iss === authServerUrl`. Pointing the adapter at an internal address while the auth server issues its public URL fails every login with only a generic verification error. + +- d1bbc6d: Require Express 5. + + BREAKING: the `express` and `@types/express` peer ranges are now `>=5.0.0`, up from `>=4.18.0` and `>=4.17.0`. Adopters still on Express 4 need to upgrade their application before taking this release. + + Under Express 4 a rejected handler promise was never routed anywhere, so an upstream failure (a network error reaching the auth server, for example) left the request hanging until the client timed out. Express 5 forwards rejected handler promises to error middleware, which makes those failures terminate properly. + + The router now also registers its own error middleware. The Express built-in handler answers with an HTML stack trace, including absolute server paths, whenever `NODE_ENV` is not `production`. Route errors now return `500` with a JSON `{ "error": "internal_error" }` body and are logged server side instead. + +- 2627da4: Send a genuine machine-to-machine service token on proxied routes, and derive the forwarded client IP from a trusted hop. + + `authFetch` no longer falls back to `authorization` when no `serviceAuthorization` is given, so the browser user's access token is never placed in the `x-seamless-service-token` header. The user's identity now travels in `Authorization` only. `serviceAuthorization` is accepted by every core handler that already accepted `forwardedClientIp`. + + The Express adapter mints a real HS256 service token for proxied routes, signed with the configured `serviceSecret` and carrying the fixed `iss`/`aud` the auth server requires. The auth server only honors `x-seamless-client-ip` when a valid service token accompanies it, so client IP forwarding previously no-opped: IP-keyed rate limiters and audit records attributed proxied requests to the adapter's egress IP instead of the end user's. Tokens are reused for 45 seconds rather than signed per request. + + The forwarded client IP is now validated as a real IP address, and is dropped when Express `trust proxy` is set to blanket `true`, since `req.ip` is then taken from a client-supplied `X-Forwarded-For`. Set `trust proxy` to an explicit hop count or subnet. A new `resolveClientIp` option lets adopters derive the address themselves when their topology needs it. + +### Patch Changes + +- 3342a66: Correct the adapter usage docs. The README Quick Start now passes every required option (`authServerUrl`, `cookieSecret`, `serviceSecret`, `audience`) and calls `requireAuth({ cookieSecret })`, so the copy-paste example runs. The Environment Variables table, which listed variables the adapter never reads (including unrelated database settings), is replaced with a configuration section stating that all settings are passed as options. The `requireAuth` JSDoc no longer claims the guard performs token refresh or documents a positional signature it does not accept, and its duplicated options interface is removed. +- 9bae2bf: Register `/users/update`, `/users/credentials`, `/sessions`, and `/admin/credential-count` in the core cookie requirements table. Without these entries the ensureCookies middleware never populated `req.cookiePayload`, so `/users/update` and `/users/credentials` returned 401 and `/sessions` and `/admin/credential-count` failed to forward the access token upstream. +- 9f12585: Drive cookie `Secure` and `SameSite` from explicit adapter options instead of ambient `NODE_ENV`. + + Session cookies previously only got `Secure` and `SameSite=None` when `process.env.NODE_ENV === "production"`, so a production deploy that forgot to set `NODE_ENV` shipped session cookies over plaintext HTTP with a weaker CSRF posture. + + `createSeamlessAuthServer` and `createEnsureCookiesMiddleware` now accept: + + - `cookieSecure?: boolean`, defaulting to `true` + - `cookieSameSite?: "lax" | "none" | "strict"`, defaulting to `none` when secure and `lax` otherwise + + Cookies are now secure by default in every environment. Set `cookieSecure: false` for local HTTP development. If you relied on the old behavior to develop over plain HTTP without setting `NODE_ENV=production`, add `cookieSecure: false` to your local configuration. + +- 0672bd8: Move secret strength validation into the core and apply it to every entry point that accepts a secret. + + The 32 character minimum on `cookieSecret` and `serviceSecret` previously only guarded + `createSeamlessAuthServer` and `createEnsureCookiesMiddleware` in the Express adapter, so an adopter + calling a core function or `requireAuth` directly got no protection. + + `@seamless-auth/core` now owns the check and exports `MIN_SECRET_LENGTH`, `assertSecretStrength`, + and `assertSecrets`. It runs in `ensureCookies`, `refreshAccessToken`, `getSeamlessUser`, and + `createServiceToken`. The Express adapter re-uses the core implementation and adds it to + `requireAuth`, which previously only checked that `cookieSecret` was present. + + `verifyCookieJwt` and `verifyRefreshCookie` are deliberately unchanged. They are low-level + primitives with a documented "return `null` on failure" contract, and every code path in these + packages that reaches them validates the secret first. + + Adopters passing a secret shorter than 32 characters to any of these functions will now get a thrown + error naming the option. Generate replacements with a CSPRNG, for example `openssl rand -base64 48`. + +- cb84eb4: Encode user-derived path segments before interpolating them into upstream auth server URLs. + + Admin handlers, session handlers, and the Express `/magic-link/verify/:token` route interpolated route params directly into the upstream URL. A param carrying an encoded `?`, `#`, `;`, or `%2F` was decoded into the URL raw, so it could append or override upstream query params or reshape the upstream path. + + Every user-derived segment now goes through `encodeURIComponent`, matching the organization and OAuth routes. A param that previously reshaped the upstream request is now confined to a single path segment, which upstream rejects as an unknown id. + +- 656288a: Stop rewriting client errors to 500 in the router error middleware. `express.json()` is mounted on the same router, so body-parser failures land in the catch-all. Those errors carry their own status (400 for `entity.parse.failed`, 413 for `entity.too.large`), and the middleware discarded it: a malformed JSON body answered `500 {"error":"internal_error"}` instead of 400. The middleware now honors a 4xx status from `status` or `statusCode` and answers with a generic body (`bad_request`, or `payload_too_large` for 413). Genuine 5xx failures are unchanged and still answer `500 {"error":"internal_error"}`. + + Client errors are also no longer written to `console.error`. Every malformed request produced an error-level log line, so an unauthenticated caller could generate unbounded error log volume. Only 5xx failures log now. + + The error object is still never serialized to the client. That matters here because `entity.parse.failed` errors carry a `body` property holding the raw payload, so echoing the parser error would reflect request content back. + +- 69ad64a: Fix `DELETE /users/credentials` proxying to the auth API as a `POST`. The adapter now forwards the request as a `DELETE`, matching the API's `deleteCredential` contract (the previous default hit `updateCredential` instead). +- 4748a6b: Forward the service token from `getSeamlessUser`, so the client IP it sends is honored again. + + `GetSeamlessUserOptions` did not declare `serviceAuthorization` and the core `authFetch` call never passed it. The Express adapter still computed the service token and passed it, but an `as GetSeamlessUserOptions` cast on the option literal discarded it without a type error. Every `getSeamlessUser` call therefore sent `x-seamless-client-ip` with no accompanying service token, and the auth server ignores the forwarded IP unless a valid service token rides with it. Rate limiting, lockout, and anomaly detection attributed those requests to the adapter's egress IP instead of the end user's. This restores the behavior added in 0.7.0. + + `GetSeamlessUserOptions.authorization` is now optional, which matches what the adapter already passed: it resolves the user's access token from `req.cookiePayload` or `req.user`, both of which are unset when `getSeamlessUser` is called outside the auth router or the `requireAuth` guard. The required type was only satisfied by the same cast that hid the dropped service token. + +- e9bd7a1: Fix assorted correctness bugs: + + - Magic link polling no longer returns a body with its 204 response. Express strips bodies on 204, so the message was never delivered. The 204 status is unchanged. + - `getSeamlessUser` no longer throws when the auth server returns a 200 with an empty body. It resolves to null instead. + - `/internal/auth-events/grouped` now forwards query params to the auth server, matching the summary and timeseries routes. Grouping and filter params were previously ignored. + - The bootstrap admin invite handler now surfaces string-shaped upstream errors instead of falling back to `bootstrap_failed`, and no longer throws when the request has no parsed body. + +- 6ea09c9: Export `createSeamlessAuthServer` by name. The symbol was imported into the package entry point and used only for the default export, so `import { createSeamlessAuthServer } from "@seamless-auth/express"` resolved to `undefined` and threw `TypeError: createSeamlessAuthServer is not a function`. That is the form used by the README Quick Start, every other README example, and the Express template scaffold, so the documented setup path did not run. The default export is unchanged and still works. + + The adapter README also documented a `getSeamlessUser` signature that no longer exists. It showed `getSeamlessUser(req, authServerUrl, cookieName?)`, while the real signature takes the same options object as `createSeamlessAuthServer`. Following the documented call reached the core secret check and threw on a missing `cookieSecret`. + + Both packages now carry a smoke test that imports every documented symbol by name from the built `dist`, so a named export that goes missing fails the suite instead of shipping. + +- 3296263: Proxy the new OAuth provider admin routes to the auth API: `GET`/`POST /system-config/oauth-providers` and `PATCH`/`DELETE /system-config/oauth-providers/:id`, all gated on the access identity. Register `/system-config/oauth-providers` in the core cookie requirements table so the ensureCookies middleware populates `req.cookiePayload` for both the collection and the id-scoped routes; without it the proxy never attaches the access token and the routes fail closed. +- c53ab04: Correct published package metadata. Both packages now declare `engines.node` matching the Node 24 repo baseline, point `repository.url` at the repo root with a `directory` field, and declare a `bugs` URL. The express package gains the `homepage` field the core package already had. +- 44f98d0: Close two path-encoding containment gaps. + + The Express console proxy relied on `new URL` normalizing `..` segments to keep requests inside the mounted subtree, but WHATWG `URL` does not decode `%2f` or `%5c`, so `/console/..%2fadmin/users` passed the prefix check and was forwarded upstream verbatim where a decoding upstream could escape the console subtree. The proxy now rejects any subpath containing an encoded path separator with a 400. + + The core `verifyMagicLinkHandler` interpolated its token into the upstream path without `encodeURIComponent`, unlike every sibling handler. A caller wiring it to a route param could send a traversal- or query-shaped token that reshaped the upstream request while carrying the caller's service authorization. The token is now encoded to a single path segment. + +- 49e31f9: Pre-release documentation and metadata corrections. The `requireRole` JSDoc example no longer calls `requireAuth()` with no arguments (which does not compile and throws), its malformed code fence is closed, and it now shares a constructed guard. The README Quick Start startup log matches its listen port, the `createSeamlessAuthServer` options block lists the `resolveClientIp` option, and the end-to-end flow references the real `webAuthn/login/finish` route. Both packages now declare `keywords` for npm discoverability. +- 7e65ca5: Fix the silent-refresh service token so it carries the M2M contract issuer and audience (`iss: seamless-portal-api`, `aud: seamless-auth`) instead of the adopter-configured issuer and the auth server URL. The auth API validates the forwarded service token with a fixed issuer and audience, so the previous values caused the token to be rejected and the real client IP to be dropped on refresh, breaking IP-based rate limiting and anomaly detection. +- 682c9f8: Remove the redundant `as any` / option-object casts across the Express adapter so the compiler checks each option literal against its handler interface. These casts were the construct that previously let a mistyped option (`serviceAuthorization`) be silently dropped. No public API change. One internal cleanup with a visible edge case: the internal metrics handlers now reduce an array-valued query parameter to its first value rather than letting it reach the upstream comma-joined, which the scalar handler contract never supported. +- 0a65ef6: Validate secret strength at startup, warn on the dev JWKS key id, and stop logging the cookie payload. + + `cookieSecret` and `serviceSecret` were only checked for presence, so a short secret could be brute + forced offline and used to forge cookie sessions and service tokens. Both are now required to be at + least 32 characters. `createSeamlessAuthServer` and `createEnsureCookiesMiddleware` throw with a + clear message when a secret is missing or too short. + + This is a behavior change at startup: a deployment running with a weak secret will now fail fast + instead of starting. Generate replacements with a CSPRNG, for example `openssl rand -base64 48`. + + `jwksKid` still defaults to `dev-main`, but the adapter now logs a warning when the default is used + so an unconfigured key id is visible in production logs. + + `createSeamlessAuthServer` no longer passes `req.cookiePayload` to `console.warn` when a request is + missing its session subject. The payload could contain `token`, `sub`, and `roles`. + +- c7f6a98: Bind the configured `audience` when verifying signed auth responses. `verifySignedAuthResponse` now enforces the `aud` claim in `jwtVerify`, and the login, finishLogin, finishRegister, OAuth, OTP, magic-link, and switch-organization handlers thread `SeamlessAuthServerOptions.audience` through to it. Previously only the issuer was checked, so on a multi-relying-party auth server a token minted by the same issuer for a different application would pass verification and be accepted as this app's session. +- 2b71440: Warn when external delivery is requested but the auth server returns no delivery payload. The four auth-message routes (OTP email, OTP SMS, magic-link email, bootstrap invite email) previously fell through to a plain success response in that case, so a `serviceSecret` that does not match the auth server's `API_SERVICE_TOKEN` produced a successful-looking response with no message sent and nothing logged. The delivery branch shared by those routes is now a single `applyExternalDelivery` helper that logs a warning on a missing payload. Response bodies and status codes are unchanged. The messaging section of the README now documents `serviceSecret` as a prerequisite for auth-message delivery. +- Updated dependencies [de96f29] +- Updated dependencies [9bae2bf] +- Updated dependencies [0672bd8] +- Updated dependencies [cb84eb4] +- Updated dependencies [4748a6b] +- Updated dependencies [c2746aa] +- Updated dependencies [e9bd7a1] +- Updated dependencies [3296263] +- Updated dependencies [c53ab04] +- Updated dependencies [44f98d0] +- Updated dependencies [d3e9274] +- Updated dependencies [49e31f9] +- Updated dependencies [2627da4] +- Updated dependencies [c7f6a98] + - @seamless-auth/core@0.9.0 + ## 0.8.0 ### Minor Changes diff --git a/packages/express/package.json b/packages/express/package.json index 24a3031..ef3bb9e 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,6 +1,6 @@ { "name": "@seamless-auth/express", - "version": "0.8.0", + "version": "0.9.0", "description": "Express adapter for Seamless Auth passwordless authentication", "keywords": [ "authentication",