All files / src/oauth routes.ts

100% Statements 21/21
100% Branches 4/4
100% Functions 1/1
100% Lines 21/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94              36x 36x                   36x 36x         36x                               36x         24x     24x   24x         24x 24x 1x 1x 2x             24x     24x       24x       24x     24x     24x               24x    
/**
 * OAuth HTTP routes
 *
 * The routing table of the OAuth endpoints, shared by the server and the HTTP-level
 * tests so both exercise the same handlers on the same paths.
 */
 
import express, { Express } from 'express';
import {
  metadataHandler,
  protectedResourceHandler,
  authorizeHandler,
  pollHandler,
  callbackHandler,
  tokenHandler,
  registerHandler,
  revokeHandler,
} from './index';
import { loadOAuthConfig } from './config';
import {
  authorizationServerMetadataUrl,
  protectedResources,
  resourceMetadataUrl,
} from './resource';
import { logInfo } from '../logger';
 
/**
 * Register OAuth endpoints on an Express app
 *
 * Adds:
 * - /.well-known/oauth-authorization-server - OAuth metadata
 * - /.well-known/oauth-protected-resource - Protected resource metadata (RFC 9470)
 * - /authorize - Authorization endpoint (supports both Device Flow and Authorization Code Flow)
 * - /oauth/poll - Device flow polling endpoint
 * - /oauth/callback - Authorization Code Flow callback from GitLab
 * - /token - Token exchange endpoint
 * - /health - Health check endpoint
 *
 * @param app - Express application
 */
export function registerOAuthEndpoints(app: Express): void {
  // NOTE: Rate limiting is applied via rateLimiterMiddleware() BEFORE this function is called.
  // All routes registered here are protected by the global rate limiter middleware.
 
  // OAuth discovery metadata (no auth required)
  app.get('/.well-known/oauth-authorization-server', metadataHandler);
 
  // Protected Resource Metadata (RFC 9470) - required by Claude.ai custom connectors
  app.get('/.well-known/oauth-protected-resource', protectedResourceHandler);
  // Metadata of the /mcp endpoint (RFC 9728 section 3.1 path-inserted form)
  app.get('/.well-known/oauth-protected-resource/mcp', protectedResourceHandler);
 
  // An issuer with a path (a proxy serving this server under a prefix and stripping it)
  // has its metadata at path-inserted URLs on the origin (RFC 8414 section 3.1, RFC 9728
  // section 3.1); the forms above cover the root issuer.
  const issuer = loadOAuthConfig()?.issuer;
  if (issuer && new URL(issuer).pathname !== '/') {
    app.get(new URL(authorizationServerMetadataUrl(issuer)).pathname, metadataHandler);
    for (const resource of protectedResources(issuer)) {
      app.get(new URL(resourceMetadataUrl(resource)).pathname, protectedResourceHandler);
    }
  }
 
  // Authorization endpoint - supports both flows:
  // - Device Flow (no redirect_uri) - returns HTML page
  // - Authorization Code Flow (with redirect_uri) - redirects to GitLab
  app.get('/authorize', authorizeHandler);
 
  // Device flow polling endpoint (no auth required)
  app.get('/oauth/poll', pollHandler);
 
  // Authorization Code Flow callback from GitLab
  // GitLab redirects here after user authorizes, then we redirect to client
  app.get('/oauth/callback', callbackHandler);
 
  // Token endpoint - exchange code for tokens (no auth required)
  // Uses URL-encoded body as per OAuth spec
  app.post('/token', express.urlencoded({ extended: true }), tokenHandler);
 
  // Dynamic Client Registration endpoint (RFC 7591) - required by Claude.ai
  app.post('/register', express.json(), registerHandler);
 
  // Token revocation (RFC 7009): disconnects the account
  app.post('/revoke', express.urlencoded({ extended: true }), revokeHandler);
 
  // NOTE: /health endpoint is registered globally in startServer() BEFORE OAuth endpoints
  // to avoid access log spam from load balancer health checks. The simple handler there
  // returns {"status": "ok"} which is sufficient for basic liveness/readiness checks.
  // For structured MCP metadata (version, tools, auth mode, instances), use GET /
  // with Accept: application/json header (dashboard endpoint).
 
  logInfo('OAuth endpoints registered');
}