Files
passport/lib/passport.service.ts
Дмитрий d49b2cf7e5
Some checks failed
Publish / Publish Job (push) Has been cancelled
feat: add generic from payload data
2026-03-27 11:36:20 +03:00

120 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Inject, Injectable } from '@nestjs/common'
import { createHmac, randomUUID } from 'crypto'
import { PASSPORT_OPTIONS } from './constants'
import { PassportOptions } from './interfaces'
import { base64UrlDecode, base64UrlEncode, constantTimeEqual } from './utils'
const TOKEN_TYPES = {
AUTH: 'PassportToken/v1',
TEMP_2FA: 'PassportToken/v1/2fa'
} as const
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<T extends Record<string, any>>(
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<T = any>(
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')
}
}