Files
passport/lib/passport.service.ts
2026-03-27 12:37:44 +03:00

115 lines
2.9 KiB
TypeScript
Raw Permalink 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, 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<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')
}
}