Skip to content

Commit

Permalink
feat: 리프레시 토큰 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
son-daehyeon committed Aug 16, 2024
1 parent 23d7eb7 commit 389b8db
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 12 deletions.
4 changes: 3 additions & 1 deletion config/config.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ s3:

jwt:
secret: ${JWT_SECRET:=secret}
expiresIn: ${JWT_EXPIRES_IN:=14d}
expiresIn:
access: ${JWT_EXPIRES_IN_ACCESS:=1h}
refresh: ${JWT_EXPIRES_IN_REFRESH:=7d}
6 changes: 6 additions & 0 deletions src/common/util/event/type/service/auth.service.event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export class LoginEvent {
constructor(public readonly member: Member) {}
}

export class RefreshEvent {
public static readonly EVENT_NAME = 'auth.refresh';

constructor(public readonly member: Member) {}
}

export class RegisterEvent {
public static readonly EVENT_NAME = 'auth.register';

Expand Down
5 changes: 5 additions & 0 deletions src/domain/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { MailModule } from '@wink/mail';
providers: [
AuthService,

{
provide: `${RedisService.name}-refresh`,
useFactory: (repository: RedisService) => repository.sub('auth:refresh'),
inject: [RedisService],
},
{
provide: `${RedisService.name}-code`,
useFactory: (repository: RedisService) => repository.sub('auth:code'),
Expand Down
13 changes: 13 additions & 0 deletions src/domain/auth/controller/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
LoginRequestDto,
LoginResponseDto,
MyInfoResponseDto,
RefreshRequestDto,
RefreshResponseDto,
RegisterRequestDto,
SendCodeRequestDto,
VerifyCodeRequestDto,
Expand All @@ -13,6 +15,7 @@ import {
import {
AlreadyRegisteredByEmailException,
AlreadyRegisteredByStudentIdException,
InvalidRefreshTokenException,
InvalidVerifyCodeException,
InvalidVerifyTokenException,
MemberNotFoundException,
Expand Down Expand Up @@ -45,6 +48,16 @@ export class AuthController {
return this.authService.login(request);
}

@Post('/refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '토큰 재발행' })
@ApiProperty({ type: RefreshRequestDto })
@ApiCustomResponse({ type: RefreshResponseDto, status: HttpStatus.OK })
@ApiCustomErrorResponse([InvalidRefreshTokenException, MemberNotFoundException])
async refresh(@Body() request: RefreshRequestDto): Promise<RefreshResponseDto> {
return this.authService.refresh(request);
}

@Post('/register')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '회원가입' })
Expand Down
22 changes: 15 additions & 7 deletions src/domain/auth/guard/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import {
SetMetadata,
UseGuards,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { JwtService, TokenExpiredError } from '@nestjs/jwt';
import { ApiBearerAuth } from '@nestjs/swagger';

import { PermissionException, UnauthorizedException } from '@wink/auth/exception';
import {
ExpiredAccessTokenException,
PermissionException,
UnauthorizedException,
} from '@wink/auth/exception';

import { Role } from '@wink/member/constant';
import { NotApprovedMemberException } from '@wink/member/exception';
Expand All @@ -38,7 +42,11 @@ export class AuthGuard implements CanActivate {
let decoded: Record<string, unknown>;
try {
decoded = await this.jwtService.verifyAsync(token as string);
} catch {
} catch (error) {
if (error instanceof TokenExpiredError) {
throw new ExpiredAccessTokenException();
}

throw new UnauthorizedException();
}

Expand Down Expand Up @@ -104,10 +112,10 @@ export const AuthAdminAccount = () =>
ApiBearerAuth(),
);

export const AuthAccountException = [UnauthorizedException, NotApprovedMemberException];

export const AuthAdminAccountException = [
export const AuthAccountException = [
UnauthorizedException,
ExpiredAccessTokenException,
NotApprovedMemberException,
PermissionException,
];

export const AuthAdminAccountException = [...AuthAccountException, PermissionException];
71 changes: 67 additions & 4 deletions src/domain/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { EventEmitter2 } from '@nestjs/event-emitter';

import {
LoginRequestDto,
LoginResponseDto,
MyInfoResponseDto,
RefreshRequestDto,
RefreshResponseDto,
RegisterRequestDto,
SendCodeRequestDto,
VerifyCodeRequestDto,
Expand All @@ -14,6 +17,7 @@ import {
import {
AlreadyRegisteredByEmailException,
AlreadyRegisteredByStudentIdException,
InvalidRefreshTokenException,
InvalidVerifyCodeException,
InvalidVerifyTokenException,
MemberNotFoundException,
Expand All @@ -25,24 +29,39 @@ import { MemberRepository } from '@wink/member/repository';
import { Member, omitMember } from '@wink/member/schema';

import { RedisService } from '@wink/redis';
import { LoginEvent, RegisterEvent, SendCodeEvent, VerifyCodeEvent } from '@wink/event';
import {
LoginEvent,
RefreshEvent,
RegisterEvent,
SendCodeEvent,
VerifyCodeEvent,
} from '@wink/event';
import { MailService, RegisterCompleteTemplate, VerifyCodeTemplate } from '@wink/mail';

import { v4 as uuid } from 'uuid';
import ms, { StringValue } from 'ms';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
private readonly accessExpiresIn: string;
private readonly refreshExpiresIn: string;

constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,

private readonly memberRepository: MemberRepository,
@Inject(`${RedisService.name}-refresh`) private readonly redisRefreshRepository: RedisService,
@Inject(`${RedisService.name}-code`) private readonly redisCodeRepository: RedisService,
@Inject(`${RedisService.name}-token`) private readonly redisTokenRepository: RedisService,

private readonly eventEmitter: EventEmitter2,
private readonly mailService: MailService,
) {}
) {
this.accessExpiresIn = this.configService.getOrThrow<string>('jwt.expiresIn.access');
this.refreshExpiresIn = this.configService.getOrThrow<string>('jwt.expiresIn.refresh');
}

async login({ email, password }: LoginRequestDto): Promise<LoginResponseDto> {
const member = await this.memberRepository.findByEmailWithPassword(email);
Expand All @@ -59,11 +78,55 @@ export class AuthService {
throw new NotApprovedMemberException();
}

const token = await this.jwtService.signAsync({ id: member._id });
const accessToken = await this.jwtService.signAsync(
{ id: member._id },
{ expiresIn: this.accessExpiresIn },
);

const refreshToken = await this.jwtService.signAsync({}, { expiresIn: this.refreshExpiresIn });

await this.redisRefreshRepository.ttl(
refreshToken,
member._id,
ms(this.refreshExpiresIn as StringValue),
);

this.eventEmitter.emit(LoginEvent.EVENT_NAME, new LoginEvent(member));

return { token };
return { accessToken, refreshToken };
}

async refresh({ refreshToken }: RefreshRequestDto): Promise<RefreshResponseDto> {
const memberId = await this.redisRefreshRepository.get(refreshToken);
if (!memberId) {
throw new InvalidRefreshTokenException();
}
await this.redisRefreshRepository.delete(refreshToken);

const member = await this.memberRepository.findById(memberId);
if (!member) {
throw new MemberNotFoundException();
}

const newAccessToken = await this.jwtService.signAsync(
{ id: memberId },
{ expiresIn: this.accessExpiresIn },
);

const newRefreshToken = await this.jwtService.signAsync(
{},
{ expiresIn: this.refreshExpiresIn },
);

await this.redisRefreshRepository.ttl(
newRefreshToken,
memberId,
ms(this.refreshExpiresIn as StringValue),
);

this.eventEmitter.emit(RefreshEvent.EVENT_NAME, new RefreshEvent(member));

return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

async register({ name, studentId, password, verifyToken }: RegisterRequestDto): Promise<void> {
Expand Down

0 comments on commit 389b8db

Please sign in to comment.