feat: add refresh token and global exception filter
This commit is contained in:
@@ -3,8 +3,9 @@ import { LoginDTO } from './dtos/login.dto';
|
||||
import { AuthService } from './providers/auth.service';
|
||||
import { VerifyOtpDTO } from './dtos/verify-otp.dto';
|
||||
import { Public } from './decorators/public.decorator';
|
||||
import { RefreshTokenDTO } from './dtos/refresh-token.dto';
|
||||
|
||||
@Controller('users')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
/**
|
||||
@@ -26,4 +27,11 @@ export class AuthController {
|
||||
public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) {
|
||||
return await this.authService.verifyOTP(verifyOtpDto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('refresh')
|
||||
@HttpCode(200)
|
||||
public async refreshToken(@Body() refreshTokenDto: RefreshTokenDTO) {
|
||||
return await this.authService.refreshToken(refreshTokenDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,16 @@ import { VerifyOTPProvider } from './providers/verify-otp.provider';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { GlobalAuthGuard } from './guards/global-auth.guard';
|
||||
import { RefreshTokensProvider } from './providers/refresh-tokens.provider';
|
||||
import { RedisModule } from '@/common/modules/redis/redis.module';
|
||||
import { AuthRedisProvider } from './providers/auth-redis.provider';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
OtpModule,
|
||||
HashingModule,
|
||||
RedisModule,
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
@@ -37,6 +41,8 @@ import { GlobalAuthGuard } from './guards/global-auth.guard';
|
||||
LoginProvider,
|
||||
VerifyOTPProvider,
|
||||
JwtStrategy,
|
||||
RefreshTokensProvider,
|
||||
AuthRedisProvider,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: GlobalAuthGuard,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class RefreshTokenDTO {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
POST http://localhost:3000/users/login
|
||||
POST http://localhost:3000/auth/login
|
||||
Content-Type: application/json
|
||||
lang: fa
|
||||
|
||||
{
|
||||
"phone": "09121111111"
|
||||
"phone": "09121111114"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
POST http://localhost:3000/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjksImp0aSI6IjljOTY5OWRhLWI0NTgtNDc0Ny04YTRkLTQyODQxMDllOTVkZCIsImlhdCI6MTc3ODMwNDMyNSwiZXhwIjoxNzc5MTY4MzI1LCJhdWQiOiJsb2NhbGhvc3QiLCJpc3MiOiJsb2NhbGhvc3QifQ.oiHuj3vJRQl7rG8H0fFEfU9cG91KrLGdXqNAYFGTXVA"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
POST http://localhost:3000/users/verify-otp
|
||||
POST http://localhost:3000/auth/verify-otp
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"phone": "09121111111",
|
||||
"otp": "83793"
|
||||
"phone": "09121111114",
|
||||
"otp": "99696"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { RedisService } from '@/common/modules/redis/providers/redis.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AuthRedisProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Injecting Redis Service
|
||||
*/
|
||||
private readonly redisService: RedisService,
|
||||
) {}
|
||||
|
||||
async storeRefreshToken(jti: string, userId: number, ttl: number) {
|
||||
await this.redisService.set(`rt:${jti}`, userId.toString(), ttl);
|
||||
}
|
||||
|
||||
async hasRefreshToken(jti: string) {
|
||||
return !!(await this.redisService.get(`rt:${jti}`));
|
||||
}
|
||||
|
||||
async revokeRefreshToken(jti: string) {
|
||||
await this.redisService.del(`rt:${jti}`);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { LoginDTO } from '../dtos/login.dto';
|
||||
import { LoginProvider } from './login.provider';
|
||||
import { VerifyOtpDTO } from '../dtos/verify-otp.dto';
|
||||
import { VerifyOTPProvider } from './verify-otp.provider';
|
||||
import { RefreshTokenDTO } from '../dtos/refresh-token.dto';
|
||||
import { RefreshTokensProvider } from './refresh-tokens.provider';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -16,6 +18,11 @@ export class AuthService {
|
||||
* Inject Verify OTP Provider
|
||||
*/
|
||||
private readonly verifyOTPProvider: VerifyOTPProvider,
|
||||
|
||||
/**
|
||||
* Inject RefreshToken Provider
|
||||
*/
|
||||
private readonly refreshTokensProvider: RefreshTokensProvider,
|
||||
) {}
|
||||
|
||||
public async logIn(loginDto: LoginDTO) {
|
||||
@@ -25,4 +32,8 @@ export class AuthService {
|
||||
public async verifyOTP(verifyOtpDto: VerifyOtpDTO) {
|
||||
return await this.verifyOTPProvider.verifyOTP(verifyOtpDto);
|
||||
}
|
||||
|
||||
public async refreshToken(refreshTokenDto: RefreshTokenDTO) {
|
||||
return await this.refreshTokensProvider.refreshTokens(refreshTokenDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AccessTokenPayload,
|
||||
RefreshTokenPayload,
|
||||
} from '../interfaces/jwt.interface';
|
||||
import { AuthRedisProvider } from './auth-redis.provider';
|
||||
|
||||
@Injectable()
|
||||
export class GenerateTokenProvider {
|
||||
@@ -21,6 +22,11 @@ export class GenerateTokenProvider {
|
||||
*/
|
||||
@Inject(jwtConfig.KEY)
|
||||
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
|
||||
|
||||
/**
|
||||
* Inject AuthRedis Provider
|
||||
*/
|
||||
private readonly authRedisProvider: AuthRedisProvider,
|
||||
) {}
|
||||
|
||||
public async signToken<T>(
|
||||
@@ -60,6 +66,13 @@ export class GenerateTokenProvider {
|
||||
this.jwtConfiguration.refresh.secret,
|
||||
{ jti },
|
||||
),
|
||||
|
||||
// Store Refresh JTI in redis
|
||||
this.authRedisProvider.storeRefreshToken(
|
||||
jti,
|
||||
user.id,
|
||||
this.jwtConfiguration.refresh.expiresIn,
|
||||
),
|
||||
]);
|
||||
|
||||
return { access, refresh };
|
||||
|
||||
@@ -5,6 +5,7 @@ import { OtpService } from '@/modules/otp/providers/otp.service';
|
||||
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
|
||||
import { GenerateTokenProvider } from './generate-token.provider';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
|
||||
@Injectable()
|
||||
export class LoginProvider {
|
||||
@@ -28,6 +29,11 @@ export class LoginProvider {
|
||||
* Inject GenerateToken Provider
|
||||
*/
|
||||
private readonly generateTokenProvider: GenerateTokenProvider,
|
||||
|
||||
/**
|
||||
* Inject I18n Service
|
||||
*/
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
public async logIn(loginDto: LoginDTO) {
|
||||
@@ -38,9 +44,13 @@ export class LoginProvider {
|
||||
// TODO: Send OTP
|
||||
const otpCode = await this.otpService.createOTP(phone);
|
||||
|
||||
const message = this.i18nService.translate('auth.messages.otpSent', {
|
||||
args: { phone },
|
||||
});
|
||||
|
||||
return {
|
||||
newUser: true,
|
||||
message: `Otp sent to ${phone}`,
|
||||
message,
|
||||
// Just for development
|
||||
code: otpCode,
|
||||
};
|
||||
@@ -49,7 +59,7 @@ export class LoginProvider {
|
||||
if (!password) {
|
||||
return {
|
||||
newUser: false,
|
||||
message: 'Please send password',
|
||||
message: this.i18nService.translate('auth.messages.sendPass'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,8 +73,6 @@ export class LoginProvider {
|
||||
return await this.generateTokenProvider.generateTokens(user, jti);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(
|
||||
'Entered phone number or password is wrong.',
|
||||
);
|
||||
throw new UnauthorizedException('auth.errors.wrongPhoneOrPass');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthRedisProvider } from './auth-redis.provider';
|
||||
import { RefreshTokenDTO } from '../dtos/refresh-token.dto';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import type { ConfigType } from '@nestjs/config';
|
||||
import jwtConfig from '@/config/jwt.config';
|
||||
import { RefreshTokenPayload } from '../interfaces/jwt.interface';
|
||||
import { UsersService } from '@/modules/users/providers/users.service';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { GenerateTokenProvider } from './generate-token.provider';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshTokensProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject JwtService
|
||||
*/
|
||||
private readonly jwtService: JwtService,
|
||||
|
||||
/**
|
||||
* Inject jwtConfig
|
||||
*/
|
||||
@Inject(jwtConfig.KEY)
|
||||
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
|
||||
|
||||
/**
|
||||
* Inject Users Service
|
||||
*/
|
||||
private readonly usersService: UsersService,
|
||||
|
||||
/**
|
||||
* Inject AuthRedis Provider
|
||||
*/
|
||||
private readonly authRedisProvider: AuthRedisProvider,
|
||||
|
||||
/**
|
||||
* Inject GenerateToken Provider
|
||||
*/
|
||||
private readonly generateTokenProvider: GenerateTokenProvider,
|
||||
) {}
|
||||
|
||||
public async refreshTokens(refreshTokenDto: RefreshTokenDTO) {
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync<RefreshTokenPayload>(
|
||||
refreshTokenDto.refreshToken,
|
||||
{
|
||||
secret: this.jwtConfiguration.refresh.secret,
|
||||
issuer: this.jwtConfiguration.issuer,
|
||||
audience: this.jwtConfiguration.audience,
|
||||
},
|
||||
);
|
||||
|
||||
const user = await this.usersService.findOneById(payload.sub);
|
||||
|
||||
if (!payload.jti) {
|
||||
throw new UnauthorizedException('auth.errors.unauthorized');
|
||||
}
|
||||
|
||||
const existsInRedis = await this.authRedisProvider.hasRefreshToken(
|
||||
payload.jti,
|
||||
);
|
||||
|
||||
if (!existsInRedis) {
|
||||
throw new UnauthorizedException('auth.errors.tokenReuse');
|
||||
}
|
||||
|
||||
await this.authRedisProvider.revokeRefreshToken(payload.jti);
|
||||
|
||||
const newJti = randomUUID();
|
||||
return await this.generateTokenProvider.generateTokens(user, newJti);
|
||||
} catch (err) {
|
||||
throw new UnauthorizedException('auth.errors.unauthorized', {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
const user = await this.usersService.findOneByPhone(payload.phone);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
throw new UnauthorizedException('auth.errors.unauthorized');
|
||||
}
|
||||
|
||||
return user;
|
||||
|
||||
Reference in New Issue
Block a user