feat(guild): validate bearer tokens via center introspection

This commit is contained in:
root
2026-05-13 07:59:57 +00:00
parent d9c5175233
commit b27cb0c2e1
6 changed files with 61 additions and 24 deletions

View File

@@ -2,13 +2,13 @@ import {
CanActivate,
ExecutionContext,
Injectable,
ServiceUnavailableException,
UnauthorizedException,
} from '@nestjs/common';
import { introspectGuildToken } from './center-auth';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest<{ path?: string; headers: Record<string, string | string[] | undefined> }>();
const path = req.path ?? '';
@@ -17,17 +17,15 @@ export class ApiKeyGuard implements CanActivate {
return true;
}
const expected = process.env.FABRIC_API_KEY;
if (!expected || expected.trim() === '') {
throw new ServiceUnavailableException('FABRIC_API_KEY is not configured');
}
const auth = req.headers['authorization'];
const authValue = Array.isArray(auth) ? auth[0] : auth;
const token = authValue?.startsWith('Bearer ') ? authValue.slice(7) : '';
if (!token) throw new UnauthorizedException('missing bearer token');
const received = req.headers['x-api-key'];
const receivedValue = Array.isArray(received) ? received[0] : received;
const result = await introspectGuildToken(token);
if (!result.active || !result.user?.id) throw new UnauthorizedException('invalid guild token');
if (!receivedValue || receivedValue !== expected) {
throw new UnauthorizedException('invalid api key');
}
(req as { userId?: string }).userId = result.user.id;
return true;
}

28
src/common/center-auth.ts Normal file
View File

@@ -0,0 +1,28 @@
export async function introspectGuildToken(token: string): Promise<{ active: boolean; user?: { id: string; email?: string } }> {
const centerBaseUrl = process.env.CENTER_BASE_URL;
const sharedSecret = process.env.CENTER_SHARED_SECRET;
const guildNodeId = process.env.GUILD_NODE_ID;
if (!centerBaseUrl || !sharedSecret || !guildNodeId) {
return { active: false };
}
const res = await fetch(`${centerBaseUrl}/api/auth/introspect`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-center-shared-secret': sharedSecret,
},
body: JSON.stringify({ token, guildNodeId }),
});
if (!res.ok) return { active: false };
const data = (await res.json()) as { active?: boolean; user?: { id: string; email?: string } };
if (!data.active) return { active: false };
return {
active: true,
user: data.user,
};
}