- Redesign the Identity module with a richer domain model. - Extend the User entity to support username, Authentik integration, activity tracking, and storage information. - Add Role and Permission domain models with many-to-many relationships. - Implement RBAC foundation using UserRole, RolePermission, and UserPermission mappings. - Add user storage quota and usage fields with default values. - Introduce Authentik identifiers and synchronization metadata. - Refactor user domain logic for role and permission management. - Update Prisma schema to support the new identity architecture. - Improve JWT authentication and permission guard integration. - Update repositories, handlers, controllers, mappers, DTOs, and Swagger configuration. - Refresh environment configuration and project dependencies.
85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
import {
|
|
Injectable,
|
|
CanActivate,
|
|
ExecutionContext,
|
|
UnauthorizedException,
|
|
} from '@nestjs/common';
|
|
import { Request } from 'express';
|
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
import { IdentityData } from '../interfaces/identity-data';
|
|
import * as jwt from 'jsonwebtoken';
|
|
|
|
/**
|
|
* JwtAuthGuard verifies JWTs issued by the external Identity Provider (Authentik)
|
|
* using JWKS (RS256). It also accepts internal RayLab JWTs signed with a local secret.
|
|
*/
|
|
@Injectable()
|
|
export class JwtAuthGuard implements CanActivate {
|
|
private jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
|
|
|
constructor() {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const request = context.switchToHttp().getRequest<Request & { identity?: IdentityData }>();
|
|
|
|
const authHeader = request.headers.authorization;
|
|
|
|
if (!authHeader) {
|
|
throw new UnauthorizedException('Authorization header is missing.');
|
|
}
|
|
|
|
const [type, token] = authHeader.split(' ');
|
|
|
|
if (type !== 'Bearer' || !token) {
|
|
throw new UnauthorizedException('Invalid authorization header.');
|
|
}
|
|
|
|
const jwksUri = process.env.AUTHENTIK_JWKS_URI;
|
|
|
|
// First try verifying with external JWKS (Authentik)
|
|
if (jwksUri) {
|
|
try {
|
|
if (!this.jwks) this.jwks = createRemoteJWKSet(new URL(jwksUri));
|
|
|
|
const { payload } = await jwtVerify(token, this.jwks, {
|
|
issuer: process.env.AUTHENTIK_ISSUER,
|
|
audience: process.env.AUTHENTIK_AUDIENCE,
|
|
});
|
|
|
|
const identity = new IdentityData(
|
|
payload.sub as string,
|
|
(payload as any).preferred_username as string | undefined,
|
|
(payload as any).email as string | undefined,
|
|
payload as Record<string, any>,
|
|
);
|
|
|
|
request.identity = identity;
|
|
return true;
|
|
} catch (err) {
|
|
// ignore and try internal verification
|
|
}
|
|
}
|
|
|
|
// Fallback: verify with internal symmetric secret
|
|
const secret = process.env.RAYLAB_JWT_SECRET;
|
|
if (!secret) {
|
|
throw new UnauthorizedException('Invalid or expired token.');
|
|
}
|
|
|
|
try {
|
|
const payload = jwt.verify(token, secret) as any;
|
|
|
|
const identity = new IdentityData(
|
|
payload.sub as string,
|
|
payload.preferred_username as string | undefined,
|
|
payload.email as string | undefined,
|
|
payload as Record<string, any>,
|
|
);
|
|
|
|
request.identity = identity;
|
|
return true;
|
|
} catch (err: any) {
|
|
throw new UnauthorizedException('Invalid or expired token.');
|
|
}
|
|
}
|
|
} |