mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
✨ feat(mana-core-auth): auto-link Matrix users on OIDC login
When users log into Matrix via OIDC (Sign in with Mana Core), their Matrix user ID is now automatically linked to their Mana account. This enables automatic bot authentication without requiring a separate !login command. - Add autoLinkOnOidcLogin() method to MatrixSessionService - Hook into OIDC userinfo endpoint to create links automatically - Calculate Matrix user ID from email using Synapse's template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
75937d6ce9
commit
85df234ff2
2 changed files with 143 additions and 2 deletions
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue