Upgrading v0.1.x → v0.2.0

TL;DR — v0.2.0 is the MCP Authorization 2026-07-28 release, and it turns the spec-surface features on: DPoP, client credentials, token exchange and XAA now default to true. Three other changes bite a deployment that edits no config — a cross-client token exchange needs an operator allowlist entry, an origin-only CIMD client_id is refused, and resource at the token endpoint is now enforced. Config and database from v0.1.2 work unchanged otherwise; migration 004 is additive and applies on first start.

These docs describe v0.2.x. Pages that document a feature whose default moved say so inline (“on by default since v0.2.0; on v0.1.x set …”), so you can read them while still on the old line. This page is the whole delta in one place.

The authoritative list, with the wire-level detail, is the authserver CHANGELOG.

Decide this before you upgrade

1. Four defaults flipped on

Config keyEnv varv0.1.xv0.2.0
dpop.enabledAUTHPLANE_DPOP_ENABLEDfalsetrue
client_credentials.enabledAUTHPLANE_CLIENT_CREDENTIALS_ENABLEDfalsetrue
token_exchange.enabledAUTHPLANE_TOKEN_EXCHANGE_ENABLEDfalsetrue
xaa.enabledAUTHPLANE_XAA_ENABLED (new — YAML-only on v0.1.x)falsetrue

A stock v0.1.x server advertised only authorization_code and refresh_token; a stock v0.2.0 server advertises all four surfaces, and the discovery document grows accordingly. The Helm chart follows suit from 0.4.0.

Three things this does not mean:

  • dpop.enabled means supported, not required. A client that sends no proof still gets a plain bearer token. Enforcement is a separate, SDK-side switch — see Enable DPoP.
  • Grants are still gated by client registration. Turning a grant on widens what a client may register for; it grants nothing retroactively to clients already registered.
  • XAA validates nothing until you register a trusted IdP via POST /admin/idps. See Cross-app access (XAA).

To keep the v0.1.x posture, set the ones you don’t want to false explicitly, before or during the upgrade:

dpop:                { enabled: false }
client_credentials:  { enabled: false }
token_exchange:      { enabled: false }
xaa:                 { enabled: false }

2. A cross-client token exchange needs an allowlist entry

A client presenting a token minted for a different client — typically an MCP server exchanging a user’s web-app or agent token for a Mint resource token — must now be named on the target Resource. On v0.1.x an empty allowlist meant “any client may act”, so any client holding another client’s token could spend that client’s consent.

PATCH /admin/resources/{id}
{"policy": {"exchange": {"allowed_client_ids": ["<exchanging-client-id>"]}}}

policy.runtime.client_ids counts too. Without one of them the exchange is refused with access_denied — not consent_required, because re-prompting the user would not fix it.

Unaffected: a client exchanging a token issued to itself, fronted exchanges, Broker resources, and an MCP server exchanging for its own resource where it already sits in runtime.client_ids. Full walkthrough in Token Vault.

3. A CIMD client_id URL needs https and a path

Per the 2026-07-28 client-registration rules. An origin-only identifier (https://example.com) collapses every client on that domain into one identity and one consent record, so it is refused at /oauth/authorize with invalid_client. Dot-segments and empty segments do not count as a path. Re-register affected clients against a metadata URL that has one.

4. resource at the token endpoint is enforced

On the authorization-code and refresh paths it used to be read and dropped — the audience came from the session, so a client naming a different resource got a token for the one it authorized, with a 200. A resource the grant does not cover is now refused with invalid_target (RFC 8707 §2.2), before the code or refresh token is consumed. Omitting it is unchanged.

The likeliest way to hit this is a spelling difference: resource URIs match exactly, and a trailing slash counts.

5. Two CIMD hardening changes

  • cimd.require_https: false is only accepted with a localhost issuer. The server refuses to boot otherwise. Both shipped configs already use a localhost issuer.
  • cimd.require_https no longer governs SSRF address filtering — the new cimd.allow_private_addresses (default false, AUTHPLANE_CIMD_ALLOW_PRIVATE_ADDRESSES) does. If you run require_https: false and serve CIMD documents from loopback, a container network or a LAN address, add allow_private_addresses: true or the fetch is blocked.

Running the upgrade

Same mechanics as any release — see Backup, upgrade, purge. Two specifics for this one:

  • Migration 004 applies on first start and is additive: a nullable application_type column on clients. Existing rows stay NULL and read as web.
  • Schedule authserver purge if you haven’t. serve runs no purge goroutines, and three of the four now-on features write expirable rows (dpop_nonces, machine_tokens, assertion_jti). On v0.1.x this only mattered if you had enabled one; on a stock v0.2.0 deployment it always does. Recipes in Scheduled purge.

What you get

Nothing below requires a config change.

  • Protected Resource Metadata (RFC 9728) is served for every registered Resource, at GET /.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/{ref} — ref being the §3.1 path suffix of the Resource URI, or its slug. A resource server that can’t host well-known paths itself now has a conformant document to point its WWW-Authenticate: resource_metadata at. See Resource servers and PRM.
  • RFC 9207 iss on every authorization response, success and error alike, plus authorization_response_iss_parameter_supported: true in discovery. Clients compare the value byte for byte.
  • authorization_grant_profiles_supported in discovery, listing urn:ietf:params:oauth:grant-profile:id-jag — the field the stable MCP Enterprise-Managed Authorization extension tells clients to check.
  • POST /oauth/register accepts and persists application_type (web or native). MCP clients are required to send it; one that omits it is defaulted to web and told so in the response — which under OIDC refuses the loopback redirect URIs native clients need.
  • The whole xaa.* block is configurable from the environment: AUTHPLANE_XAA_ENABLED, _TOKEN_EXPIRY, _MAX_ASSERTION_AGE, _REQUIRE_RESOURCE, _SUBJECT_MODE, _JWKS_CACHE_TTL. It was YAML-only on v0.1.x. See Configuration reference.
  • CIMD documents are cached according to their own HTTP cache headers (Cache-Control, Expires), bounded above by cimd.cache_ttl and below by a short floor; no-store is honoured. cimd.cache_ttl is a ceiling now, not a fixed lifetime.

Deprecated and removed

  • identity_assertion_supported in AS metadata is deprecated — still emitted this release, removed in v0.3.0. Read authorization_grant_profiles_supported instead.
  • Dynamic Client Registration as the primary registration path is deprecated per the specification, which prefers CIMD — no registration endpoint needed, and on by default. DCR still works; the server now warns at boot when dcr.mode is open. Set dcr.mode: admin_only or approved_redirects once your clients register via CIMD.
  • may_act is removed. Its writer went long ago; the reader, claim field and docs were unreachable leftovers. Operator impact: none.

Embedding authserver as a Go library? The release also changes a handful of exported signatures — Fetcher.SetAllowLoopback and AccessTokenClaims.MayAct are gone, ssrf.NewSafeTransport is variadic, and api/public.NewServer panics without an IssuerProvider. The CHANGELOG lists them under Go API (embedders only).

Coming from v0.1.0 or v0.1.1?

Upgrade through v0.1.2 first, or read its breaking changes alongside this page: introspection now checks who is asking (each resource server that introspects must be authorized before you upgrade), rate_limit.enabled: false no longer disables account lockout, and the Helm probes moved to GET /livez. They’re in the same CHANGELOG.