Skip to content

feat(oauth): authorization server metadata, JWKS, and OIDC discovery endpoints#152

Merged
EsTharian merged 3 commits intomainfrom
feat/oauth-discovery-metadata
Apr 24, 2026
Merged

feat(oauth): authorization server metadata, JWKS, and OIDC discovery endpoints#152
EsTharian merged 3 commits intomainfrom
feat/oauth-discovery-metadata

Conversation

@EsTharian
Copy link
Copy Markdown
Member

Closes #148.

Summary

  • Adds /.well-known/oauth-authorization-server (RFC 8414), /.well-known/jwks.json (RFC 7517), and /.well-known/openid-configuration (OIDC Discovery 1.0), each unauthenticated and cacheable (Cache-Control: public, max-age=3600).
  • Introduces a jwks helper in libs/server/jwt and extends the JWT Fastify plugin with getIssuer / getJwks / keyId.
  • Discovery metadata is composed from JWT_ISSUER (no new env vars). registration_endpoint and refresh_token are advertised per the issue (sibling PRs wire the actual backing).

Caveats for reviewers

  • scopes_supported is a compile-time default (openid profile email offline_access). Realm-aware lookup is flagged as a TODO — appropriate follow-up once the authorize route consults a realm-level scope registry.
  • Response zod schemas intentionally omitted on discovery routes so the wire body can carry unknown fields (RFC 8414 / OIDC both permit extensions).
  • JWKS round-trip verification test lives in libs/server/jwt (+ JWT plugin), not in the route test — kept jose out of auth-server's direct deps.

Test plan

  • pnpm nx test — 3 projects, 136 tests passing (17 new)
  • pnpm nx lint / pnpm nx build auth-server
  • Manual: hit /.well-known/oauth-authorization-server and verify RFC 8414 shape via a public validator
  • Manual: fetch /.well-known/jwks.json, verify a qauth-issued JWT against it using jose

Adds `exportPublicJwk` to @qauth-labs/server-jwt so callers can convert
imported EdDSA public keys into RFC 7517 JWKs with `use: 'sig'`,
`alg: 'EdDSA'`, and an optional `kid`. Defensive strip of the `d` member
prevents accidental private-key leakage if a caller passes the wrong key.

Prepares the JWT plugin for serving `/.well-known/jwks.json` (issue #148).
Adds `getIssuer()` and `getJwks()` to the Fastify JWT plugin so routes
can publish RFC 8414 / OIDC Discovery metadata and RFC 7517 JWKS without
reaching into the plugin internals. An optional `keyId` plugin option
feeds the JWKS `kid` member so future key rotation is a config change.

Supports issue #148 (discovery endpoints).
Publishes three unauthenticated well-known documents so clients (incl.
MCP clients kicking off OAuth on 401) can auto-discover endpoints and
signing keys:

- GET /.well-known/oauth-authorization-server (RFC 8414)
- GET /.well-known/openid-configuration (OIDC Discovery 1.0)
- GET /.well-known/jwks.json (RFC 7517)

All three return Cache-Control: public, max-age=3600, and the JWKS
uses application/jwk-set+json. Endpoint URLs derive from JWT_ISSUER
so operators configure discovery by setting the issuer, not per-endpoint
env vars. Metadata advertises the OAuth 2.1 surface: response_types
limited to code, code_challenge_methods limited to S256, grant_types
including refresh_token (sibling branch) and registration_endpoint
(#149) so discovery stays stable as those land.

Closes #148.
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 24, 2026

View your CI Pipeline Execution ↗ for commit a662efc

Command Status Duration Result
nx affected -t lint test build ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-24 08:42:12 UTC

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements OAuth 2.0 and OpenID Connect discovery endpoints, including metadata builders and a JWKS provider. The changes update the JWT plugin to expose necessary issuer and key information and include comprehensive tests for the new functionality. Feedback suggests using a regular expression for more robust trailing slash removal in the issuer URL and optimizing the discovery routes by pre-calculating static metadata and refining the JWKS content-type header for RFC compliance.

Comment on lines +93 to +95
function stripTrailingSlash(url: string): string {
return url.endsWith('/') ? url.slice(0, -1) : url;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of stripTrailingSlash only removes a single trailing slash. Using a regular expression is more robust and ensures compliance with RFC 8414 §2, which recommends that the issuer identifier does not include a trailing slash, even if multiple are accidentally provided in the configuration.

Suggested change
function stripTrailingSlash(url: string): string {
return url.endsWith('/') ? url.slice(0, -1) : url;
}
function stripTrailingSlash(url: string): string {
return url.replace(/\/+$/, '');
}

Comment on lines +25 to +78
const buildInput = () => ({ issuer: fastify.jwtUtils.getIssuer() });

fastify.get(
'/.well-known/oauth-authorization-server',
{
schema: {
description:
'OAuth 2.0 Authorization Server Metadata (RFC 8414). Unauthenticated, cacheable.',
tags: ['Discovery'],
},
},
async (_request, reply) => {
reply
.header('Cache-Control', DISCOVERY_CACHE_CONTROL)
.header('Content-Type', 'application/json; charset=utf-8');
return reply.send(buildAuthorizationServerMetadata(buildInput()));
}
);

fastify.get(
'/.well-known/openid-configuration',
{
schema: {
description:
'OpenID Connect Discovery 1.0 document. Superset of RFC 8414 AS metadata with OIDC-specific fields. Unauthenticated, cacheable.',
tags: ['Discovery'],
},
},
async (_request, reply) => {
reply
.header('Cache-Control', DISCOVERY_CACHE_CONTROL)
.header('Content-Type', 'application/json; charset=utf-8');
return reply.send(buildOpenIdConfiguration(buildInput()));
}
);

fastify.get(
'/.well-known/jwks.json',
{
schema: {
description:
'JSON Web Key Set (RFC 7517) containing the active EdDSA public signing key(s). Used by clients and resource servers to verify JWT signatures.',
tags: ['Discovery'],
},
},
async (_request, reply) => {
// Public keys can be served by any cache; see RFC 7517 §8.5.1.
// `application/jwk-set+json` is the registered media type.
reply
.header('Cache-Control', DISCOVERY_CACHE_CONTROL)
.header('Content-Type', 'application/jwk-set+json; charset=utf-8');
const jwks = await fastify.jwtUtils.getJwks();
return reply.send(jwks);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The discovery metadata and OIDC configuration are static based on the server configuration. Re-building these objects on every request is inefficient. Pre-calculating them during route registration improves performance. Additionally, the charset=utf-8 parameter is removed from the application/jwk-set+json content type as it is not defined for that media type in RFC 7517.

  const issuer = fastify.jwtUtils.getIssuer();
  const asMetadata = buildAuthorizationServerMetadata({ issuer });
  const oidcConfig = buildOpenIdConfiguration({ issuer });

  fastify.get(
    '/.well-known/oauth-authorization-server',
    {
      schema: {
        description:
          'OAuth 2.0 Authorization Server Metadata (RFC 8414). Unauthenticated, cacheable.',
        tags: ['Discovery'],
      },
    },
    async (_request, reply) => {
      return reply
        .header('Cache-Control', DISCOVERY_CACHE_CONTROL)
        .send(asMetadata);
    }
  );

  fastify.get(
    '/.well-known/openid-configuration',
    {
      schema: {
        description:
          'OpenID Connect Discovery 1.0 document. Superset of RFC 8414 AS metadata with OIDC-specific fields. Unauthenticated, cacheable.',
        tags: ['Discovery'],
      },
    },
    async (_request, reply) => {
      return reply
        .header('Cache-Control', DISCOVERY_CACHE_CONTROL)
        .send(oidcConfig);
    }
  );

  fastify.get(
    '/.well-known/jwks.json',
    {
      schema: {
        description:
          'JSON Web Key Set (RFC 7517) containing the active EdDSA public signing key(s). Used by clients and resource servers to verify JWT signatures.',
        tags: ['Discovery'],
      },
    },
    async (_request, reply) => {
      const jwks = await fastify.jwtUtils.getJwks();
      return reply
        .header('Cache-Control', DISCOVERY_CACHE_CONTROL)
        .header('Content-Type', 'application/jwk-set+json')
        .send(jwks);
    }
  );

@EsTharian EsTharian merged commit ee6891d into main Apr 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(oauth): authorization server metadata, JWKS, and OIDC discovery endpoints

1 participant