Refresh Token + Token Rotation
Muammodan boshlaylik
Oldingi darsda access token muddatini 15 daqiqa qildik. To'g'ri qaror — xavfsiz. Lekin muammo bor: 15 daqiqada token o'chadi, foydalanuvchi qayta login qilishi kerak. Har 15 daqiqada! Bu qabul qilib bo'lmaydi.
Muddatni uzaytirsa bo'ladi — 7 kun. Lekin token o'g'irlansa 7 kun davomida hamma narsaga kirish mumkin.
Refresh Token bu ikkilamchini hal qiladi: qisqa access token + uzun refresh token.
Refresh Token qanday ishlaydi?
Ikkita token:
Access Token — 15 daqiqa, har so'rovda ishlatiladi
Refresh Token — 7 kun, faqat yangi access token olish uchun
Hayotiy o'xshatma: kutubxona kartasi. Bitta kunlik o'tish taloni (access token) — har kuni yangilanadi. Bitta yillik a'zolik kartasi (refresh token) — faqat yangi o'tish taloni olish uchun ko'rsatiladi.
Oqim:
1. Login → access token (15 daq) + refresh token (7 kun)
2. So'rov → access token bilan
3. Access token o'chdi → refresh token bilan yangi access token oling
4. Agar refresh token ham o'chdi → qayta login
Token Rotation nima?
Har refresh token ishlatilganda — yangi refresh token ham beriladi, eskilik bekor qilinadi. Bu — Token Rotation.
Nima uchun kerak? Agar refresh token o'g'irlangan bo'lsa va xaker undan foydalansa — siz ham o'sha tokenni ishlatmoqchi bo'lasiz. Ikki so'rov keladi bir xil token bilan. Server: "Bu token allaqachon ishlatilgan — ikkala sessiya ham bekor qilinadi, ikkalasi ham qayta login qilsin."
Amalga oshirish
User sxemasiga refresh token qo'shish
// src/users/schemas/user.schema.ts
@Schema({ timestamps: true })
export class User {
@Prop({ required: true })
name: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true, select: false })
password: string;
// Refresh token hash saqlanadi (plain emas)
@Prop({ select: false, default: null })
refreshToken: string | null;
}
Nima uchun hash? Refresh token ham parol kabi maxfiy. Baza buzilsa plain tokenlar o'g'irlanadi. Hash qilinsa — yaroqsiz.
Auth Service — ikkala token
// src/auth/auth.service.ts
import { Injectable, ForbiddenException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private config: ConfigService,
) {}
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user) return null;
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return null;
const { password: _, ...result } = user.toObject();
return result;
}
// Login — ikkala token yaratish
async login(user: any) {
const tokens = await this.generateTokens(user._id, user.email, user.role);
await this.saveRefreshToken(user._id, tokens.refreshToken);
return { ...tokens, user: { id: user._id, name: user.name, email: user.email } };
}
// Refresh — yangi access token + yangi refresh token
async refresh(userId: string, refreshToken: string) {
const user = await this.usersService.findByIdWithToken(userId);
if (!user || !user.refreshToken) {
throw new ForbiddenException('Kirish taqiqlangan');
}
// Kelgan token bilan bazadagi hashni solishtiramiz
const tokenMatch = await bcrypt.compare(refreshToken, user.refreshToken);
if (!tokenMatch) {
// Token o'g'irlangan bo'lishi mumkin — sessiyani butunlay tozalaymiz
await this.usersService.clearRefreshToken(userId);
throw new ForbiddenException('Token yaroqsiz — qayta login qiling');
}
// Token Rotation — yangi refresh token beriladi, eski bekor
const tokens = await this.generateTokens(userId, user.email, user.role);
await this.saveRefreshToken(userId, tokens.refreshToken);
return tokens;
}
// Logout — refresh tokenni o'chirish
async logout(userId: string) {
await this.usersService.clearRefreshToken(userId);
return { message: 'Chiqildi' };
}
// Yordamchi: ikkala tokenni yaratish
private async generateTokens(userId: string, email: string, role: string) {
const payload = { sub: userId, email, role };
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.config.get('JWT_SECRET'),
expiresIn: this.config.get('JWT_EXPIRES_IN'), // '15m'
}),
this.jwtService.signAsync(payload, {
secret: this.config.get('JWT_REFRESH_SECRET'), // boshqa secret!
expiresIn: this.config.get('JWT_REFRESH_EXPIRES_IN'), // '7d'
}),
]);
return { accessToken, refreshToken };
}
// Yordamchi: refresh tokenni hash qilib saqlash
private async saveRefreshToken(userId: string, token: string) {
const hash = await bcrypt.hash(token, 10);
await this.usersService.updateRefreshToken(userId, hash);
}
}
Nima uchun alohida JWT_REFRESH_SECRET? Access va refresh token bir xil secret bilan imzolansa — biri o'g'irlansa ikkinchisi ham yaroqsiz bo'ladi, lekin bu aniqlanmaydi. Alohida secret — agar access secret ochilib qolsa, refresh tokenlar hali xavfsiz.
UsersService ga yangi metodlar
// src/users/users.service.ts
async findByIdWithToken(id: string) {
return this.userModel.findById(id).select('+refreshToken').exec();
}
async updateRefreshToken(id: string, hash: string | null) {
return this.userModel.findByIdAndUpdate(id, { refreshToken: hash }).exec();
}
async clearRefreshToken(id: string) {
return this.updateRefreshToken(id, null);
}
Refresh Token Guard va Strategy
// src/auth/strategies/jwt-refresh.strategy.ts
import { Injectable, ForbiddenException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(private config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('JWT_REFRESH_SECRET'), // ← refresh secret
passReqToCallback: true, // ← req ni validate ga uzatish
});
}
async validate(req: Request, payload: any) {
// Authorization: Bearer <token> — tokenni olamiz
const refreshToken = req.get('Authorization')?.replace('Bearer', '').trim();
if (!refreshToken) throw new ForbiddenException();
return { ...payload, refreshToken }; // ← req.user
}
}
// src/auth/guards/jwt-refresh.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {}
Controller — to'liq auth endpoints
// src/auth/auth.controller.ts
import { Controller, Post, Get, UseGuards, Req } from '@nestjs/common';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { JwtRefreshGuard } from './guards/jwt-refresh.guard';
import { Public } from './decorators/public.decorator';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
// POST /auth/login
@Post('login')
@Public()
@UseGuards(LocalAuthGuard)
login(@Req() req) {
return this.authService.login(req.user);
}
// POST /auth/refresh — yangi access token olish
@Post('refresh')
@Public()
@UseGuards(JwtRefreshGuard) // ← refresh token tekshiradi
refresh(@Req() req) {
const { sub, refreshToken } = req.user;
return this.authService.refresh(sub, refreshToken);
}
// POST /auth/logout
@Post('logout')
@UseGuards(JwtAuthGuard)
logout(@Req() req) {
return this.authService.logout(req.user.sub);
}
// GET /auth/me
@Get('me')
@UseGuards(JwtAuthGuard)
me(@Req() req) {
return req.user;
}
}
.env
JWT_SECRET=access-token-secret-kamida-32-belgi
JWT_EXPIRES_IN=15m
JWT_REFRESH_SECRET=refresh-token-secret-boshqa-kamida-32-belgi
JWT_REFRESH_EXPIRES_IN=7d
Frontend da token boshqarish
Login qilganda:
→ accessToken localStorage/memory da saqlang
→ refreshToken httpOnly cookie da saqlang (xavfsizroq)
Har so'rovda:
Authorization: Bearer <accessToken>
401 kelsa (token o'chdi):
→ POST /auth/refresh
→ Yangi accessToken oling
→ Asl so'rovni qayta yuboring
Refresh ham ishlamasa:
→ Login sahifasiga yo'naltiring
Tez-tez uchraydigan xatolar
Refresh tokenni localStorage da saqlash
❌ localStorage — XSS hujumida o'qiladi
✅ httpOnly cookie — JavaScript o'qiy olmaydi, xavfsizroq
Bitta secret ishlatish
# ❌ Bir xil secret
JWT_SECRET=mysecret
JWT_REFRESH_SECRET=mysecret
# ✅ Har biri alohida
JWT_SECRET=access_aK9mP2vL3qR8nT5wY1uA6bZ4
JWT_REFRESH_SECRET=refresh_cE7fH0iJ2kN5pQ8rS1tU3
Token rotation amalga oshirmaslik
// ❌ Har doim bir xil refresh token — o'g'irlansa bilinmaydi
async refresh(userId, refreshToken) {
// tekshirish ...
return { accessToken: this.jwtService.sign(...) };
// ← yangi refresh token berilmadi
}
// ✅ Har refresh da yangi refresh token
const tokens = await this.generateTokens(...);
await this.saveRefreshToken(userId, tokens.refreshToken); // ← yangilandi
return tokens;
Xulosa
Refresh Token — access token qisqa bo'lsa ham foydalanuvchi doimiy kirib turishi uchun mexanizm. Token Rotation — har ishlatganda yangi refresh token, eski bekor — xavfsizlik qatlami.
Eslab qoling:
- Access token →
15m, Refresh token →7d - Ikki alohida secret —
JWT_SECRETvaJWT_REFRESH_SECRET - Refresh token bazada hash sifatida saqlanadi
- Token Rotation — eski token ishlatilsa yangi beriladi, eski o'chadi
- Baza buzilsa →
clearRefreshTokenbilan barcha sessiyalarni bekor qiling
Keyingi dars
28-dars: Google OAuth / Social Login