Skip to content

Commit

Permalink
feat(auth): AuthGuard에 전략패턴 적용하여 AT, RT authorization 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
IsthisLee committed Aug 13, 2023
1 parent f312f5a commit d7356c4
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 183 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.1.0",
"@nestjs/passport": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.1.7",
"@nestjs/throttler": "^4.2.1",
Expand All @@ -45,6 +46,8 @@
"express-basic-auth": "^1.2.1",
"google-auth-library": "^9.0.0",
"openai": "^4.0.0-beta.9",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"xml-js": "^1.6.11"
Expand All @@ -57,6 +60,7 @@
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^3.0.9",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
Expand Down
20 changes: 9 additions & 11 deletions src/apis/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Controller, Param, ParseEnumPipe, Body, Res, Req, Post, UseGuards } fro
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { Provider } from '@prisma/client';
import { Request, Response } from 'express';
import { Response } from 'express';
import { LoginDto } from './dtos/login.dto';
import { AuthGuard } from './security/guards/access-token.guard';
import { SetAuthGuardType } from 'src/common/decorators/auth-guard-type.decorator';
import { AuthGuardType } from 'src/common/types';
import { AuthGuard } from '@nestjs/passport';
import { JwtUserPayload } from 'src/common/decorators/jwt-user.decorator';
import { JwtPayloadInfo } from 'src/common/types';

@Controller('auth')
@ApiTags('Auth')
Expand Down Expand Up @@ -55,17 +55,15 @@ export class AuthController {
}

@Post('/silent-refresh')
@SetAuthGuardType(AuthGuardType.REFRESH)
@UseGuards(AuthGuard)
@UseGuards(AuthGuard('jwt-refresh'))
@ApiOperation({
summary: '토큰 리프레시 API',
description: `
AT 만료 시 쿠키에 담겨오는 RT를 활용하여 AT를 refresh합니다.(보안을 위해 RT도 함께 refresh 됩니다.)`,
AT 만료 시 쿠키에 담겨오는 RT를 활용하여 AT를 refresh합니다.(추가 보안 작업 하면서 RT refresh도 함께 진행될 예정)`,
})
async refreshToken(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
const userRefreshToken = req.cookies['refreshToken'];
const { accessToken, refreshToken } = await this.authService.refreshToken(res, userRefreshToken);
this.authService.setRefreshToken(res, refreshToken);
async refreshToken(@JwtUserPayload() jwtUser: JwtPayloadInfo, @Res({ passthrough: true }) res: Response) {
const { accessToken } = await this.authService.generateTokens(jwtUser);
// this.authService.setRefreshToken(res, refreshToken); // TODO: 추가 보안 작업 및 RT refresh도 함께 진행할 것.

return { accessToken };
}
Expand Down
9 changes: 5 additions & 4 deletions src/apis/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Global, Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { AccessTokenStrategy } from './security/strategies/access-token.strategy';
import { RefreshTokenStrategy } from './security/strategies/refresh-token.strategy';

@Global()
@Module({
imports: [
UsersModule,
Expand All @@ -13,7 +14,7 @@ import { JwtModule } from '@nestjs/jwt';
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService, UsersModule],
providers: [AuthService, AccessTokenStrategy, RefreshTokenStrategy],
exports: [AuthService],
})
export class AuthModule {}
56 changes: 9 additions & 47 deletions src/apis/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { UserPayloadInfo, CreateUserInfo } from 'src/common/types';
import { JwtPayloadInfo, CreateUserInfo } from 'src/common/types';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Provider, User } from '@prisma/client';
Expand Down Expand Up @@ -39,45 +39,13 @@ export class AuthService {

// 토큰 발급
const userPayloadInfo = this.getUserPayloadInfo(user);
const { accessToken, refreshToken } = await this.getTokens(userPayloadInfo);
const { accessToken, refreshToken } = await this.generateTokens(userPayloadInfo);

return { accessToken, refreshToken };
}

async refreshToken(res: Response, userRefreshToken: string) {
let userId: number;

if (!userRefreshToken) {
throw new UnauthorizedException('Refresh Token이 없습니다.');
}

try {
const payload = await this.verifyToken(userRefreshToken, {
isRefreshToken: true,
});
userId = payload.userId;
} catch (err) {
res.cookie('refreshToken', '', {
maxAge: 0,
});
throw new UnauthorizedException(err.message);
}

const user = await this.usersService.findOneById(userId);
if (!user) {
throw new UnauthorizedException('존재하지 않는 유저입니다.');
}

const { accessToken, refreshToken } = await this.getTokens(this.getUserPayloadInfo(user));

return {
accessToken,
refreshToken,
};
}

verifyToken(token: string, { isRefreshToken }: { isRefreshToken: boolean } = { isRefreshToken: false }) {
const payload: UserPayloadInfo | { userId: number } = this.jwtService.verify(token, {
const payload: JwtPayloadInfo = this.jwtService.verify(token, {
secret: this.configService.get(`${isRefreshToken ? 'REFRESH' : 'ACCESS'}_SECRET_KEY`),
});

Expand Down Expand Up @@ -110,28 +78,22 @@ export class AuthService {
return account;
}

getUserPayloadInfo(user: User): UserPayloadInfo {
getUserPayloadInfo(user: User): JwtPayloadInfo {
return {
userId: user.id,
email: user.email,
name: user.name,
profileImage: user.profileImage,
};
}

private async getTokens(user: UserPayloadInfo): Promise<{ accessToken: string; refreshToken: string }> {
async generateTokens(user: JwtPayloadInfo): Promise<{ accessToken: string; refreshToken: string }> {
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(user, {
secret: this.configService.get('ACCESS_SECRET_KEY'),
expiresIn: this.configService.get('ACCESS_TOKEN_EXPIRES_IN'),
}),
this.jwtService.signAsync(
{ userId: user.userId },
{
secret: this.configService.get('REFRESH_SECRET_KEY'),
expiresIn: this.configService.get('REFRESH_TOKEN_EXPIRES_IN'),
},
),
this.jwtService.signAsync(user, {
secret: this.configService.get('REFRESH_SECRET_KEY'),
expiresIn: this.configService.get('REFRESH_TOKEN_EXPIRES_IN'),
}),
]);

return {
Expand Down
97 changes: 0 additions & 97 deletions src/apis/auth/security/guards/access-token.guard.ts

This file was deleted.

32 changes: 32 additions & 0 deletions src/apis/auth/security/strategies/access-token.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';
import { JwtPayloadInfo } from '../../../../common/types';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt-access') {
constructor(private readonly configService: ConfigService) {
// token 유효 확인
super({
secretOrKey: configService.get<string>('ACCESS_SECRET_KEY'),
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
const headerToken = this.extractTokenFromHeader(request);
return headerToken;
},
]),
passReqToCallback: true,
});
}

validate(req: Request, payload: JwtPayloadInfo): JwtPayloadInfo {
return payload; // req.user에 저장됨.
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
48 changes: 48 additions & 0 deletions src/apis/auth/security/strategies/refresh-token.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';
import { JwtPayloadInfo } from '../../../../common/types';
import { ConfigService } from '@nestjs/config';
import { NotFoundException } from '@nestjs/common';
import { UsersService } from '../../../users/users.service';
import { AuthService } from '../../auth.service';

@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService,
private readonly authService: AuthService,
) {
// token 유효 확인
super({
secretOrKey: configService.get<string>('REFRESH_SECRET_KEY'),
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
const cookieToken = request.cookies['refreshToken'];
return cookieToken;
},
]),
passReqToCallback: true,
});
}

async validate(req: Request, payload: JwtPayloadInfo): Promise<JwtPayloadInfo> {
const { userId } = payload;
const user = await this.usersService.findOneById(userId);
if (!user) {
throw new NotFoundException('토큰값에 해당하는 유저가 존재하지 않습니다.');
}

// TODO: 1. RT의 jti 검증
// -> RT 토큰의 jti를 Cache에 저장해두고 검증
// -> RT 토큰의 jti가 Cache에 존재하는 jti와 일치하지 않으면 검증 실패

// TODO: 2. AT 리프래시 할 때 RT도 함께 리프래시하고, 새로운 RT의 jti를 Cache에 저장
// -> 최초 로그인 시에도 RT의 jti를 Cache에 저장해야 함.
// -> (TODO2 로직은 AuthController의 silentRefresh API에서 구현)

return payload; // req.user에 저장됨.
}
}
5 changes: 0 additions & 5 deletions src/common/decorators/auth-guard-type.decorator.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/common/decorators/jwt-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const JwtUserPayload = createParamDecorator((data, ctx: ExecutionContext): ParameterDecorator => {
const request = ctx.switchToHttp().getRequest();
return request.user;
});
8 changes: 0 additions & 8 deletions src/common/decorators/user.decorator.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/common/swagger-setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const swaggerCustomOptions: SwaggerCustomOptions = {
export const setupSwagger = (app: INestApplication): void => {
const config = new DocumentBuilder()
.setTitle('LawLow(로우로우) API Docs')
.setDescription('LawLow(로우로우) API 명세서에 오신 걸 환영합니다 ^~^ \n\n 궁금한 점은 언제든지 물어봐 주세용!')
.setDescription('LawLow(로우로우) API 명세서에 오신 걸 환영합니다 ^~^ \n\n 궁금한 점은 언제든지 물어봐 주세요~!')
.setVersion('0.0.1')
.addBearerAuth(
{
Expand All @@ -19,7 +19,7 @@ export const setupSwagger = (app: INestApplication): void => {
name: 'JWT',
in: 'header',
},
'accessToken',
'access-token',
)
.build();

Expand Down
Loading

0 comments on commit d7356c4

Please sign in to comment.