Repository navigation
Self-hosted OAuth 2.1 Authorization Server for the Model Context Protocol.
Security and signing-key patch release. No new config keys, schema
migrations, metrics or audit actions; two metrics and one audit action gain
new reason values, and the token.revoked detail changes format. Several
signing-key behaviours change: read the upgrade notes first if you run the
Helm chart with more than one replica, run authserver admin key, use the
keyfile store, or share one Vault between signing and data encryption.
Upgrade notes
- Helm chart 0.4.3 refuses to render,
helm upgradeincluded, when the
keyfile signing store would sit in a per-podemptyDir
(persistence.enabled: false) withreplicaCount > 1or
autoscaling.enabled: every pod had its own key, and tokens from one pod
failed verification against another. Switch toconfig.signing.key_store: postgres_key(which also needs PostgreSQL storage, the chart default, and
config.data_encryptionconfigured) or Vault Transit
(vault.signing.enabled: true), or run one replica with
persistence.enabled: true. Each of these starts with a new signing key,
so tokens issued before the change stop verifying. authserver admin key rotateandadmin key listrefuse to run
unless they load a config file (--config) or
AUTHPLANE_SIGNING_KEY_STORE/AUTHPLANE_SIGNING_KEY_PATHis set.
Without one they used built-in defaults, which on the Helm chart, and for
any server whose config file names another key store or path, is a key
store the server never reads. On the Helm chart, pass--config /etc/authplane/config.yaml. A container started with no config file and
no signing variables, like the README'sdocker run … serve, does use the
defaults: there, rundocker exec -e AUTHPLANE_SIGNING_KEY_STORE=keyfile <container> /authserver admin key rotate. Their first output line is now
key store: ….- Vault Transit signing on the same Vault address and Transit mount as
vault_transit_encryptdata encryption now authenticates with its own
signing.vault_transittoken or AppRole, which it used to ignore. Check
that this credential can read and sign with the signing key before
upgrading: a wrong AppRole, or a Vault the AppRole login cannot reach,
fails the boot; a token without access starts the server, and token
requests and the JWKS endpoint then fail. - A Vault Transit signing key of a type that cannot sign
signing.algorithm(for exampleecdsa-p256withRS256, or an RSA
key withES256) now stops the server at startup. If the key cannot be
read within two seconds at boot, the server starts, and such a key then
fails every token request and JWKS fetch. A symmetric Transit key is not
caught at startup; signing with it fails, as before. A mismatched key
never produced a token that verified. - The keyfile store adds a lock file,
<key dir>/.lock, taken on every
read and write on Unix systems; it coordinates processes on one host. Keys
in a directory the server cannot write, such as a read-only mount, are
still served; rotating there fails with an error. A current key that
cannot be used (wrong type for its algorithm, a missing companion file, a
PEM that does not parse) still fails every token request, but is no longer
published in the JWKS (a type mismatch was) or takes the JWKS down (a
missing file or bad PEM did): the previous key stays published.
authserver admin key listreports such a key: it labels the remaining
keypreviousand exits with an error naming the fix,authserver admin key rotate, which replaces the key and keeps the previous one; it warns
about an unusable previous key. - The JWKS endpoint and
admin key listno longer create a signing key.
The server creates one at startup, waiting up to ten seconds for the key
store, or on the first token request if the key store is unreachable then.
Until a key exists,/.well-known/jwks.jsonreturns an empty key set and
admin key listprintsno signing keys found. /oauth/revokeanswers503 temporarily_unavailablewith
Retry-Afterwhen the server cannot look up or record a revocation; it
used to answer200, in some cases for a token that stayed valid. Clients
should retry. Access tokens that 0.2.2 accepted at/oauth/revokewithout
revoking (see Security) stay valid until they expire.- Token exchange: a token this server mints by exchange now expires no
later than its subject token, soexpires_incan be shorter than the
configured lifetime (a Broker resource still returns the upstream
provider's own token). A subject token with less than a second left,
including one past itsexpbut still inside the verifier's 30-second
clock-skew allowance, is refused withinvalid_grant. - Metrics and audit (update alerts and queries that match on these
values):authserver_tokens_revoked_totaland thetoken.revokedaudit event
count a token once, on the call that first revokes it; repeated revokes,
and revokes that change nothing, no longer count. The metric gains the
reasonexplicit_accessfor an access token that has no machine-token
row, andexplicit_machinenow means the call revoked that row.- The
token.revokeddetail for an access token is nowaccess_token jti=… revoked_in=…(wasmachine_token jti=…). token.exchange_deniedandauthplane_token_exchange_denied_totalgain
the reasonsactor_token_revokedandsubject_token_expired. A
revocation lookup that fails is no longer recorded as
subject_token_revoked; the exchange fails as a server error.
- An issuer URL with userinfo, such as
https://localhost:@auth.example.com, is no longer treated as localhost,
so the production config checks apply and can fail the boot. - Building from source needs Go 1.26.9 or later.
Security
/oauth/revokeanswered200for access tokens from the
authorization-code and refresh grants and from token exchange to a
registered resource, and wrote atoken.revokedaudit event for them, but
did not revoke them: they stayed active at introspection and as a
token-exchangesubject_token. Atoken.revokedevent for such a token
in a 0.2.2 audit log does not mean it was revoked. An access token revoked
by the client it was issued to is now revoked until it expires./oauth/revokealso answered200when it failed to revoke a
refresh-token family, to denylist that family's access tokens, or to mark
a machine token revoked, telling the client a token was revoked while it
stayed valid. It now answers503(see Upgrade notes).- Token exchange accepted a revoked
subject_tokenissued by the jwt-bearer
grant or by a token exchange withoutresource, and never checked the
actor_tokenfor revocation. Both are now refused, and a failed
machine-token or denylist lookup refuses the exchange instead of allowing
it. - A token from a token exchange was valid for the full configured lifetime,
so exchanging the newest token in a chain extended access indefinitely.
Its expiry is now capped at the subject token's (a Broker resource still
returns the upstream provider's own token). /connect/{provider}accepted aresourcebelonging to another provider,
so a link could send a signed-in user to one provider with another
resource's scopes, overwrite their stored grant for that provider, and
redirect them to the other resource's return URL. The request now fails
withinvalid_target, and the callback checks again before exchanging the
code.authserver purgedeleted revocation records as soon as a token'sexp
passed, while the verifier still accepts the token for 30 seconds of clock
skew; in that window a revoked token introspected active and could be
exchanged. The records are now kept until the token can no longer verify.- Introspection ignored a failed read of the machine-token store, so a
revoked token could introspect active while the store was failing. It now
answersactive: false. - Built with Go 1.26.9 and
golang.org/x/netv0.60.0, closing eleven
advisories reachable from the server: GO-2026-6599 and GO-2026-6600
(html/template); GO-2026-6603, GO-2026-6605, GO-2026-6610, GO-2026-6611,
GO-2026-6612, GO-2026-6613 and GO-2026-6617 (net/httpand
golang.org/x/net); GO-2026-6607 (crypto/tls); GO-2026-6608
(net/textprotoandmime/multipart).
Fixed
- Signing keys are published when they change. A key rotated outside the
running server (byauthserver admin key rotate, in Vault, or on a
replica whose change notification was missed) could stay unpublished while
the server signed with it, so its tokens failed verification. On
postgres_keythis lasted until a restart, a SIGHUP or the next
key-change notification; on keyfile and Vault Transit, once the server had
created the first key, rotated through the admin API or reloaded on
SIGHUP, until the next restart or SIGHUP. The server now reloads its JWKS
before signing with a key it has not published, and every other server
picks the change up within 60 seconds (withpostgres_key, as soon as its
notification arrives, and again whenever its listener reconnects). SIGHUP
also refreshes the cachedpostgres_keysigning key. The JWKS response is
still served withCache-Control: max-age=3600, so a verifier that
honours it can keep an older key set for up to an hour. - The server signs with the key the store holds. On Vault Transit, when
the server created the Transit key itself, the request that created it was
signed with a locally generated key no JWKS carried, and a rotation
reported a key ID that did not exist. Several requests arriving before the
first key existed could each create one, which on Vault Transit rotated
the new key at once and on PostgreSQL demoted another replica's new key.
The server now reads the key back from the store after every write, and
creates a first key only when none exists. - Keyfile writes are atomic and coordinated. A rotation could be read
half-written by a concurrent token request or JWKS fetch, interleave with
authserver admin key rotatein the same container, or leave no current
key after a crash. Key files are now written atomically, in an order that
never leaves the current key missing, under a lock shared with the CLI on
the same host. A crash during a rotation that changessigning.algorithm
can still leave a current key that fails to load; the previous key stays
published, and the next rotation replaces it. An unusable current or
previous key file no longer makes every JWKS request fail: the usable key
is still published. - RS256 tokens signed with Vault Transit verify. Vault signed them with
RSA-PSS while the token and the JWKS said RS256. ES256, the default, was
not affected. - A
postgres_keyrotation keeps the key that was signing. With replica
clocks out of step, or two overlapping rotations, a rotation could delete
the key that signed every outstanding token. - Helm chart: the install notes warn when a single pod keeps its keyfile
signing key in anemptyDir(a new key, and invalid earlier tokens, on
every restart) and when keyfile runs on a persistent volume with more than
one replica. - Signing-key documentation describes what the server does: exactly one
previous key is kept until the next rotation, so a leaked key needs two
rotations (or, on Vault Transit, one rotation and
min_decryption_version); the JWKS cache header; that onlyPOST /admin/keys/rotatewrites akey.rotatedaudit event; which Vault
capabilities creating and rotating the Transit key need; and that
signing.postgres_key.encryption_key_envis not used. The configuration
and systemd guide examples that definedserver:twice, and the audit-log
jqexamples, are fixed.
Full Changelog: v0.2.2...v0.2.3