add refresh token
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import { Controller, Post, Body, ValidationPipe } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
ValidationPipe,
|
||||
UseGuards,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import AuthService from './auth.service';
|
||||
import { LoginRequestDto } from './dto/login-request.dto';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
import { JwtRefreshAuthGuard } from './jwt-refresh-auth.guard';
|
||||
import express from 'express';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@@ -10,4 +20,18 @@ export class AuthController {
|
||||
async login(@Body(new ValidationPipe()) body: LoginRequestDto) {
|
||||
return await this.authService.login(body);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('logout')
|
||||
async logout(@Req() req: express.Request) {
|
||||
const user = req.user as { sub: number };
|
||||
return await this.authService.logout(user.sub);
|
||||
}
|
||||
|
||||
@UseGuards(JwtRefreshAuthGuard)
|
||||
@Post('refresh')
|
||||
async refresh(@Req() req: express.Request) {
|
||||
const user = req.user as { sub: number; refreshToken: string };
|
||||
return await this.authService.refreshToken(user.sub, user.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { AuthService } from './auth.service';
|
||||
import AuthService from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { JwtRefreshTokenStrategy } from './jwt-refresh.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule, // <--- Import ConfigModule here
|
||||
ConfigModule,
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
@@ -17,11 +18,11 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '60m' },
|
||||
signOptions: { expiresIn: '2m' },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
providers: [AuthService, JwtStrategy, JwtRefreshTokenStrategy],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -4,12 +4,15 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { User } from '../entity/user';
|
||||
import { LoginRequest, LoginResponse } from '../types';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { StringValue } from 'ms';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
class AuthService {
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private jwtService: JwtService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async validateUser(username: string, pass: string): Promise<User | null> {
|
||||
@@ -34,19 +37,77 @@ export class AuthService {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const tokens = await this.getTokens(user);
|
||||
await this.userService.setRefreshToken(user.id, tokens.refreshToken);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
async logout(userId: number): Promise<void> {
|
||||
await this.userService.setRefreshToken(userId, null);
|
||||
}
|
||||
|
||||
async refreshToken(
|
||||
userId: number,
|
||||
refreshToken: string,
|
||||
): Promise<LoginResponse> {
|
||||
const user = await this.userService.findOne(userId);
|
||||
if (!user || !user.hashedRefreshToken) {
|
||||
throw new UnauthorizedException('Access Denied');
|
||||
}
|
||||
|
||||
const refreshTokenMatches = await bcrypt.compare(
|
||||
refreshToken,
|
||||
user.hashedRefreshToken,
|
||||
);
|
||||
|
||||
if (!refreshTokenMatches) {
|
||||
throw new UnauthorizedException('Access Denied');
|
||||
}
|
||||
|
||||
const tokens = await this.getTokens(user);
|
||||
await this.userService.setRefreshToken(user.id, tokens.refreshToken);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private async getTokens(user: User): Promise<LoginResponse> {
|
||||
const roles: Set<string> = new Set<string>();
|
||||
for (const group of user.groups ?? []) {
|
||||
for (const role of group.roles ?? []) {
|
||||
roles.add(role.name);
|
||||
}
|
||||
}
|
||||
const payload = {
|
||||
username: user.username,
|
||||
sub: user.id,
|
||||
roles: Array.from(roles),
|
||||
};
|
||||
|
||||
// let accessToken: string, refreshToken: string;
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.signAsync(
|
||||
{
|
||||
username: user.username,
|
||||
sub: user.id,
|
||||
roles: Array.from(roles),
|
||||
},
|
||||
{
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
expiresIn: this.configService.get<StringValue>('JWT_EXPIRATION_TIME'),
|
||||
},
|
||||
),
|
||||
this.jwtService.signAsync(
|
||||
{
|
||||
sub: user.id,
|
||||
},
|
||||
{
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
expiresIn: this.configService.get<StringValue>(
|
||||
'JWT_REFRESH_EXPIRATION_TIME',
|
||||
),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
access_token: this.jwtService.sign(payload),
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthService;
|
||||
|
||||
5
server/src/auth/jwt-refresh-auth.guard.ts
Normal file
5
server/src/auth/jwt-refresh-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtRefreshAuthGuard extends AuthGuard('jwt-refresh') {}
|
||||
47
server/src/auth/jwt-refresh.strategy.ts
Normal file
47
server/src/auth/jwt-refresh.strategy.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy, StrategyOptionsWithRequest } from 'passport-jwt';
|
||||
import { Request } from 'express';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
// Define the shape of the JWT payload
|
||||
interface JwtPayload {
|
||||
sub: number;
|
||||
// iat and exp are automatically added by passport-jwt
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
// Define the shape of the object returned by the validate function
|
||||
interface JwtPayloadWithRefreshToken extends JwtPayload {
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtRefreshTokenStrategy extends PassportStrategy(
|
||||
Strategy,
|
||||
'jwt-refresh',
|
||||
) {
|
||||
constructor(private configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
passReqToCallback: true,
|
||||
} as StrategyOptionsWithRequest);
|
||||
}
|
||||
|
||||
validate(req: Request, payload: JwtPayload): JwtPayloadWithRefreshToken {
|
||||
const refreshToken = req.get('Authorization')?.replace('Bearer', '').trim();
|
||||
|
||||
// Ensure refreshToken is not undefined before returning
|
||||
if (!refreshToken) {
|
||||
// This case should be rare given the guard is used, but it's good practice
|
||||
// to handle it. Depending on strictness, you might throw an error.
|
||||
// For now, we'll proceed, but in a real-world scenario, logging or an error
|
||||
// might be better.
|
||||
return { ...payload, refreshToken: '' };
|
||||
}
|
||||
|
||||
return { ...payload, refreshToken };
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,7 @@ export class User {
|
||||
@ManyToMany(() => UserGroup)
|
||||
@JoinTable()
|
||||
groups: UserGroup[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
hashedRefreshToken?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddRefreshTokenToUserObject1763106308120 implements MigrationInterface {
|
||||
name = 'AddRefreshTokenToUserObject1763106308120'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "hashedRefreshToken" character varying`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hashedRefreshToken"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,8 +4,7 @@ export interface LoginRequest{
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse{
|
||||
access_token: string;
|
||||
export interface LoginResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -54,4 +54,20 @@ export class UserService {
|
||||
this.logger.log(`Removing user with id: ${id}`, 'UserService');
|
||||
await this.usersRepository.delete(id);
|
||||
}
|
||||
|
||||
async setRefreshToken(
|
||||
id: number,
|
||||
refreshToken: string | null,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`Updating refresh token for user with id: ${id}`,
|
||||
'UserService',
|
||||
);
|
||||
if (refreshToken) {
|
||||
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
||||
await this.usersRepository.update(id, { hashedRefreshToken });
|
||||
} else {
|
||||
await this.usersRepository.update(id, { hashedRefreshToken: undefined });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user