Skip to content

v1.0.0-beta.69

Choose a tag to compare

@github-actions github-actions released this 14 Oct 09:20
e188e00

v1.0.0-beta.69 is a security release. It binds tokens, keys and sessions to their realm, splits rate limiting into two stages, and tightens login, provisioning and HTTP defaults. It also adds a realm-scoped token endpoint. These notes cover the changes since v1.0.0-beta.68.

📣 Some News

This release is mostly the outcome of a security review of the whole server, shipped as three pull requests (#3690, #3691, #3692). Most of the changes need no action. Some of them can break an existing setup, and a few sign users out once. The upgrade guide sorts every change into "may need action", "users sign in again" and "no action". Read at least its first group before you upgrade.

👀 Highlights

🛡️ Tokens, keys and sessions are bound to their realm

A token is now accepted only when its subject and its session belong to the realm the token names. This applies to API requests, the refresh grant, introspection and RP-initiated logout. Tokens Authup issues always meet this, so a correctly configured client notices nothing.

The realm signing key used to prove only the realm, while sub and session_id were whatever the key holder signed. So importing signature key material now needs a key-create grant that reaches beyond your own realm, which the global admin role has. Realm administrators can still generate signature keys but can no longer import them.

Disabling or deleting a signature key now ends the tokens it signed right away, instead of at their expiry. Enabling the key again makes its unexpired tokens valid again.

Warning

Signature keys imported under the earlier rules stay trusted. Review your imported signature keys, and disable or rotate any that a realm administrator imported. On a Redis cache, drop the oauth2_token_claims* keys once after upgrading. Otherwise a token verified before the upgrade is served from that cache without the new check until it expires.

The server adapters (@authup/server-adapter-*) in local mode now fetch the signing key from the token's own realm and refuse a token without realm_id.

📖 Tokens are bound to their realm

🚦 Rate limiting runs in two stages

Before authentication, a source address may cause 300 failed authentications a minute: a wrong Basic password, a wrong client secret, or a bearer this deployment never signed. Valid requests do not count. Neither does an expired or revoked token, a token whose signing key was disabled, or a request without credentials. Those are what thousands of users behind one NAT address produce in ordinary use, because every access token is renewed after a 401.

After authentication, a request counts against its identity rather than its address, so users who share an address no longer share a budget. Each budget has its own key:

core:
    rateLimitAnonymousMax: 1200   # per source address, env RATE_LIMIT_ANONYMOUS_MAX
    rateLimitUserMax: 6000        # per user, env RATE_LIMIT_USER_MAX
    rateLimitClientMax: 300000    # per client, env RATE_LIMIT_CLIENT_MAX

A sign-in is anonymous until it succeeds, so raise rateLimitAnonymousMax when many people sign in from one address, such as a hospital or an office network.

Warning

With trustProxy: true (the default) the limiter keys on the X-Forwarded-For value a client sends, so a direct client picks its own bucket. Set trustProxy to the hop count or the proxy address of your deployment.

Note

A numeric max in middlewareRateLimit still replaces all three budgets. Prefer the dedicated keys. The Swagger UI at /docs is now limited by the anonymous budget.

📖 Rate limiting configuration

🔑 HTTP Basic names resolve in a known realm

A Basic user or client name used to match in whatever realm held it. It is now looked up in the route realm and then the master realm on /realms/<realm>/..., and in the master realm alone on every other route. A master operator therefore keeps managing other realms by name, and a realm administrator who creates their own admin user can no longer shadow the master one.

- curl -u svc-reports:secret https://auth.example.com/users
+ curl -u svc-reports:secret https://auth.example.com/realms/tenant-a/users

Sending the identity's id instead of its name works on every route, since an id resolves globally.

With userAuthBasic and mfaEnabled both on, Basic user credentials are refused for a user who holds a confirmed authenticator. Scripts acting as such a user must authenticate as a confidential client.

📖 HTTP Basic changes

🏷️ Realm-scoped token endpoint

The token endpoint is now also mounted at /realms/<realm>/token, next to /token. The route realm acts as the realm hint and wins over a realm_id or realm_name in the body, for every grant and for introspection and revocation.

curl -X POST https://auth.example.com/realms/tenant-a/token \
    -d grant_type=password \
    -d username=alice \
    -d password=start123

A route realm that does not exist answers 404. A missing body hint on /token still falls back to the master realm. The route scopes name resolution only: a user or client addressed by its id still resolves in its own realm. Discovery keeps advertising /token.

📖 Realm-scoped token endpoint

🧩 Grants travel with the identity in policy data

IdentityPermissionBindingPolicyEvaluator no longer loads grants through a provider in the middle of an evaluation. It reads them from the identity it evaluates, identity.grants, so an evaluation depends on its data alone. This matters if you build your own PolicyEngine or evaluator from @authup/access:

- const evaluator = new IdentityPermissionBindingPolicyEvaluator(identityPermissionProvider);
+ const evaluator = new IdentityPermissionBindingPolicyEvaluator();

  const data = new PolicyData({
-     [BuiltInPolicyType.IDENTITY]: { type: 'user', id: user.id },
+     [BuiltInPolicyType.IDENTITY]: { type: 'user', id: user.id, grants },
  });

An identity without grants is denied with DATA_MISSING. On the server, a failed grant read now fails the request with its real error instead of answering 403.

📖 Read grants from policy data

👋 Some users sign in again

  • On https deployments served at the root of their host, the console session cookie is now named __Host-authup_session, so console users sign in once more after upgrading. http and sub-path deployments keep authup_session.
  • Resetting a password ends every session of the user. Changing a password ends every other session.
  • Deactivating a user or client ends its sessions and refuses its access tokens right away, whatever wrote the deactivation, USER_ADMIN_ENABLED=false included.
  • An authorization or device code minted before a sign-out, a password change or a deactivation can no longer be redeemed.
  • The kit store writes its token cookies with SameSite=Lax, and with Secure on https pages. Behind a TLS-terminating proxy, send X-Forwarded-Proto: https so server-side writes in @authup/client-web-nuxt get Secure too.

📖 Users sign in again, sessions end

🧰 Smaller fixes worth knowing about

  • /token/introspect and /token/revoke refuse a token query parameter (400). Send it in the body.
  • Authorization codes are 256-bit and expire after 5 minutes, independent of the access-token lifetime.
  • Activation and reset codes are stored as a SHA-256 digest. A data migration converts issued codes, but on sqlite a link mailed before the upgrade stops working.
  • POST /password-forgot answers an unknown user with the same 202 and body as a known one.
  • attributes policies can no longer be written with $regex (400). Existing policies are unaffected.
  • Request bodies are capped at 10mb after decompression, and middlewareBody, middlewareCookie and middlewareQuery are now honoured.
  • SMTP verifies the relay certificate for every form. A self-signed relay needs rejectUnauthorized: false.
  • Entity events on the redis and socket.io bus no longer carry select: false columns such as user email and client secrets.
  • LDAP filter templates insert login names and entry values as literal values.
  • With the failed-login throttle on, concurrent attempts beyond the threshold are refused while the others run.
  • MySQL db.replication is supported again: every master and slave session runs in UTC.
  • create-authup sets worker.enabled in every helm values file, since chart 0.5.0 enables the worker by default.

⚠️ Heads-Up Before Upgrading

Change What to do
Tokens bound to their realm Review imported signature keys and rotate any a realm administrator imported. On Redis, drop the oauth2_token_claims* keys once.
Signature key import needs a reach beyond the own realm Import signature keys as a global admin. Realm administrators can still generate them.
Basic names resolve in the route realm or master Call /realms/<realm>/... or authenticate by id when the identity lives outside the master realm.
Basic user credentials refused under MFA Use a confidential client for scripts acting as a user with a confirmed authenticator.
Provisioning createOnly binds relations only on create Use strategy: merge where roles, permissions or scopes must reach an existing row.
realm_admin can no longer edit global roles' permissions Expect 403 there. A customized reach is left alone.
Client reads are realm-gated A grant at own or ownOrNull no longer sees other realms' clients.
Cross-realm policy references refused Rebind a client whose accessPolicyId names another realm's policy. Such a client now denies every login.
SMTP verifies certificates Add rejectUnauthorized: false for a relay with an untrusted certificate.
USER_ADMIN_ENABLED is strict Use a recognized boolean. false now deactivates an existing default admin.
Rate limiter trusts X-Forwarded-For by default Pin trustProxy, and raise rateLimitAnonymousMax for many users behind one address.
token query parameter refused Send token in the form body to /token/introspect and /token/revoke.
$regex refused in attributes policies Use $startsWith, $endsWith, $in or equality.
Bus payloads drop hidden columns Read email and secrets through the API in bus consumers.
IdentityPermissionBindingPolicyEvaluator takes no argument Put the grants on identity.grants in the policy data you evaluate.

⬆️ Upgrading

For an installation using the operator CLI:

npm install authup@1.0.0-beta.69 --save-exact

Update the @authup/* SDK packages your application consumes to the same release. Stop older replicas before newer ones serve activation and password reset, since an older replica still writes codes in the old form. The Node.js requirement remains ^22.13.0 || ^23.5.0 || >=24.0.0.

📖 Upgrade guide

👉 Changelog

Compare v1.0.0-beta.68...v1.0.0-beta.69

⚠ BREAKING CHANGES

  • server-core: a script authenticating over HTTP Basic by name as a user or client outside the master realm must use its own realm's nested routes or the identity's id.
  • IdentityPermissionBindingPolicyEvaluator takes no constructor argument and reads the identity's grants from identity.grants; an evaluation without them denies with DATA_MISSING. server-core's PolicyEngine and OAuth2AccessPolicyEvaluator no longer take an identity permission provider.
  • harden the HTTP layer, domain events and client cookies (security hardening 3/3) (#3692)
  • harden login workflows, authorization reads and provisioning (security hardening 2/3) (#3691)
  • bind tokens, keys and sessions to their realm (security hardening 1/3) (#3690)

Features

  • server-core: realm-scoped token endpoint (#3686) (9c43a11)

Bug Fixes

  • beta.69 release blockers (rate limit, login throttle, helm worker, release-as) (#3718) (240ddac)
  • bind tokens, keys and sessions to their realm (security hardening 1/3) (#3690) (3d031be)
  • deps: bump source-map-js from 1.2.1 to 1.2.2 in /docs (#3712) (1af4a68)
  • deps: bump the minorandpatch group with 30 updates (#3716) (a615381)
  • harden login workflows, authorization reads and provisioning (security hardening 2/3) (#3691) (ea60877)
  • harden the HTTP layer, domain events and client cookies (security hardening 3/3) (#3692) (6f33cd6)
  • remove some unnecessary sidebar items (802ecd2)
  • server-core: resolve Basic names in the route realm or master (#3715) (da615bd)

Code Refactoring

  • read the identity's grants from policy data (#3702) (231bd6b)