managarten/services/mana-core-auth/src/auth/oidc.controller.ts
Till-JS baea194677 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>
2026-01-29 12:48:50 +01:00

171 lines
4.7 KiB
TypeScript

/**
* OIDC Provider Controller
*
* 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/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';
import { Request, Response } from 'express';
import { BetterAuthService } from './services/better-auth.service';
@Controller()
export class OidcController {
constructor(private readonly betterAuthService: BetterAuthService) {}
/**
* OIDC Discovery Document
*
* Returns the OpenID Connect discovery document with all endpoints.
*/
@Get('.well-known/openid-configuration')
async getOpenIdConfiguration(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
// ============================================
// Better Auth Native OAuth2 Endpoints
// These match the paths in the discovery document
// ============================================
/**
* 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) {
return this.handleOidcRequest(req, res);
}
/**
* Token Endpoint (alternative path)
*/
@Post('api/oidc/token')
async token(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* UserInfo Endpoint (alternative path)
*/
@Get('api/oidc/userinfo')
async userinfo(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* JWKS Endpoint (alternative path)
*/
@Get('api/oidc/jwks')
async jwks(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* Catch-all for other OIDC endpoints (alternative path)
*/
@All('api/oidc/*')
async catchAll(@Req() req: Request, @Res() res: Response) {
return this.handleOidcRequest(req, res);
}
/**
* Handle OIDC request by forwarding to Better Auth
*/
private async handleOidcRequest(req: Request, res: Response) {
console.log('[OIDC Controller] Handling request:', req.method, req.originalUrl);
try {
const response = await this.betterAuthService.handleOidcRequest(req);
console.log('[OIDC Controller] Better Auth response status:', response.status);
// Set status code
res.status(response.status || HttpStatus.OK);
// Copy headers from Better Auth response
if (response.headers) {
for (const [key, value] of Object.entries(response.headers)) {
if (value) {
res.setHeader(key, value as string);
}
}
}
// Handle redirects
if (response.status === 302 || response.status === 301) {
const location = response.headers?.location || response.headers?.Location;
if (location) {
return res.redirect(response.status, location as string);
}
}
// Return body
if (response.body) {
return res.send(response.body);
}
return res.end();
} catch (error) {
console.error('[OIDC] Error handling request:', error);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
error: 'server_error',
error_description: 'Internal server error',
});
}
}
}