Files
passport/lib/passport.service.ts
Дмитрий be1e21e699
Some checks failed
Publish / Publish Job (push) Failing after 37s
first commit
2026-03-27 10:38:23 +03:00

125 lines
3.1 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, TokenRolePayload } 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(
userId: string,
role: TokenRolePayload,
ttl: number,
tokenType: TokenType = TOKEN_TYPES.AUTH
) {
const issuedAt = this.now()
const expiresAt = issuedAt + ttl
const jti = randomUUID()
const userPart = base64UrlEncode(userId)
const rolePart = base64UrlEncode(JSON.stringify(role))
const iatPart = base64UrlEncode(String(issuedAt))
const expPart = base64UrlEncode(String(expiresAt))
const jtiPart = base64UrlEncode(jti)
const serialized = this.serialize(
tokenType,
userPart,
rolePart,
iatPart,
expPart,
jtiPart
)
const mac = this.computeHmac(this.SECRET_KEY, serialized)
return `${userPart}.${rolePart}.${iatPart}.${expPart}.${jtiPart}.${mac}`
}
public verify(token: string, expectedType: TokenType = TOKEN_TYPES.AUTH) {
try {
const parts = token.split('.')
if (parts.length !== 6)
return { valid: false, reason: 'Не верный формат токена.' }
const [userPart, rolePart, iatPart, expPart, jtiPart, mac] = parts
const serialized = this.serialize(
expectedType,
userPart,
rolePart,
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 decodedRole = JSON.parse(
base64UrlDecode(rolePart)
) as TokenRolePayload
return {
valid: true,
userId: base64UrlDecode(userPart),
role: decodedRole,
jti: base64UrlDecode(jtiPart)
}
} catch (error) {
return { valid: false, reason: 'Поврежденный токен.' }
}
}
private now() {
return Math.floor(Date.now() / 1000)
}
private serialize(
domain: string,
user: string,
role: string,
iat: string,
exp: string,
jti: string
) {
return [domain, user, role, iat, exp, jti].join(
PassportService.INTERNAL_SEPARATOR
)
}
private computeHmac(secretKey: string, data: string) {
return createHmac('sha256', secretKey).update(data).digest('hex')
}
}