first commit
This commit is contained in:
118
server/utils/refresh-token.ts
Normal file
118
server/utils/refresh-token.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { createHash, randomBytes } from 'node:crypto'
|
||||
import { createError, type H3Event } from 'h3'
|
||||
|
||||
import { getAuthRuntimeConfig } from './auth-config'
|
||||
import { prisma } from './prisma'
|
||||
|
||||
export interface IssuedRefreshToken {
|
||||
token: string
|
||||
expiresAt: Date
|
||||
expiresInSec: number
|
||||
recordId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera um hash seguro do refresh token usando pepper do ambiente.
|
||||
* O token bruto nunca é salvo no banco.
|
||||
*
|
||||
* @param rawToken Refresh token em texto puro.
|
||||
* @param pepper Segredo adicional do servidor.
|
||||
* @returns Hash SHA-256 do token.
|
||||
*/
|
||||
export function hashRefreshToken(rawToken: string, pepper: string): string {
|
||||
return createHash('sha256').update(`${rawToken}${pepper}`).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria um refresh token aleatório para entregar ao cliente.
|
||||
*
|
||||
* @returns Token aleatório em formato URL-safe.
|
||||
*/
|
||||
function generateRawRefreshToken(): string {
|
||||
return randomBytes(48).toString('base64url')
|
||||
}
|
||||
|
||||
/**
|
||||
* Emite e persiste um novo refresh token para um usuário.
|
||||
*
|
||||
* @param event Evento HTTP usado para ler config.
|
||||
* @param userId ID do usuário dono do token.
|
||||
* @returns Metadados do refresh token emitido.
|
||||
*/
|
||||
export async function issueRefreshToken(event: H3Event, userId: string): Promise<IssuedRefreshToken> {
|
||||
const config = getAuthRuntimeConfig(event)
|
||||
const token = generateRawRefreshToken()
|
||||
const tokenHash = hashRefreshToken(token, config.refreshTokenPepper)
|
||||
const expiresAt = new Date(Date.now() + config.refreshTtlSec * 1000)
|
||||
|
||||
const created = await prisma.refreshToken.create({
|
||||
data: {
|
||||
userId,
|
||||
tokenHash,
|
||||
expiresAt
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
token,
|
||||
expiresAt,
|
||||
expiresInSec: config.refreshTtlSec,
|
||||
recordId: created.id
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotaciona um refresh token válido:
|
||||
* cria um novo token e marca o antigo como revogado.
|
||||
*
|
||||
* @param event Evento HTTP usado para ler config.
|
||||
* @param rawRefreshToken Token atual enviado pelo cliente.
|
||||
* @returns Dados do novo refresh token e usuário autenticado.
|
||||
* @throws {H3Error} Quando o token for inválido, revogado ou expirado.
|
||||
*/
|
||||
export async function rotateRefreshToken(event: H3Event, rawRefreshToken: string) {
|
||||
const config = getAuthRuntimeConfig(event)
|
||||
const tokenHash = hashRefreshToken(rawRefreshToken, config.refreshTokenPepper)
|
||||
|
||||
const existing = await prisma.refreshToken.findUnique({
|
||||
where: { tokenHash },
|
||||
include: { user: true }
|
||||
})
|
||||
|
||||
if (!existing || existing.revokedAt || existing.expiresAt.getTime() <= Date.now()) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Invalid refresh token' })
|
||||
}
|
||||
|
||||
const newRawRefreshToken = generateRawRefreshToken()
|
||||
const newTokenHash = hashRefreshToken(newRawRefreshToken, config.refreshTokenPepper)
|
||||
const newExpiresAt = new Date(Date.now() + config.refreshTtlSec * 1000)
|
||||
|
||||
const rotated = await prisma.$transaction(async (tx) => {
|
||||
const created = await tx.refreshToken.create({
|
||||
data: {
|
||||
userId: existing.userId,
|
||||
tokenHash: newTokenHash,
|
||||
expiresAt: newExpiresAt
|
||||
}
|
||||
})
|
||||
|
||||
await tx.refreshToken.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
revokedAt: new Date(),
|
||||
replacedById: created.id
|
||||
}
|
||||
})
|
||||
|
||||
return created
|
||||
})
|
||||
|
||||
return {
|
||||
user: existing.user,
|
||||
token: newRawRefreshToken,
|
||||
expiresAt: rotated.expiresAt,
|
||||
expiresInSec: config.refreshTtlSec,
|
||||
recordId: rotated.id,
|
||||
replacedTokenId: existing.id
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user