1111
1212import { toNodeHandler } from 'better-auth/node' ;
1313import { oAuthDiscoveryMetadata , oAuthProtectedResourceMetadata } from 'better-auth/plugins' ;
14+ import cors from 'cors' ;
1415import type { Request , Response as ExpressResponse , Router } from 'express' ;
1516import express from 'express' ;
1617
@@ -25,6 +26,12 @@ export interface SetupAuthServerOptions {
2526 * Examples should be used for **demo** only and not for production purposes, however this mode disables some logging and other features.
2627 */
2728 demoMode : boolean ;
29+ /**
30+ * Enable verbose logging of better-auth requests/responses.
31+ * WARNING: This may log sensitive information like tokens and cookies.
32+ * Only use for debugging purposes.
33+ */
34+ dangerousLoggingEnabled ?: boolean ;
2835}
2936
3037// Store auth instance globally so it can be used for token verification
@@ -79,7 +86,7 @@ async function ensureDemoUserExists(auth: DemoAuth): Promise<void> {
7986 * @param options - Server configuration
8087 */
8188export function setupAuthServer ( options : SetupAuthServerOptions ) : void {
82- const { authServerUrl, mcpServerUrl, demoMode } = options ;
89+ const { authServerUrl, mcpServerUrl, demoMode, dangerousLoggingEnabled = false } = options ;
8390
8491 // Create better-auth instance with MCP plugin
8592 const auth = createDemoAuth ( {
@@ -96,54 +103,68 @@ export function setupAuthServer(options: SetupAuthServerOptions): void {
96103 const authApp = express ( ) ;
97104
98105 // Enable CORS for all origins (demo only) - must be before other middleware
99- authApp . use ( ( _req , res , next ) => {
100- res . header ( 'Access-Control-Allow-Origin' , '*' ) ;
101- res . header ( 'Access-Control-Allow-Methods' , 'GET, POST, OPTIONS' ) ;
102- res . header ( 'Access-Control-Allow-Headers' , 'Content-Type, Authorization' ) ;
103- res . header ( 'Access-Control-Expose-Headers' , 'WWW-Authenticate' ) ;
104- if ( _req . method === 'OPTIONS' ) {
105- res . sendStatus ( 200 ) ;
106- return ;
107- }
108- next ( ) ;
109- } ) ;
106+ // WARNING: This configuration is for demo purposes only. In production, you should restrict this to specific origins and configure CORS yourself.
107+ authApp . use (
108+ cors ( {
109+ origin : '*' // WARNING: This allows all origins to access the auth server. In production, you should restrict this to specific origins.
110+ } )
111+ ) ;
110112
111- // Request logging middleware for OAuth endpoints
112- authApp . use ( '/api/auth' , ( req , res , next ) => {
113- const timestamp = new Date ( ) . toISOString ( ) ;
114- console . log ( `${ timestamp } [Auth Request] ${ req . method } ${ req . url } ` ) ;
115- if ( req . method === 'POST' ) {
116- console . log ( `${ timestamp } [Auth Request] Content-Type: ${ req . headers [ 'content-type' ] } ` ) ;
117- }
113+ // Create better-auth handler
114+ // toNodeHandler bypasses Express methods
115+ const betterAuthHandler = toNodeHandler ( auth ) ;
118116
119- if ( demoMode ) {
120- // Log response when it finishes
121- const originalSend = res . send . bind ( res ) ;
122- res . send = function ( body ) {
123- console . log ( `${ timestamp } [Auth Response] ${ res . statusCode } ${ req . url } ` ) ;
124- if ( res . statusCode >= 400 && body ) {
125- try {
126- const parsed = typeof body === 'string' ? JSON . parse ( body ) : body ;
127- console . log ( `${ timestamp } [Auth Response] Error:` , parsed ) ;
128- } catch {
129- // Not JSON, log as-is if short
130- if ( typeof body === 'string' && body . length < 200 ) {
131- console . log ( `${ timestamp } [Auth Response] Body: ${ body } ` ) ;
132- }
117+ // Mount better-auth handler BEFORE body parsers
118+ // toNodeHandler reads the raw request body, so Express must not consume it first
119+ if ( dangerousLoggingEnabled ) {
120+ // Verbose logging mode - intercept at Node.js level to see all requests/responses
121+ // WARNING: This may log sensitive information like tokens and cookies
122+ authApp . all ( '/api/auth/{*splat}' , ( req , res ) => {
123+ const ts = new Date ( ) . toISOString ( ) ;
124+ console . log ( `\n${ '=' . repeat ( 60 ) } ` ) ;
125+ console . log ( `${ ts } [AUTH] ${ req . method } ${ req . originalUrl } ` ) ;
126+ console . log ( `${ ts } [AUTH] Query:` , JSON . stringify ( req . query ) ) ;
127+ console . log ( `${ ts } [AUTH] Headers.Cookie:` , req . headers . cookie ?. slice ( 0 , 100 ) ) ;
128+
129+ // Intercept writeHead to capture status and headers (including redirects)
130+ const originalWriteHead = res . writeHead . bind ( res ) ;
131+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
132+ res . writeHead = function ( statusCode : number , ...args : any [ ] ) {
133+ console . log ( `${ ts } [AUTH] >>> Response Status: ${ statusCode } ` ) ;
134+ // Headers can be in different positions depending on the overload
135+ const headers = args . find ( a => typeof a === 'object' && a !== null ) ;
136+ if ( headers ) {
137+ if ( headers . location || headers . Location ) {
138+ console . log ( `${ ts } [AUTH] >>> Location (redirect): ${ headers . location || headers . Location } ` ) ;
133139 }
140+ console . log ( `${ ts } [AUTH] >>> Headers:` , JSON . stringify ( headers ) ) ;
134141 }
135- return originalSend ( body ) ;
142+ return originalWriteHead ( statusCode , ... args ) ;
136143 } ;
137- }
138- next ( ) ;
139- } ) ;
140144
141- // Mount better-auth handler BEFORE body parsers
142- // toNodeHandler reads the raw request body, so Express must not consume it first
143- authApp . all ( '/api/auth/{*splat}' , toNodeHandler ( auth ) ) ;
145+ // Intercept write to capture response body
146+ const originalWrite = res . write . bind ( res ) ;
147+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148+ res . write = function ( chunk : any , ...args : any [ ] ) {
149+ if ( chunk ) {
150+ const bodyPreview = typeof chunk === 'string' ? chunk . slice ( 0 , 500 ) : chunk . toString ( ) . slice ( 0 , 500 ) ;
151+ console . log ( `${ ts } [AUTH] >>> Body: ${ bodyPreview } ` ) ;
152+ }
153+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154+ return originalWrite ( chunk , ...( args as [ any ] ) ) ;
155+ } ;
156+
157+ return betterAuthHandler ( req , res ) ;
158+ } ) ;
159+ } else {
160+ // Normal mode - no verbose logging
161+ authApp . all ( '/api/auth/{*splat}' , toNodeHandler ( auth ) ) ;
162+ }
144163
145164 // OAuth metadata endpoints using better-auth's built-in handlers
146- authApp . get ( '/.well-known/oauth-authorization-server' , toNodeHandler ( oAuthDiscoveryMetadata ( auth ) ) ) ;
165+ // Add explicit OPTIONS handler for CORS preflight
166+ authApp . options ( '/.well-known/oauth-authorization-server' , cors ( ) ) ;
167+ authApp . get ( '/.well-known/oauth-authorization-server' , cors ( ) , toNodeHandler ( oAuthDiscoveryMetadata ( auth ) ) ) ;
147168
148169 // Body parsers for non-better-auth routes (like /sign-in)
149170 authApp . use ( express . json ( ) ) ;
@@ -239,14 +260,25 @@ export function setupAuthServer(options: SetupAuthServerOptions): void {
239260 * This is needed because MCP clients discover the auth server by first
240261 * fetching protected resource metadata from the MCP server.
241262 *
263+ * Per RFC 9728 Section 3, the metadata URL includes the resource path.
264+ * E.g., for resource http://localhost:3000/mcp, metadata is at
265+ * http://localhost:3000/.well-known/oauth-protected-resource/mcp
266+ *
242267 * See: https://www.better-auth.com/docs/plugins/mcp#oauth-protected-resource-metadata
268+ *
269+ * @param resourcePath - The path of the MCP resource (e.g., '/mcp'). Defaults to '/mcp'.
243270 */
244- export function createProtectedResourceMetadataRouter ( ) : Router {
271+ export function createProtectedResourceMetadataRouter ( resourcePath = '/mcp' ) : Router {
245272 const auth = getAuth ( ) ;
246273 const router = express . Router ( ) ;
247274
248- // Serve at the standard well-known path
249- router . get ( '/.well-known/oauth-protected-resource' , toNodeHandler ( oAuthProtectedResourceMetadata ( auth ) ) ) ;
275+ // Construct the metadata path per RFC 9728 Section 3
276+ const metadataPath = `/.well-known/oauth-protected-resource${ resourcePath } ` ;
277+
278+ // Enable CORS for browser-based clients to discover the auth server
279+ // Add explicit OPTIONS handler for CORS preflight
280+ router . options ( metadataPath , cors ( ) ) ;
281+ router . get ( metadataPath , cors ( ) , toNodeHandler ( oAuthProtectedResourceMetadata ( auth ) ) ) ;
250282
251283 return router ;
252284}
0 commit comments