import { Inject, Injectable } from '@nestjs/common' import { createHmac, randomUUID } from 'crypto' import { PASSPORT_OPTIONS, TOKEN_TYPES } from './constants' import { PassportOptions } from './interfaces' import { base64UrlDecode, base64UrlEncode, constantTimeEqual } from './utils' type TokenType = (typeof TOKEN_TYPES)[keyof typeof TOKEN_TYPES] @Injectable() export class PassportService { private readonly SECRET_KEY: string private static readonly INTERNAL_SEPARATOR = '|' public constructor( @Inject(PASSPORT_OPTIONS) private readonly options: PassportOptions ) { this.SECRET_KEY = options.secretKey } public generate>( payload: T, ttl: number, tokenType: TokenType = TOKEN_TYPES.AUTH ) { const issuedAt = this.now() const expiresAt = issuedAt + ttl const jti = randomUUID() const payloadPart = base64UrlEncode(JSON.stringify(payload)) const iatPart = base64UrlEncode(String(issuedAt)) const expPart = base64UrlEncode(String(expiresAt)) const jtiPart = base64UrlEncode(jti) const serialized = this.serialize( tokenType, payloadPart, iatPart, expPart, jtiPart ) const mac = this.computeHmac(this.SECRET_KEY, serialized) return `${payloadPart}.${iatPart}.${expPart}.${jtiPart}.${mac}` } public verify( token: string, expectedType: TokenType = TOKEN_TYPES.AUTH ) { try { const parts = token.split('.') if (parts.length !== 5) return { valid: false, reason: 'Не верный формат токена.' } const [payloadPart, iatPart, expPart, jtiPart, mac] = parts const serialized = this.serialize( expectedType, payloadPart, iatPart, expPart, jtiPart ) const expectedMac = this.computeHmac(this.SECRET_KEY, serialized) if (!constantTimeEqual(expectedMac, mac)) return { valid: false, reason: 'Невалидная подпись токена или неверный формат.' } const expNumber = Number(base64UrlDecode(expPart)) if (!Number.isFinite(expNumber)) return { valid: false, reason: 'Ошибка формата даты.' } if (this.now() > expNumber) return { valid: false, reason: 'Срок действия токена истёк.' } const decodedPayload = JSON.parse(base64UrlDecode(payloadPart)) as T return { valid: true, payload: decodedPayload, jti: base64UrlDecode(jtiPart) } } catch (error) { return { valid: false, reason: 'Поврежденный токен.' } } } private now() { return Math.floor(Date.now() / 1000) } private serialize( domain: string, payload: string, iat: string, exp: string, jti: string ) { return [domain, payload, iat, exp, jti].join( PassportService.INTERNAL_SEPARATOR ) } private computeHmac(secretKey: string, data: string) { return createHmac('sha256', secretKey).update(data).digest('hex') } }