Skip to content

feat(auth): DCR hooks, introspection auth, and prehandler fixes #99

@getlarge

Description

@getlarge

Summary

When integrating with Ory Hydra for OAuth authentication, several issues prevent DCR (Dynamic Client Registration) and token introspection from working correctly.

This issue covers three related fixes that build on #95 (OIDC Discovery):

Problem 1: /oauth/register requires authentication

File: src/auth/prehandler.ts

The DCR endpoint /oauth/register is not in the auth skip list, but DCR is the first step before a client has credentials. This creates a chicken-and-egg problem.

Fix: Add /oauth/register to the skip list alongside /oauth/authorize and /oauth/callback.

Problem 2: Token introspection authentication

File: src/auth/token-validator.ts

Current behavior sends no Authorization header to the introspection endpoint. Ory Hydra's admin introspection endpoint (/admin/oauth2/introspect) requires an API key.

Fix: Add introspectionAuth configuration:

tokenValidation: {
  introspectionEndpoint: 'https://ory.example.com/admin/oauth2/introspect',
  introspectionAuth: {
    type: 'bearer',
    token: 'ory-api-key'
  }
  // Also supports: { type: 'basic', clientId, clientSecret } or { type: 'none' }
}

Problem 3: DCR infinite loop when acting as proxy

Files: src/routes/auth-routes.ts, src/types/auth-types.ts

When the authorization server's registration_endpoint (from OIDC discovery) points to the MCP server itself (a valid pattern for adding custom logic like response cleaning), fastify-mcp tries to forward to itself → infinite loop → crash.

Use case: Ory returns empty strings (client_uri: "") in DCR responses that break Claude Code's Zod validation. The MCP server needs to proxy DCR requests to clean the response.

Fix: Add dcrHooks configuration:

authorization: {
  enabled: true,
  dcrHooks: {
    // REQUIRED: bypasses OIDC discovery to avoid loop
    upstreamEndpoint: 'https://ory.example.com/oauth2/register',
    
    onRequest: async (request, log) => {
      // Transform request before forwarding
      return request;
    },
    
    onResponse: async (response, request, log) => {
      // Clean empty fields that break clients
      if (response.client_uri === '') delete response.client_uri;
      return response;
    }
  }
}

When dcrHooks is not configured, /oauth/register returns 501 Not Implemented (prevents accidental loops).

Breaking Change

/oauth/register now returns 501 unless dcrHooks is configured. This is intentional - the previous behavior would cause infinite loops when OIDC discovery pointed back to the MCP server.

Files Changed

  • src/types/auth-types.ts - Add IntrospectionAuthConfig, DCRRequest, DCRResponse, DCRHooks types
  • src/auth/prehandler.ts - Add /oauth/register to skip list
  • src/auth/token-validator.ts - Support introspectionAuth config
  • src/routes/auth-routes.ts - Implement DCR proxy with hooks
  • src/index.ts - Export new types, pass dcrHooks to auth routes

Related


I have a working implementation tested against Ory Hydra. Happy to submit a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions