feat(auth): шаг 0.6 — TTL access 15 минут + хэширование refresh/reset-токенов в БД
Some checks are pending
Branch Check / Type Check & Build (push) Waiting to run
Some checks are pending
Branch Check / Type Check & Build (push) Waiting to run
- server/utils/jwt.ts: дефолт JWT_ACCESS_EXPIRES 30d → 15m (env сохранено), hashToken() (sha256 hex) - migrations/0082_token_hashes.sql: *_hash колонки в user_sessions/users, DELETE FROM user_sessions (глобальный разлогин), legacy plain-колонки сохранены, но не пишутся - storage: lookup/отзыв сессий и reset-токенов по хэшу; markSessionReplaced по id сессии - auth.service: ротация в rotateFamilySession(), grace-period ротирует активную сессию вместо возврата plain-токена - auth.core.routes: forgot/reset-password пишут/ищут sha256-хэш, plain только в письме - tests/token-security.test.ts: 12 тестов (выпуск по хэшу, ротация, reuse detection, grace, reset) - .env.example, swagger, IMPLEMENTATION_LOG.md обновлены Проверки: npm run check чисто, vitest 118/118, lint 0 errors, build собирается
This commit is contained in:
@@ -2,6 +2,7 @@ import { users, organizations, userSessions, invitations, userCustomFields, user
|
||||
import { db } from "../db";
|
||||
import { eq, and, desc, asc, sql, ilike, or, gte, lte, inArray, isNull } from "drizzle-orm";
|
||||
import crypto from "crypto";
|
||||
import { hashToken } from "../utils/jwt";
|
||||
import { userStatusesStorage } from "./user-statuses.storage";
|
||||
import { invalidateAccessibleTasksForUser } from "../utils/cache";
|
||||
|
||||
@@ -72,7 +73,8 @@ export class UsersStorage {
|
||||
}
|
||||
|
||||
async getUserByResetPasswordToken(token: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.resetPasswordToken, token));
|
||||
// Шаг 0.6: поиск по sha256-хэшу, plain-токен в БД не хранится.
|
||||
const [user] = await db.select().from(users).where(eq(users.resetPasswordTokenHash, hashToken(token)));
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
@@ -283,10 +285,17 @@ export class UsersStorage {
|
||||
}
|
||||
|
||||
// User sessions
|
||||
// Шаг 0.6: в БД хранятся только sha256-хэши токенов (refresh_token_hash и т.д.);
|
||||
// plain-колонки (refresh_token, parent_refresh_token, replaced_by_token) не пишутся.
|
||||
async createUserSession(session: InsertUserSession): Promise<UserSession> {
|
||||
const [userSession] = await db
|
||||
.insert(userSessions)
|
||||
.values(session)
|
||||
.values({
|
||||
...session,
|
||||
refreshToken: null,
|
||||
parentRefreshToken: null,
|
||||
replacedByToken: null,
|
||||
})
|
||||
.returning();
|
||||
return userSession;
|
||||
}
|
||||
@@ -295,7 +304,7 @@ export class UsersStorage {
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(userSessions)
|
||||
.where(eq(userSessions.refreshToken, refreshToken));
|
||||
.where(eq(userSessions.refreshTokenHash, hashToken(refreshToken)));
|
||||
return session || undefined;
|
||||
}
|
||||
|
||||
@@ -303,7 +312,7 @@ export class UsersStorage {
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(userSessions)
|
||||
.where(eq(userSessions.parentRefreshToken, parentRefreshToken));
|
||||
.where(eq(userSessions.parentRefreshTokenHash, hashToken(parentRefreshToken)));
|
||||
return session || undefined;
|
||||
}
|
||||
|
||||
@@ -322,18 +331,18 @@ export class UsersStorage {
|
||||
return session || undefined;
|
||||
}
|
||||
|
||||
async markSessionReplaced(refreshToken: string, replacedByToken: string): Promise<void> {
|
||||
async markSessionReplaced(sessionId: number, replacedByToken: string): Promise<void> {
|
||||
await db
|
||||
.update(userSessions)
|
||||
.set({ isReplaced: true, replacedByToken })
|
||||
.where(eq(userSessions.refreshToken, refreshToken));
|
||||
.set({ isReplaced: true, replacedByTokenHash: hashToken(replacedByToken), replacedByToken: null })
|
||||
.where(eq(userSessions.id, sessionId));
|
||||
}
|
||||
|
||||
async revokeSession(refreshToken: string): Promise<void> {
|
||||
await db
|
||||
.update(userSessions)
|
||||
.set({ isRevoked: true })
|
||||
.where(eq(userSessions.refreshToken, refreshToken));
|
||||
.where(eq(userSessions.refreshTokenHash, hashToken(refreshToken)));
|
||||
}
|
||||
|
||||
async revokeSessionFamily(familyId: string): Promise<void> {
|
||||
@@ -352,7 +361,7 @@ export class UsersStorage {
|
||||
|
||||
/** Physical deletion kept for cleanup operations. */
|
||||
async deleteUserSession(refreshToken: string): Promise<void> {
|
||||
await db.delete(userSessions).where(eq(userSessions.refreshToken, refreshToken));
|
||||
await db.delete(userSessions).where(eq(userSessions.refreshTokenHash, hashToken(refreshToken)));
|
||||
}
|
||||
|
||||
async deleteUserSessions(userId: number): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user