chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
@@ -0,0 +1,90 @@
import { randomBytes, timingSafeEqual } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import type { Request, Response } from 'express';
import { AuthConfigService } from '../../common/config/auth-config.service';
@Injectable()
export class CookieService {
constructor(private readonly config: AuthConfigService) {}
setAuthCookies(
response: Response,
accessToken: string,
refreshToken: string,
): string {
response.cookie(this.config.accessCookieName, accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
maxAge: this.config.accessTokenTtlSeconds * 1000,
});
response.cookie(this.config.refreshCookieName, refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/api/v3/auth',
maxAge: this.config.refreshTokenTtlSeconds * 1000,
});
return this.rotateCsrfCookie(response);
}
rotateCsrfCookie(response: Response): string {
const csrfToken = randomBytes(32).toString('base64url');
response.cookie(this.config.csrfCookieName, csrfToken, {
httpOnly: false,
secure: true,
sameSite: 'strict',
path: '/',
maxAge: this.config.refreshTokenTtlSeconds * 1000,
});
return csrfToken;
}
clearAuthCookies(response: Response): void {
response.clearCookie(this.config.accessCookieName, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
});
response.clearCookie(this.config.refreshCookieName, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/api/v3/auth',
});
response.clearCookie(this.config.csrfCookieName, {
httpOnly: false,
secure: true,
sameSite: 'strict',
path: '/',
});
}
getAccessToken(request: Request): string | undefined {
return this.readCookie(request, this.config.accessCookieName);
}
getRefreshToken(request: Request): string | undefined {
return this.readCookie(request, this.config.refreshCookieName);
}
csrfMatches(request: Request): boolean {
const cookieValue = this.readCookie(request, this.config.csrfCookieName);
const headerValue = request.header('x-csrf-token');
if (!cookieValue || !headerValue) return false;
const cookieBuffer = Buffer.from(cookieValue, 'utf8');
const headerBuffer = Buffer.from(headerValue, 'utf8');
return (
cookieBuffer.length === headerBuffer.length &&
timingSafeEqual(cookieBuffer, headerBuffer)
);
}
private readCookie(request: Request, name: string): string | undefined {
const cookies = request.cookies as Record<string, unknown> | undefined;
const value = cookies?.[name];
return typeof value === 'string' ? value : undefined;
}
}
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
const HASH_OPTIONS: argon2.HashOptions = {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
};
const DUMMY_PASSWORD_HASH =
'$argon2id$v=19$m=65536,p=1,t=3$aTSorDuRpmrZ/ILFbcoRYQ$sjvsmuiBlN3oTS1qeUiYxSShzsEVWiNnMY4Jo5MQw2c';
@Injectable()
export class PasswordService {
hash(password: string): Promise<string> {
return argon2.hash(password, HASH_OPTIONS);
}
async verify(hash: string, password: string): Promise<boolean> {
try {
return await argon2.verify(hash, password);
} catch {
return false;
}
}
async verifyUnknown(password: string): Promise<void> {
await this.verify(DUMMY_PASSWORD_HASH, password);
}
needsRehash(hash: string): boolean {
return argon2.needsRehash(hash, {
memoryCost: HASH_OPTIONS.memoryCost,
timeCost: HASH_OPTIONS.timeCost,
parallelism: HASH_OPTIONS.parallelism,
});
}
}
+98
View File
@@ -0,0 +1,98 @@
import {
createHmac,
randomBytes,
randomUUID,
timingSafeEqual,
} from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { AuthConfigService } from '../../common/config/auth-config.service';
import type { AccessTokenPayload } from '../interfaces/access-token-payload';
const REFRESH_TOKEN_PATTERN =
/^(?<sessionId>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.(?<secret>[A-Za-z0-9_-]{64})$/i;
export interface IssuedRefreshToken {
sessionId: string;
token: string;
tokenHash: string;
}
@Injectable()
export class TokenService {
constructor(
private readonly jwtService: JwtService,
private readonly config: AuthConfigService,
) {}
get accessTokenTtlSeconds(): number {
return this.config.accessTokenTtlSeconds;
}
get maxLoginAttempts(): number {
return this.config.maxLoginAttempts;
}
get lockoutSeconds(): number {
return this.config.lockoutSeconds;
}
refreshExpiresAt(from = new Date()): Date {
return new Date(from.getTime() + this.config.refreshTokenTtlSeconds * 1000);
}
issueAccessToken(input: {
userId: string;
sessionId: string;
username: string;
}): Promise<string> {
const payload: AccessTokenPayload = {
sub: input.userId,
sid: input.sessionId,
username: input.username,
typ: 'access',
};
return this.jwtService.signAsync(payload, {
secret: this.config.accessTokenSecret,
algorithm: 'HS256',
expiresIn: this.config.accessTokenTtlSeconds,
issuer: 'dhv2-api',
audience: 'dhv2',
});
}
verifyAccessToken(token: string): Promise<AccessTokenPayload> {
return this.jwtService.verifyAsync<AccessTokenPayload>(token, {
secret: this.config.accessTokenSecret,
algorithms: ['HS256'],
issuer: 'dhv2-api',
audience: 'dhv2',
});
}
issueRefreshToken(): IssuedRefreshToken {
const sessionId = randomUUID();
const secret = randomBytes(48).toString('base64url');
const token = `${sessionId}.${secret}`;
return { sessionId, token, tokenHash: this.hashRefreshToken(token) };
}
parseRefreshToken(token: string): { sessionId: string } | null {
const match = REFRESH_TOKEN_PATTERN.exec(token);
const sessionId = match?.groups?.sessionId;
return sessionId ? { sessionId: sessionId.toLowerCase() } : null;
}
verifyRefreshToken(token: string, expectedHash: string): boolean {
const actual = Buffer.from(this.hashRefreshToken(token), 'hex');
const expected = Buffer.from(expectedHash, 'hex');
return actual.length === expected.length && timingSafeEqual(actual, expected);
}
private hashRefreshToken(token: string): string {
return createHmac('sha256', this.config.refreshTokenPepper)
.update(token, 'utf8')
.digest('hex');
}
}