KI-AGENT: Persistente App-Sessions mit Refresh-Tokens ergänzen
This commit is contained in:
93
backend/src/utils/authTokens.ts
Normal file
93
backend/src/utils/authTokens.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { createHash, randomBytes } from "node:crypto"
|
||||
import jwt from "jsonwebtoken"
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { and, eq, gt, isNull } from "drizzle-orm"
|
||||
|
||||
import { authRefreshTokens, authUsers } from "../../db/schema"
|
||||
import { secrets } from "./secrets"
|
||||
|
||||
const ACCESS_TOKEN_TTL_SECONDS = 60 * 60 * 6
|
||||
const REFRESH_TOKEN_TTL_DAYS = 90
|
||||
|
||||
export function createAccessToken(user: { id: string; email: string }, tenantId: number | null) {
|
||||
return jwt.sign(
|
||||
{
|
||||
user_id: user.id,
|
||||
email: user.email,
|
||||
tenant_id: tenantId,
|
||||
},
|
||||
secrets.JWT_SECRET!,
|
||||
{ expiresIn: ACCESS_TOKEN_TTL_SECONDS }
|
||||
)
|
||||
}
|
||||
|
||||
export function hashRefreshToken(token: string) {
|
||||
return createHash("sha256").update(token, "utf8").digest("hex")
|
||||
}
|
||||
|
||||
export async function issueRefreshToken(
|
||||
server: FastifyInstance,
|
||||
userId: string,
|
||||
tenantId: number | null
|
||||
) {
|
||||
const token = randomBytes(48).toString("base64url")
|
||||
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000)
|
||||
|
||||
await server.db.insert(authRefreshTokens).values({
|
||||
userId,
|
||||
tenantId,
|
||||
tokenHash: hashRefreshToken(token),
|
||||
expiresAt,
|
||||
})
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
export async function rotateRefreshToken(
|
||||
server: FastifyInstance,
|
||||
token: string,
|
||||
tenantOverride?: number | null,
|
||||
expectedUserId?: string
|
||||
) {
|
||||
const tokenHash = hashRefreshToken(token)
|
||||
const [session] = await server.db
|
||||
.update(authRefreshTokens)
|
||||
.set({ revokedAt: new Date() })
|
||||
.where(and(
|
||||
eq(authRefreshTokens.tokenHash, tokenHash),
|
||||
isNull(authRefreshTokens.revokedAt),
|
||||
gt(authRefreshTokens.expiresAt, new Date())
|
||||
))
|
||||
.returning({
|
||||
id: authRefreshTokens.id,
|
||||
userId: authRefreshTokens.userId,
|
||||
tenantId: authRefreshTokens.tenantId,
|
||||
})
|
||||
|
||||
if (!session) return null
|
||||
if (expectedUserId && session.userId !== expectedUserId) return null
|
||||
|
||||
const [user] = await server.db
|
||||
.select({ id: authUsers.id, email: authUsers.email })
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, session.userId))
|
||||
.limit(1)
|
||||
|
||||
if (!user) return null
|
||||
|
||||
const tenantId = tenantOverride === undefined ? session.tenantId : tenantOverride
|
||||
const refreshToken = await issueRefreshToken(server, session.userId, tenantId)
|
||||
const accessToken = createAccessToken(user, tenantId)
|
||||
|
||||
return { token: accessToken, refreshToken }
|
||||
}
|
||||
|
||||
export async function revokeRefreshToken(server: FastifyInstance, token: string) {
|
||||
await server.db
|
||||
.update(authRefreshTokens)
|
||||
.set({ revokedAt: new Date() })
|
||||
.where(and(
|
||||
eq(authRefreshTokens.tokenHash, hashRefreshToken(token)),
|
||||
isNull(authRefreshTokens.revokedAt)
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user