mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
577b96156c
commit
00d28bc522
4 changed files with 196 additions and 3 deletions
|
|
@ -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],
|
||||
})
|
||||
|
|
|
|||
122
services/mana-core-auth/src/auth/oidc.controller.ts
Normal file
122
services/mana-core-auth/src/auth/oidc.controller.ts
Normal file
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, string>;
|
||||
body: unknown;
|
||||
}> {
|
||||
try {
|
||||
// Convert Express request to Fetch Request
|
||||
const url = new URL(
|
||||
req.originalUrl,
|
||||
this.configService.get<string>('BASE_URL') ||
|
||||
`http://localhost:${this.configService.get<number>('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<string, string> = {};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<number>('port') || 3001;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue