From 00d28bc522b3e7224582e05e4853bc371b7a95bf Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:49:26 +0100 Subject: [PATCH] feat(auth): add OIDC Controller for Matrix SSO endpoints - Add OidcController to expose Better Auth OIDC Provider endpoints - Add handleOidcRequest method to BetterAuthService - Exclude OIDC routes from global /api/v1 prefix - Register OidcController in AuthModule Endpoints: - GET /.well-known/openid-configuration - GET /api/oidc/authorize - POST /api/oidc/token - GET /api/oidc/userinfo - GET /api/oidc/jwks Co-Authored-By: Claude Opus 4.5 --- .../mana-core-auth/src/auth/auth.module.ts | 3 +- .../src/auth/oidc.controller.ts | 122 ++++++++++++++++++ .../src/auth/services/better-auth.service.ts | 69 ++++++++++ services/mana-core-auth/src/main.ts | 5 +- 4 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 services/mana-core-auth/src/auth/oidc.controller.ts diff --git a/services/mana-core-auth/src/auth/auth.module.ts b/services/mana-core-auth/src/auth/auth.module.ts index 7c59135f5..47298364a 100644 --- a/services/mana-core-auth/src/auth/auth.module.ts +++ b/services/mana-core-auth/src/auth/auth.module.ts @@ -1,12 +1,13 @@ import { Module, forwardRef } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { BetterAuthPassthroughController } from './better-auth-passthrough.controller'; +import { OidcController } from './oidc.controller'; import { BetterAuthService } from './services/better-auth.service'; import { ReferralsModule } from '../referrals/referrals.module'; @Module({ imports: [forwardRef(() => ReferralsModule)], - controllers: [AuthController, BetterAuthPassthroughController], + controllers: [AuthController, BetterAuthPassthroughController, OidcController], providers: [BetterAuthService], exports: [BetterAuthService], }) diff --git a/services/mana-core-auth/src/auth/oidc.controller.ts b/services/mana-core-auth/src/auth/oidc.controller.ts new file mode 100644 index 000000000..a6dd49703 --- /dev/null +++ b/services/mana-core-auth/src/auth/oidc.controller.ts @@ -0,0 +1,122 @@ +/** + * OIDC Provider Controller + * + * Exposes Better Auth's OIDC Provider endpoints for external services + * like Matrix/Synapse to use SSO authentication. + * + * 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 + */ + +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); + } + + /** + * Authorization Endpoint + * + * Handles OAuth2 authorization requests. + */ + @Get('api/oidc/authorize') + async authorize(@Req() req: Request, @Res() res: Response) { + return this.handleOidcRequest(req, res); + } + + /** + * Token Endpoint + * + * Exchanges authorization codes for tokens. + */ + @Post('api/oidc/token') + async token(@Req() req: Request, @Res() res: Response) { + return this.handleOidcRequest(req, res); + } + + /** + * UserInfo Endpoint + * + * Returns user information for the authenticated user. + */ + @Get('api/oidc/userinfo') + async userinfo(@Req() req: Request, @Res() res: Response) { + return this.handleOidcRequest(req, res); + } + + /** + * JWKS Endpoint + * + * Returns JSON Web Key Set for token verification. + */ + @Get('api/oidc/jwks') + async jwks(@Req() req: Request, @Res() res: Response) { + return this.handleOidcRequest(req, res); + } + + /** + * Catch-all for other OIDC endpoints + */ + @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', + }); + } + } +} diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index 646ae3b57..f8e6b84a7 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -1184,4 +1184,73 @@ export class BetterAuthService { console.error('[initializeUserReferrals] Error setting up referrals:', error); } } + + // ========================================================================= + // OIDC Provider Methods + // ========================================================================= + + /** + * Handle OIDC request by forwarding to Better Auth's handler + * + * This method converts an Express request to a Fetch Request, + * passes it to Better Auth's handler, and returns the response. + * + * @param req - Express request + * @returns Response data from Better Auth + */ + async handleOidcRequest(req: import('express').Request): Promise<{ + status: number; + headers: Record; + body: unknown; + }> { + try { + // Convert Express request to Fetch Request + const url = new URL( + req.originalUrl, + this.configService.get('BASE_URL') || + `http://localhost:${this.configService.get('PORT') || 3001}` + ); + + const headers = new Headers(); + for (const [key, value] of Object.entries(req.headers)) { + if (value) { + headers.set(key, Array.isArray(value) ? value[0] : value); + } + } + + // 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, + }); + + // Call Better Auth's handler + const response = await this.auth.handler(fetchRequest); + + // Convert Response to our format + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + // Get body + let body: unknown; + const contentType = response.headers.get('content-type'); + if (contentType?.includes('application/json')) { + body = await response.json(); + } else { + body = await response.text(); + } + + return { + status: response.status, + headers: responseHeaders, + body, + }; + } catch (error) { + console.error('[handleOidcRequest] Error:', error); + throw error; + } + } } diff --git a/services/mana-core-auth/src/main.ts b/services/mana-core-auth/src/main.ts index f0c5f2680..c58681f06 100644 --- a/services/mana-core-auth/src/main.ts +++ b/services/mana-core-auth/src/main.ts @@ -79,10 +79,11 @@ async function bootstrap() { }) ); - // Global prefix (exclude metrics, health, and Better Auth native routes) + // 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/* app.setGlobalPrefix('api/v1', { - exclude: ['metrics', 'health', 'api/auth/(.*)'], + exclude: ['metrics', 'health', 'api/auth/(.*)', '.well-known/(.*)', 'api/oidc/(.*)'], }); const port = configService.get('port') || 3001;