Skip to content

Commit 3db566b

Browse files
committed
add refresh token
1 parent 863d609 commit 3db566b

File tree

9 files changed

+120
-15
lines changed

9 files changed

+120
-15
lines changed

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ POSTGRES_PASSWORD=templateUserPass
77
POSTGRES_USER=templateUser
88
POSTGRES_HOST=localhost
99

10-
JWT_SECRET=secret
10+
JWT_SECRET=secret
11+
JWT_REFRESH_SECRET=secret
12+
13+
ACCESS_TOKEN_EXPIRATION=1d
14+
REFRESH_TOKEN_EXPIRATION=1y

src/app.module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { Module } from '@nestjs/common';
22
import { ConfigModule, ConfigType } from '@nestjs/config';
33
import { TypeOrmModule } from '@nestjs/typeorm';
44
import * as Joi from 'joi';
5+
import { AuthModule } from './auth/auth.module';
56
import config from './config';
67
import { enviroments } from './environments';
78
import { UsersModule } from './users/users.module';
8-
import { AuthModule } from './auth/auth.module';
99

1010
@Module({
1111
imports: [
@@ -15,6 +15,9 @@ import { AuthModule } from './auth/auth.module';
1515
isGlobal: true,
1616
validationSchema: Joi.object({
1717
JWT_SECRET: Joi.string().required(),
18+
JWT_REFRESH_SECRET: Joi.string().required(),
19+
ACCESS_TOKEN_EXPIRATION: Joi.string().required(),
20+
REFRESH_TOKEN_EXPIRATION: Joi.string().required(),
1821
}),
1922
validationOptions: {
2023
abortEarly: true, //when true, stops validation on the first error, otherwise returns all the errors found. Defaults to true.
@@ -23,7 +26,6 @@ import { AuthModule } from './auth/auth.module';
2326
TypeOrmModule.forRootAsync({
2427
inject: [config.KEY],
2528
useFactory: (configService: ConfigType<typeof config>) => {
26-
console.log(configService);
2729
return {
2830
type: 'postgres',
2931
host: configService.postgres.host,

src/auth/auth.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import { LocalStrategy } from './strategies/local.strategy';
1616
inject: [config.KEY],
1717
useFactory: (configService: ConfigType<typeof config>) => {
1818
return {
19-
secret: configService.jwtSecret,
19+
secret: configService.jwt.jwtSecret,
2020
signOptions: {
21-
expiresIn: '1d',
21+
expiresIn: configService.jwt.accessTokenExpiration,
2222
},
2323
};
2424
},

src/auth/controllers/auth.controller.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
1+
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
22
import { ApiTags } from '@nestjs/swagger';
33
import { User } from '../../users/entities/user.entity';
4+
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
45
import { LocalAuthGuard } from '../guards/local-auth.guard';
56
import { AuthService } from '../services/auth.service';
67

@@ -13,6 +14,14 @@ export class AuthController {
1314
@Post('login')
1415
login(@Request() req: { user: User }) {
1516
const user = req.user;
16-
return this.authService.generateJWT(user);
17+
return this.authService.login(user);
18+
}
19+
20+
@Get('logout')
21+
@ApiTags('Authentication')
22+
@UseGuards(JwtAuthGuard)
23+
async logOut(@Request() req: any, user) {
24+
await this.authService.logout(user.email);
25+
req.res.setHeader('Authorization', null);
1726
}
1827
}

src/auth/models/token.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export interface PayloadToken {
22
role: string;
3-
id: number;
3+
email: string;
44
}

src/auth/services/auth.service.ts

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { Injectable } from '@nestjs/common';
2-
import { JwtService } from '@nestjs/jwt';
1+
import {
2+
HttpException,
3+
HttpStatus,
4+
Injectable,
5+
UnauthorizedException,
6+
} from '@nestjs/common';
7+
import { ConfigService } from '@nestjs/config';
8+
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
39
import * as bcrypt from 'bcrypt';
410
import { User } from '../../users/entities/user.entity';
511
import { UsersService } from '../../users/services/users.service';
@@ -10,6 +16,7 @@ export class AuthService {
1016
constructor(
1117
private usersService: UsersService,
1218
private jwtService: JwtService,
19+
private configService: ConfigService,
1320
) {}
1421

1522
async validateUser(email: string, password: string) {
@@ -28,11 +35,64 @@ export class AuthService {
2835
return null;
2936
}
3037

31-
generateJWT(user: User) {
32-
const payload: PayloadToken = { role: user.role, id: user.id };
38+
login(user: User) {
39+
const payload: PayloadToken = { role: user.role, email: user.email };
3340
return {
3441
access_token: this.jwtService.sign(payload),
3542
user,
3643
};
3744
}
45+
46+
async logout(user: User) {
47+
return await this.usersService.removeRefreshToken(user.email);
48+
}
49+
50+
async createAccessTokenFromRefreshToken(refreshToken: string) {
51+
try {
52+
const decoded = this.jwtService.decode(refreshToken) as PayloadToken;
53+
54+
if (!decoded) {
55+
throw new Error();
56+
}
57+
58+
const user = await this.usersService.findByEmailAndGetPassword(
59+
decoded.email,
60+
);
61+
62+
if (!user) {
63+
throw new HttpException(
64+
'User with this id does not exist',
65+
HttpStatus.NOT_FOUND,
66+
);
67+
}
68+
69+
const isRefreshTokenMatching = await bcrypt.compare(
70+
refreshToken,
71+
user.refreshToken,
72+
);
73+
74+
if (!isRefreshTokenMatching) {
75+
throw new UnauthorizedException('Invalid token');
76+
}
77+
78+
await this.jwtService.verifyAsync(refreshToken, this.getTokenOptions());
79+
return this.login(user);
80+
} catch {
81+
throw new UnauthorizedException('Invalid token');
82+
}
83+
}
84+
85+
private getTokenOptions() {
86+
const options: JwtSignOptions = {
87+
secret: this.configService.get('JWT_REFRESH_SECRET'),
88+
};
89+
90+
const expiration: string = this.configService.get(
91+
'REFRESH_TOKEN_EXPIRATION',
92+
);
93+
if (expiration) {
94+
options.expiresIn = expiration;
95+
}
96+
return options;
97+
}
3898
}

src/auth/strategies/jwt.strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
1111
super({
1212
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
1313
ignoreExpiration: false,
14-
secretOrKey: configService.jwtSecret,
14+
secretOrKey: configService.jwt.jwtSecret,
1515
});
1616
}
1717

src/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export default registerAs('config', () => {
1212
password: process.env.POSTGRES_PASSWORD,
1313
user: process.env.POSTGRES_USER,
1414
},
15-
jwtSecret: process.env.JWT_SECRET,
15+
jwt: {
16+
jwtSecret: process.env.JWT_SECRET,
17+
jwtRefreshSecret: process.env.JWT_REFRESH_SECRET,
18+
refreshTokenExpiration: process.env.REFRESH_TOKEN_EXPIRATION,
19+
accessTokenExpiration: process.env.ACCESS_TOKEN_EXPIRATION,
20+
},
1621
};
1722
});

src/users/services/users.service.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
3+
import * as bcrypt from 'bcrypt';
34
import { Repository } from 'typeorm';
45
import { CreateUserDto, UpdateUserDto } from '../dto/create-user.dto';
56
import { User } from '../entities/user.entity';
@@ -41,4 +42,28 @@ export class UsersService {
4142
remove(id: number) {
4243
return `This action removes a #${id} user`;
4344
}
45+
46+
async setCurrentRefreshToken(refreshToken: string, userId: number) {
47+
const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10);
48+
49+
return await this.userRepository.update(userId, {
50+
refreshToken: currentHashedRefreshToken,
51+
});
52+
}
53+
54+
async removeRefreshToken(email: string) {
55+
const user = await this.findByEmailAndGetPassword(email);
56+
if (!user) {
57+
throw new HttpException(
58+
'User with this id does not exist',
59+
HttpStatus.NOT_FOUND,
60+
);
61+
}
62+
return this.userRepository.update(
63+
{ email },
64+
{
65+
refreshToken: null,
66+
},
67+
);
68+
}
4469
}

0 commit comments

Comments
 (0)