feat(auth): access 24ч, jti против гонок ротации, транзакционная ротация, отзыв сессий по sid
This commit is contained in:
@@ -31,8 +31,10 @@ const REFRESH_TOKEN_SECRET = requireSecret('JWT_REFRESH_SECRET');
|
||||
const SUPERADMIN_TOKEN_SECRET = requireSecret('JWT_SUPERADMIN_SECRET');
|
||||
const BOT_TOKEN_SECRET = process.env.JWT_BOT_SECRET || ACCESS_TOKEN_SECRET;
|
||||
|
||||
// Шаг 0.6: дефолт access-токена — 15 минут (было 30d). Env-переопределение сохранено.
|
||||
export const ACCESS_TOKEN_EXPIRY = process.env.JWT_ACCESS_EXPIRES || '15m';
|
||||
// Access-токен — 24 часа (рабочий день). Рефреш-ротация и отзыв сессий
|
||||
// покрывают безопасность; 15 минут давали 401-штормы и гонки ротации на
|
||||
// каждом переключении вкладок. Env-переопределение сохранено.
|
||||
export const ACCESS_TOKEN_EXPIRY = process.env.JWT_ACCESS_EXPIRES || '24h';
|
||||
const REFRESH_TOKEN_EXPIRY_REMEMBER = process.env.JWT_REFRESH_EXPIRES_REMEMBER || '90d';
|
||||
const REFRESH_TOKEN_EXPIRY_SESSION = process.env.JWT_REFRESH_EXPIRES_SESSION || '30d';
|
||||
const SUPERADMIN_TOKEN_EXPIRY = process.env.JWT_SUPERADMIN_EXPIRES || '1h';
|
||||
@@ -45,6 +47,8 @@ export interface TokenPayload {
|
||||
organizationId: number;
|
||||
appRole: string;
|
||||
role?: string; // legacy fallback for old tokens
|
||||
/** id сессии (user_sessions) — для отзыва устройства в моменте */
|
||||
sid?: number;
|
||||
}
|
||||
|
||||
export interface BotServiceTokenPayload {
|
||||
@@ -83,16 +87,14 @@ export interface TokenPair {
|
||||
familyId?: string;
|
||||
}
|
||||
|
||||
export function generateTokens(payload: TokenPayload, remember: boolean = false, familyId?: string): TokenPair {
|
||||
export function generateTokens(payload: TokenPayload, remember: boolean = false, familyId?: string, sid?: number): TokenPair {
|
||||
// Ensure we don't include legacy role in new tokens
|
||||
const { role, ...cleanPayload } = payload as any;
|
||||
const accessToken = jwt.sign(cleanPayload, ACCESS_TOKEN_SECRET, {
|
||||
expiresIn: ACCESS_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'],
|
||||
issuer: 'workflow-system',
|
||||
audience: 'workflow-users'
|
||||
});
|
||||
const accessToken = signAccessToken(cleanPayload, sid);
|
||||
|
||||
const refreshToken = jwt.sign(cleanPayload, REFRESH_TOKEN_SECRET, {
|
||||
// jti гарантирует уникальность токена даже при двух ротациях в одну секунду
|
||||
// (одинаковый payload+iat давал идентичные токены → конфликт unique по хэшу).
|
||||
const refreshToken = jwt.sign({ ...cleanPayload, jti: crypto.randomUUID() }, REFRESH_TOKEN_SECRET, {
|
||||
expiresIn: (remember ? REFRESH_TOKEN_EXPIRY_REMEMBER : REFRESH_TOKEN_EXPIRY_SESSION) as jwt.SignOptions['expiresIn'],
|
||||
issuer: 'workflow-system',
|
||||
audience: 'workflow-users'
|
||||
@@ -106,6 +108,16 @@ export function generateTokens(payload: TokenPayload, remember: boolean = false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Подпись access-токена; sid — id сессии для отзыва устройства. */
|
||||
export function signAccessToken(payload: Omit<TokenPayload, 'sid'>, sid?: number): string {
|
||||
const { role, ...cleanPayload } = payload as any;
|
||||
return jwt.sign(sid ? { ...cleanPayload, sid } : cleanPayload, ACCESS_TOKEN_SECRET, {
|
||||
expiresIn: ACCESS_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'],
|
||||
issuer: 'workflow-system',
|
||||
audience: 'workflow-users'
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyAccessToken(token: string): TokenPayload {
|
||||
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET, {
|
||||
algorithms: ['HS256'],
|
||||
|
||||
Reference in New Issue
Block a user