Authorization Server - ChristianPresley/zig-sdk GitHub Wiki

Authorization Server

mcp.auth.AuthorizationServer is an OAuth 2.11 authorization server for MCP deployments. It issues JWT access tokens for the MCP servers of the deployment. The resource server helpers of the SDK verify these tokens with its JWK set. See Authorization for the client side.

Applies to: MCP 2026-07-28, zig-sdk 0.2.0.

What the server does

  • It serves the metadata2 at /.well-known/oauth-authorization-server and at the OpenID Connect paths.
  • The authorization endpoint runs the code flow with PKCE S2563 and the iss parameter4.
  • The token endpoint offers authorization_code, refresh_token, client_credentials and the JWT bearer grant5 for ID-JAGs and workload JWTs.
  • Each grant can give a DPoP-bound token6.
  • /jwks publishes the public signing keys.

The tokens have the typ header at+jwt10. The aud claim is the MCP server7.

Steps to run the server

  1. Load a signing key, or make one with generateSigningKey(io).
  2. Write the options: the issuer, the keys, the MCP servers, the scopes and the authorizer.
  3. Call AuthorizationServer.init.
  4. Call listen and serve. Or call handle from the HTTP server of your application.
  5. Give the public keys of the server to the JwtVerifier of each MCP server. Set its token_type to at+jwt.

In the same process, verificationKeys(arena) gives the keys for the JwtVerifier. Another process reads the JWK set at /jwks with mcp.auth.jwt.parseJwks.

const keys = [_]as.SigningKey{.{ .key = &key, .kid = "2026-10" }};
const resources = [_]as.Resource{.{ .uri = "https://mcp.example.com/mcp" }};
var server = try mcp.auth.AuthorizationServer.init(io, gpa, .{
    .issuer = "https://auth.example.com",
    .signing_keys = &keys,
    .resources = &resources,
    .scopes_supported = &.{ "mcp:read", "mcp:write" },
    .authorizer = .{ .userdata = &app, .decide = App.decide },
});
defer server.deinit();
try server.listen(.{ .port = 8443, .tls = &tls_server });
try server.serve();

Here as is mcp.auth.authorization_server. The example examples/authorization_server.zig runs the server next to an MCP server.

Options

Option What it does
issuer An https URL without a query, a fragment or a slash at the end.
signing_keys The first key signs. The JWK set has all keys, for a key rotation.
resources The MCP servers. A request without resource gets the first one.
scopes_supported, default_scopes The scopes that clients can get, and the scopes of a request without scope.
authorizer User authentication and consent. Null disables the authorization endpoint.
clients The clients to register at the start.
grants The grants, and the validators of the JWT bearer grant.
dpop DPoP at the token endpoint, with an optional nonce.
dynamic_registration Null by default.
client_metadata Client ID metadata documents8. Enabled by default.
store The store. Null uses a MemoryStore.
allow_http Accept a loopback http issuer. Use it for tests only.

The lifetimes are 900 seconds for access tokens, 60 seconds for codes and 30 days for a grant family.

User authentication and consent

The server checks the client, the redirect URI, PKCE, the resource and the scopes. Then it calls the decide function of the authorizer. The function returns approve with a subject, deny, or respond with a page of the application. A consent form sends its answer to the authorization endpoint with POST. hiddenFields gives the OAuth parameters for the form. consentToken and checkConsentToken bind the form to a secret value of the browser.

AutoApprove approves each request for one subject. Use it for tests only.

Clients

The client authentication methods are none, client_secret_basic, client_secret_post and private_key_jwt. The jti of a client assertion is single use. A client_id that is an https URL names a client ID metadata document. MCP deprecates dynamic client registration9, so the server disables it by default.

The storage interface

authorization_store.Store has the functions get_client, put_client, put_code, take_code, put_refresh_token, take_refresh_token, revoke_family and record_jti. A store keeps SHA-256 hashes of codes, refresh tokens and client secrets. MemoryStore has a limit for each map and removes expired records.

Security notes

  • The server compares redirect URIs exactly. A loopback http URI can have any port11.
  • The server refuses PKCE plain and a request without a challenge.
  • A code that comes a second time revokes the refresh tokens of its grant.
  • The server rotates refresh tokens. A used token that comes again revokes its grant family.
  • The fetch of a client document accepts only https and public addresses. It does not follow redirects, and it has a size limit and a time limit.
  • Each token response has Cache-Control: no-store. The log has no tokens or secrets.

Conformance

The conformance suite has two authorization server scenarios: authorization-server-metadata-endpoint and authorization-code-grant. The CI job conformance runs both against the fixture mcp-conformance-authorization-server, and both pass. See Conformance.

Limits

  • A revocation stops refresh tokens. The access tokens stay valid until they expire.
  • The server has no introspection and no revocation endpoint.
  • A request names one resource only.
  • MemoryStore loses all records at a restart.

References

  1. OAUTH21-DRAFT: The OAuth 2.1 Authorization Framework.
  2. RFC8414: OAuth 2.0 Authorization Server Metadata.
  3. RFC7636: Proof Key for Code Exchange.
  4. RFC9207: OAuth 2.0 Authorization Server Issuer Identification.
  5. RFC7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants.
  6. RFC9449: OAuth 2.0 DPoP.
  7. RFC8707: Resource Indicators for OAuth 2.0.
  8. OAUTH-CIMD-DRAFT: Client ID Metadata Documents.
  9. RFC7591: OAuth 2.0 Dynamic Client Registration Protocol.
  10. RFC9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens.
  11. RFC8252: OAuth 2.0 for Native Apps.
⚠️ **GitHub.com Fallback** ⚠️