diff --git a/services/mana-core-auth/src/auth/oidc.controller.ts b/services/mana-core-auth/src/auth/oidc.controller.ts index f1eb01881..a1d947cad 100644 --- a/services/mana-core-auth/src/auth/oidc.controller.ts +++ b/services/mana-core-auth/src/auth/oidc.controller.ts @@ -20,6 +20,7 @@ import { Controller, Get, Post, All, Req, Res, HttpStatus } from '@nestjs/common'; import { Request, Response } from 'express'; import { BetterAuthService } from './services/better-auth.service'; +import { MatrixSessionService } from './services/matrix-session.service'; import { LoggerService } from '../common/logger'; @Controller() @@ -28,6 +29,7 @@ export class OidcController { constructor( private readonly betterAuthService: BetterAuthService, + private readonly matrixSessionService: MatrixSessionService, loggerService: LoggerService ) { this.logger = loggerService.setContext('OidcController'); @@ -70,10 +72,14 @@ export class OidcController { /** * UserInfo Endpoint (Better Auth native path) + * + * When Matrix/Synapse calls this endpoint, we automatically create + * the Matrix user link so bots can recognize the user without + * requiring a separate !login command. */ @Get('api/auth/oauth2/userinfo') async userinfoOauth2(@Req() req: Request, @Res() res: Response) { - return this.handleOidcRequest(req, res); + return this.handleOidcRequestWithMatrixLink(req, res); } /** @@ -193,10 +199,13 @@ export class OidcController { /** * UserInfo Endpoint (alternative path) + * + * When Matrix/Synapse calls this endpoint, we automatically create + * the Matrix user link so bots can recognize the user. */ @Get('api/oidc/userinfo') async userinfo(@Req() req: Request, @Res() res: Response) { - return this.handleOidcRequest(req, res); + return this.handleOidcRequestWithMatrixLink(req, res); } /** @@ -259,4 +268,67 @@ export class OidcController { }); } } + + /** + * Handle OIDC userinfo request with automatic Matrix user linking + * + * This method forwards the request to Better Auth, and if successful, + * automatically creates a Matrix user link so bots can recognize + * the user without requiring a separate !login command. + */ + private async handleOidcRequestWithMatrixLink(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); + } + } + } + + // If userinfo was successful, create the Matrix user link + if (response.status === 200 && response.body) { + try { + const userInfo = response.body as { sub?: string; email?: string }; + if (userInfo.sub && userInfo.email) { + // Create Matrix user link asynchronously (don't block the response) + this.matrixSessionService + .autoLinkOnOidcLogin(userInfo.sub, userInfo.email) + .catch((err) => { + this.logger.warn('Failed to auto-link Matrix user on OIDC login', { + error: err instanceof Error ? err.message : 'Unknown error', + }); + }); + } + } catch (linkError) { + // Log but don't fail the request + this.logger.warn('Error parsing userinfo for Matrix link', { + error: linkError instanceof Error ? linkError.message : 'Unknown error', + }); + } + } + + // Return body + if (response.body) { + return res.send(response.body); + } + + return res.end(); + } catch (error) { + this.logger.error( + 'OIDC userinfo request failed', + error instanceof Error ? error.stack : undefined + ); + 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/matrix-session.service.ts b/services/mana-core-auth/src/auth/services/matrix-session.service.ts index 3cfcf4b0f..77cf25f27 100644 --- a/services/mana-core-auth/src/auth/services/matrix-session.service.ts +++ b/services/mana-core-auth/src/auth/services/matrix-session.service.ts @@ -188,4 +188,73 @@ export class MatrixSessionService { return links.length > 0; } + + /** + * Auto-link Matrix user during OIDC login + * + * Called when a user logs into Matrix via OIDC (Sign in with Mana Core). + * Creates the Matrix user link automatically so bots can recognize them. + * + * @param manaUserId - Mana Core Auth user ID + * @param email - User's email address + * @param matrixDomain - Matrix homeserver domain (default: matrix.mana.how) + */ + async autoLinkOnOidcLogin( + manaUserId: string, + email: string, + matrixDomain = 'matrix.mana.how' + ): Promise { + try { + // Calculate Matrix user ID from email using Synapse's template: + // localpart_template: "{{ user.email.split('@')[0] }}" + const localpart = email.split('@')[0].toLowerCase(); + const matrixUserId = `@${localpart}:${matrixDomain}`; + + // Check if link already exists + const existing = await this.db + .select() + .from(matrixUserLinks) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)) + .limit(1); + + if (existing.length > 0) { + // Update existing link (in case user ID changed) + if (existing[0].userId !== manaUserId) { + await this.db + .update(matrixUserLinks) + .set({ + userId: manaUserId, + email, + lastUsedAt: new Date(), + }) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)); + this.logger.log(`Updated Matrix auto-link: ${matrixUserId} -> ${manaUserId}`); + } else { + // Just update lastUsedAt + await this.db + .update(matrixUserLinks) + .set({ lastUsedAt: new Date() }) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)); + } + return; + } + + // Create new link + await this.db.insert(matrixUserLinks).values({ + id: nanoid(), + matrixUserId, + userId: manaUserId, + email, + linkedAt: new Date(), + }); + + this.logger.log(`Created Matrix auto-link on OIDC login: ${matrixUserId} -> ${manaUserId}`); + } catch (error) { + // Log but don't throw - this is a best-effort operation + this.logger.error( + 'Failed to auto-link Matrix user on OIDC login', + error instanceof Error ? error.stack : undefined + ); + } + } }