Skip to content

Commit

Permalink
Intergrate Redis to save refresh-token instead of client-cookie
Browse files Browse the repository at this point in the history
  • Loading branch information
zenkiet committed Sep 21, 2023
1 parent 62125b2 commit d101d84
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 5 deletions.
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"ioredis": "^5.3.2",
"nestjs-prisma": "^0.22.0",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
Expand Down
58 changes: 58 additions & 0 deletions server/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions server/src/iam/authentication/authentication.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AccessTokenStrategy } from './strategies/access-token/access-token.stra
import { AccessTokenGuard } from './guards/access-token/access-token.guard';
import { AuthenticationGuard } from './guards/authentication/authentication.guard';
import { APP_GUARD } from '@nestjs/core';
import { RefreshTokenIdsStorage } from './utils/refresh-token-ids.storage/refresh-token-ids.storage';

@Module({
providers: [
Expand All @@ -23,6 +24,7 @@ import { APP_GUARD } from '@nestjs/core';
provide: APP_GUARD,
useClass: AuthenticationGuard,
},
RefreshTokenIdsStorage,
AuthenticationService,
PrismaService,
AccessTokenStrategy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import { Body, Controller, Post } from '@nestjs/common';
import { AuthenticationService } from '../services/authentication.service';
import { SignUpDto } from '../dto/sign-up.dto/sign-up.dto';
import { SignInDto } from '../dto/sign-in.dto/sign-in.dto';
import { AccessTokenStrategy } from '../strategies/access-token/access-token.strategy';
import { Auth } from '../decorators/auth/auth.decorator';
import { AuthType } from '../enums/auth-type.enum';
import { RefreshTokenDto } from '../dto/refresh-token.dto/refresh-token.dto';

@Auth(AuthType.None)
@Controller('authentication')
export class AuthenticationController {
constructor(
private readonly authService: AuthenticationService,
private readonly accessTokenStrategy: AccessTokenStrategy,
) {}
constructor(private readonly authService: AuthenticationService) {}

@Post('sign-up')
signUp(@Body() signUpDto: SignUpDto) {
Expand All @@ -23,4 +20,9 @@ export class AuthenticationController {
signIn(@Body() signInDto: SignInDto) {
return this.authService.signIn(signInDto);
}

@Post('refresh-token')
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshTokens(refreshTokenDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RefreshTokenDto } from './refresh-token.dto';

describe('RefreshTokenDto', () => {
it('should be defined', () => {
expect(new RefreshTokenDto()).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsNotEmpty } from 'class-validator';

export class RefreshTokenDto {
@IsNotEmpty()
refreshToken: string;
}
47 changes: 47 additions & 0 deletions server/src/iam/authentication/services/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import jwtConfig from '../../config/jwt.config/jwt.config';
import { ConfigType } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { ActiveUserData } from '../interfaces/active-user-data.interface';
import {
RefreshTokenIdsStorage,
RefreshTokenIdsStorageError,
} from '../utils/refresh-token-ids.storage/refresh-token-ids.storage';
import { RefreshTokenDto } from '../dto/refresh-token.dto/refresh-token.dto';

@Injectable()
export class AuthenticationService {
Expand All @@ -24,6 +29,7 @@ export class AuthenticationService {
@Inject(jwtConfig.KEY)
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
private readonly jwtService: JwtService,
private readonly refreshTokenIdsStorage: RefreshTokenIdsStorage,
) {}

private async checkUserExist(email: string): Promise<User | null> {
Expand Down Expand Up @@ -92,6 +98,9 @@ export class AuthenticationService {
this.signToken(user.id, this.jwtConfiguration.refreshTokenTtl, {
refreshTokenId,
}),

//* Insert refreshTokenId into storage
await this.refreshTokenIdsStorage.insert(user.id, refreshTokenId),
]);

return {
Expand All @@ -100,6 +109,44 @@ export class AuthenticationService {
};
}

async refreshTokens(refreshToken: RefreshTokenDto) {
try {
const { sub, refreshTokenId } = await this.jwtService.verifyAsync<
Pick<ActiveUserData, 'sub'> & { refreshTokenId: string }
>(refreshToken.refreshToken, {
secret: this.jwtConfiguration.secret,
audience: this.jwtConfiguration.audience,
issuer: this.jwtConfiguration.issuer,
});

const user = await this.prismaService.user.findUnique({
where: {
id: sub,
},
});

if (!user) throw new UnauthorizedException('User not found');

const isValid = await this.refreshTokenIdsStorage.validate(
sub,
refreshTokenId,
);

if (isValid) {
await this.refreshTokenIdsStorage.invalidate(user.id);
} else {
throw new UnauthorizedException('Invalid refresh token');
}

return await this.generateToken(user);
} catch (error) {
if (error instanceof RefreshTokenIdsStorageError) {
throw new UnauthorizedException('Invalid refresh token');
}
throw new UnauthorizedException(error.message);
}
}

private async signToken<T>(userID: number, expiresIn: number, payload?: T) {
return await this.jwtService.signAsync(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RefreshTokenIdsStorage } from './refresh-token-ids.storage';

describe('RefreshTokenIdsStorage', () => {
it('should be defined', () => {
expect(new RefreshTokenIdsStorage()).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
Injectable,
OnApplicationBootstrap,
OnApplicationShutdown,
} from '@nestjs/common';

import redisClient from 'ioredis';

export class RefreshTokenIdsStorageError extends Error {
constructor(message: string) {
super(message);
}
}

@Injectable()
export class RefreshTokenIdsStorage
implements OnApplicationBootstrap, OnApplicationShutdown
{
private redisClient: redisClient;

onApplicationBootstrap() {
this.redisClient = new redisClient(process.env.REDIS_URL);
}
onApplicationShutdown() {
this.redisClient.quit();
}

async insert(userId: number, tokenID: string): Promise<void> {
await this.redisClient.set(this.getKey(userId), tokenID);
}

async validate(userId: number, tokenID: string): Promise<boolean> {
const storedTokenID = await this.redisClient.get(this.getKey(userId));
if (!storedTokenID)
throw new RefreshTokenIdsStorageError('Token ID not found');
return storedTokenID === tokenID;
}

async invalidate(userId: number): Promise<void> {
await this.redisClient.del(this.getKey(userId));
}

private getKey(userId: number): string {
return `user-${userId}`;
}
}

0 comments on commit d101d84

Please sign in to comment.