mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 12:29:40 +02:00
256 lines
7.3 KiB
TypeScript
256 lines
7.3 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) {
|
|
console.log('[OIDC Authorize] URL:', req.originalUrl);
|
|
console.log('[OIDC Authorize] Query:', req.query);
|
|
console.log('[OIDC Authorize] redirect_uri:', req.query.redirect_uri);
|
|
return this.handleOidcRequest(req, res);
|
|
}
|
|
|
|
/**
|
|
* Token Endpoint (Better Auth native path)
|
|
*
|
|
* Accepts both JSON and form-urlencoded body (OAuth2 spec requires form-urlencoded).
|
|
* The body-parser middleware in main.ts parses form data into req.body object.
|
|
*/
|
|
@Post('api/auth/oauth2/token')
|
|
async tokenOauth2(@Req() req: Request, @Res() res: Response) {
|
|
console.log('[Token Endpoint] Content-Type:', req.headers['content-type']);
|
|
console.log('[Token Endpoint] Body:', req.body);
|
|
console.log('[Token Endpoint] Body keys:', Object.keys(req.body || {}));
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Better Auth Sign-in Endpoint
|
|
*
|
|
* This endpoint is needed for OIDC login flow.
|
|
* When users log in via the /login page, it posts to this endpoint
|
|
* which sets the session cookie needed for the OAuth2 flow.
|
|
*/
|
|
@Post('api/auth/sign-in/email')
|
|
async signInEmail(@Req() req: Request, @Res() res: Response) {
|
|
return this.handleBetterAuthRequest(req, res);
|
|
}
|
|
|
|
/**
|
|
* Handle Better Auth requests by forwarding to Better Auth's handler
|
|
* This is a simpler handler that just passes through to Better Auth
|
|
*/
|
|
private async handleBetterAuthRequest(req: Request, res: Response) {
|
|
try {
|
|
const baseUrl = process.env.BASE_URL || 'http://localhost:3001';
|
|
const url = new URL(req.originalUrl, baseUrl);
|
|
|
|
const headers = new Headers();
|
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
if (value && typeof value === 'string') {
|
|
headers.set(key, value);
|
|
} else if (Array.isArray(value)) {
|
|
headers.set(key, value[0]);
|
|
}
|
|
}
|
|
|
|
// Create Fetch Request
|
|
const fetchRequest = new Request(url.toString(), {
|
|
method: req.method,
|
|
headers,
|
|
body: req.method !== 'GET' && req.method !== 'HEAD' ? JSON.stringify(req.body) : undefined,
|
|
});
|
|
|
|
// Get Better Auth handler and call it directly
|
|
const handler = this.betterAuthService.getHandler();
|
|
const response = await handler(fetchRequest);
|
|
|
|
// Copy status
|
|
res.status(response.status);
|
|
|
|
// Copy headers including Set-Cookie for session
|
|
response.headers.forEach((value: string, key: string) => {
|
|
// Handle multiple Set-Cookie headers
|
|
if (key.toLowerCase() === 'set-cookie') {
|
|
res.append(key, value);
|
|
} else {
|
|
res.setHeader(key, value);
|
|
}
|
|
});
|
|
|
|
// Handle redirects
|
|
if (response.status === 302 || response.status === 301) {
|
|
const location = response.headers.get('location');
|
|
if (location) {
|
|
return res.redirect(response.status, location);
|
|
}
|
|
}
|
|
|
|
// Return body
|
|
const body = await response.text();
|
|
if (body) {
|
|
return res.send(body);
|
|
}
|
|
|
|
return res.end();
|
|
} catch (error) {
|
|
console.error('[BetterAuth] Error handling request:', error);
|
|
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
|
error: 'server_error',
|
|
error_description: 'Internal server error',
|
|
});
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// 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) {
|
|
try {
|
|
const response = await this.betterAuthService.handleOidcRequest(req);
|
|
|
|
// 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',
|
|
});
|
|
}
|
|
}
|
|
}
|