chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import type { Request } from 'express';
|
||||
import { PermissionResolverService } from '../../authorization/permission-resolver.service';
|
||||
import { AuthSessionsRepository } from '../../core-data/repositories/auth-sessions.repository';
|
||||
import { UsersRepository } from '../../core-data/repositories/users.repository';
|
||||
import { UserStatus } from '../../database/entities';
|
||||
import type { AuthTransport, RequestWithContext } from '../../common/http/request-context';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { TokenService } from '../services/token.service';
|
||||
import type { AccessTokenPayload } from '../interfaces/access-token-payload';
|
||||
|
||||
function unauthorized(): UnauthorizedException {
|
||||
return new UnauthorizedException({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Autenticación requerida',
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly tokens: TokenService,
|
||||
private readonly cookies: CookieService,
|
||||
private readonly users: UsersRepository,
|
||||
private readonly sessions: AuthSessionsRepository,
|
||||
private readonly permissions: PermissionResolverService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) return true;
|
||||
|
||||
const request = context.switchToHttp().getRequest<RequestWithContext>();
|
||||
const extracted = this.extractToken(request);
|
||||
if (!extracted) throw unauthorized();
|
||||
|
||||
let payload: AccessTokenPayload;
|
||||
try {
|
||||
payload = await this.tokens.verifyAccessToken(extracted.token);
|
||||
} catch {
|
||||
throw unauthorized();
|
||||
}
|
||||
|
||||
if (
|
||||
payload.typ !== 'access' ||
|
||||
typeof payload.sub !== 'string' ||
|
||||
typeof payload.sid !== 'string' ||
|
||||
typeof payload.username !== 'string'
|
||||
) {
|
||||
throw unauthorized();
|
||||
}
|
||||
|
||||
const [user, session, authorization] = await Promise.all([
|
||||
this.users.findById(payload.sub),
|
||||
this.sessions.findActiveById(payload.sid),
|
||||
this.permissions.resolveForUser(payload.sub),
|
||||
]);
|
||||
|
||||
if (
|
||||
!user ||
|
||||
user.status !== UserStatus.ACTIVE ||
|
||||
!session ||
|
||||
session.userId !== user.id
|
||||
) {
|
||||
throw unauthorized();
|
||||
}
|
||||
|
||||
request.auth = {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
sessionId: session.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
mustChangePassword: user.mustChangePassword,
|
||||
roles: authorization.roles,
|
||||
permissions: authorization.permissions,
|
||||
transport: extracted.transport,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractToken(
|
||||
request: Request,
|
||||
): { token: string; transport: AuthTransport } | null {
|
||||
const authorization = request.header('authorization');
|
||||
if (authorization) {
|
||||
const [scheme, token, extra] = authorization.split(' ');
|
||||
if (scheme?.toLowerCase() === 'bearer' && token && !extra) {
|
||||
return { token, transport: 'bearer' };
|
||||
}
|
||||
}
|
||||
|
||||
const cookieToken = this.cookies.getAccessToken(request);
|
||||
return cookieToken ? { token: cookieToken, transport: 'cookie' } : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import type { RequestWithContext } from '../../common/http/request-context';
|
||||
import { AuthConfigService } from '../../common/config/auth-config.service';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
import { SKIP_CSRF_KEY } from '../decorators/skip-csrf.decorator';
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
|
||||
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
||||
|
||||
@Injectable()
|
||||
export class CsrfGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly cookies: CookieService,
|
||||
private readonly config: AuthConfigService,
|
||||
private readonly reflector: Reflector,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<RequestWithContext>();
|
||||
if (SAFE_METHODS.has(request.method.toUpperCase())) return true;
|
||||
if (request.auth?.transport === 'bearer') return true;
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (
|
||||
!isPublic &&
|
||||
/^Bearer\s+\S+$/i.test(request.header('authorization') ?? '')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const origin = request.header('origin');
|
||||
if (origin && origin !== this.config.webOrigin) throw this.invalidCsrf();
|
||||
const skipToken = this.reflector.getAllAndOverride<boolean>(SKIP_CSRF_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (skipToken) return true;
|
||||
if (!this.cookies.csrfMatches(request)) throw this.invalidCsrf();
|
||||
return true;
|
||||
}
|
||||
|
||||
private invalidCsrf(): ForbiddenException {
|
||||
return new ForbiddenException({
|
||||
code: 'CSRF_INVALID',
|
||||
message: 'Verificación CSRF inválida',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user