fix(auth): add OAuth2 routes for OIDC discovery compatibility

Better Auth's OIDC discovery document advertises endpoints at
/api/auth/oauth2/* paths. Add routes for these native paths to
ensure Matrix Synapse and other OIDC clients can complete the
authorization flow.

Routes added:
- GET /api/auth/oauth2/authorize
- POST /api/auth/oauth2/token
- GET /api/auth/oauth2/userinfo
- GET /api/auth/jwks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-29 12:48:50 +01:00
parent c3dd7703b2
commit baea194677
2 changed files with 76 additions and 33 deletions

View file

@ -4,12 +4,17 @@
* Exposes Better Auth's OIDC Provider endpoints for external services
* like Matrix/Synapse to use SSO authentication.
*
* Better Auth exposes OIDC endpoints at /api/auth/oauth2/* paths.
* This controller provides routes at both:
* - /api/auth/oauth2/* (native Better Auth paths from discovery document)
* - /api/oidc/* (alternative paths for convenience)
*
* Endpoints:
* - GET /.well-known/openid-configuration - OIDC Discovery
* - GET /api/oidc/authorize - Authorization endpoint
* - POST /api/oidc/token - Token endpoint
* - GET /api/oidc/userinfo - UserInfo endpoint
* - GET /api/oidc/jwks - JWKS endpoint
* - GET /api/auth/oauth2/authorize - Authorization endpoint
* - POST /api/auth/oauth2/token - Token endpoint
* - GET /api/auth/oauth2/userinfo - UserInfo endpoint
* - GET /api/auth/jwks - JWKS endpoint
*/
import { Controller, Get, Post, All, Req, Res, HttpStatus } from '@nestjs/common';
@ -30,10 +35,58 @@ export class OidcController {
return this.handleOidcRequest(req, res);
}
// ============================================
// Better Auth Native OAuth2 Endpoints
// These match the paths in the discovery document
// ============================================
/**
* Authorization Endpoint
*
* Handles OAuth2 authorization requests.
* Authorization Endpoint (Better Auth native path)
*/
@Get('api/auth/oauth2/authorize')
async authorizeOauth2(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* Token Endpoint (Better Auth native path)
*/
@Post('api/auth/oauth2/token')
async tokenOauth2(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* UserInfo Endpoint (Better Auth native path)
*/
@Get('api/auth/oauth2/userinfo')
async userinfoOauth2(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* JWKS Endpoint (Better Auth native path)
*/
@Get('api/auth/jwks')
async jwksAuth(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* Catch-all for other Better Auth OAuth2 endpoints
*/
@All('api/auth/oauth2/*')
async catchAllOauth2(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
// ============================================
// Alternative /api/oidc/* paths
// For backwards compatibility and convenience
// ============================================
/**
* Authorization Endpoint (alternative path)
*/
@Get('api/oidc/authorize')
async authorize(@Req() req: Request, @Res() res: Response) {
@ -41,9 +94,7 @@ export class OidcController {
}
/**
* Token Endpoint
*
* Exchanges authorization codes for tokens.
* Token Endpoint (alternative path)
*/
@Post('api/oidc/token')
async token(@Req() req: Request, @Res() res: Response) {
@ -51,9 +102,7 @@ export class OidcController {
}
/**
* UserInfo Endpoint
*
* Returns user information for the authenticated user.
* UserInfo Endpoint (alternative path)
*/
@Get('api/oidc/userinfo')
async userinfo(@Req() req: Request, @Res() res: Response) {
@ -61,9 +110,7 @@ export class OidcController {
}
/**
* JWKS Endpoint (via /api/oidc/jwks)
*
* Returns JSON Web Key Set for token verification.
* JWKS Endpoint (alternative path)
*/
@Get('api/oidc/jwks')
async jwks(@Req() req: Request, @Res() res: Response) {
@ -71,18 +118,7 @@ export class OidcController {
}
/**
* JWKS Endpoint (via /api/auth/jwks)
*
* Better Auth's discovery document points to this path,
* so we need to expose it directly as well.
*/
@Get('api/auth/jwks')
async jwksAlt(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* Catch-all for other OIDC endpoints
* Catch-all for other OIDC endpoints (alternative path)
*/
@All('api/oidc/*')
async catchAll(@Req() req: Request, @Res() res: Response) {

View file

@ -81,18 +81,25 @@ async function bootstrap() {
// Global prefix (exclude metrics, health, Better Auth native routes, and OIDC routes)
// Better Auth generates verification URLs with /api/auth/* prefix
// OIDC Provider requires routes without prefix: /.well-known/*, /api/oidc/*
// OIDC Provider requires routes without prefix: /.well-known/*, /api/auth/oauth2/*, /api/oidc/*
app.setGlobalPrefix('api/v1', {
exclude: [
{ path: 'metrics', method: RequestMethod.ALL },
{ path: 'health', method: RequestMethod.ALL },
// Better Auth routes - use path-to-regexp wildcards
{ path: 'api/auth/(.*)', method: RequestMethod.ALL },
// Better Auth routes (verification emails, password reset)
{ path: 'api/auth/verify-email', method: RequestMethod.ALL },
{ path: 'api/auth/reset-password/(.*)', method: RequestMethod.ALL },
// Better Auth OIDC/OAuth2 routes (native paths from discovery document)
{ path: 'api/auth/jwks', method: RequestMethod.ALL },
{ path: 'api/auth/:path*', method: RequestMethod.ALL },
// OIDC routes
{ path: 'api/auth/oauth2/(.*)', method: RequestMethod.ALL },
{ path: 'api/auth/oauth2/authorize', method: RequestMethod.ALL },
{ path: 'api/auth/oauth2/token', method: RequestMethod.ALL },
{ path: 'api/auth/oauth2/userinfo', method: RequestMethod.ALL },
{ path: 'api/auth/oauth2/:path*', method: RequestMethod.ALL },
// OIDC discovery
{ path: '.well-known/(.*)', method: RequestMethod.ALL },
{ path: '.well-known/openid-configuration', method: RequestMethod.ALL },
// Alternative OIDC routes
{ path: 'api/oidc/(.*)', method: RequestMethod.ALL },
{ path: 'api/oidc/:path*', method: RequestMethod.ALL },
],