NestJS JWT Auth 구현하기
NestJS에서 Access_Token, Refresh_Token을 이용한 Auth 구현을 소개 합니다 .

구현 목표
JWT를 사용하여 NestJS서버에서 토큰 기반 인증을 구현 합니다 .
구현할 API는 다음과 같습니다 .
회원가입
로그인
refresh_token을 이용한 토큰 발급
프로젝트 생성부터 구현까지 전 과정에 대해 설명할 예정이지만, JWT, 토큰기반인증에 대한 개념들은 따로 다루지 않겠습니다 !
구현 과정
프로젝트 생성
nest new nest-jwt-auth
프로젝트 이름은 nest-jwt-auth 입니다 . 패키지 매니저는 npm을 사용 하였습니다.
데이터베이스 연결
npm install --save @nestjs/typeorm typeorm mysql2
이 프로젝트에서 데이터베이스는 mysql, ORM은 TypeORM을 사용 하겠습니다 !
App.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '1234',
database: 'nest-jwt-auth-dev',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
TypemORM 모듈을 AppModule에 imports 합니다. 여기서 데이터베이스 정보는 본인의 데이터베이스 정보에 맞게 입력하시면 됩니다.
NestJS에서 TypeORM부분에 대해서 공식문서에 자세히 나와 있습니다.
이제 User Entity를 구현하기 앞서 User Module을 생성 하겠습니다.
nest g res users --no-spec
user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
password: string;
@Column({ name: 'refresh_token', default: null })
refreshToken: string;
}
user.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
User Entity Class를 구현 합니다. refreshToken은 일반적으로 따로 테이블을 구성하지만, 이번 프로젝트에선 User 테이블 내에서 관리 하겠습니다 . synchronize 옵션을 true로 설정하였기 때문에, 서버를 구동시키면, user 테이블이 생성된 것을 확인하실 수 있습니다 .

JWT 인증 구현
auth 리소스 생성 및 패키지 설치
nest g res auth --no-spec
npm install --save @nestjs/passport @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
auth 폴더 내에 strategies폴더를 생성하고, 아래 두 파일을 생성 합니다.
access.token.strategy.ts
refresh.token.strategy.ts
access.token.strategy.ts
import {
Injectable,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
type JwtPayload = {
sub: string;
name: string;
};
@Injectable()
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'access-token-secret',
});
}
async validate(payload: JwtPayload) {
try {
const user = await this.userRepository.findOne({
where: { id: Number(payload.sub) },
});
if (!user) {
throw new UnauthorizedException('invalid token');
}
return user;
} catch (error) {
throw new InternalServerErrorException(error.message);
}
}
}
refresh.token.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';
import { Injectable } from '@nestjs/common';
@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(
Strategy,
'jwt-refresh',
) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'refresh-token-secret',
passReqToCallback: true,
});
}
validate(req: Request, payload: any) {
const refreshToken = req.get('Authorization').replace('Bearer', '').trim();
return { ...payload, refreshToken };
}
}
validate는 refreshToken을 추출하고 반환된 객체에 전달합니다. 이렇게 하면 이후 refreshtoken을 이용하여 토큰을 재발급 할 때 접근할 수 있습니다.
auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { AccessTokenStrategy } from './strategies/access.token.strategy';
import { RefreshTokenStrategy } from './strategies/refresh.token.strategy';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
@Module({
imports: [JwtModule.register({}), TypeOrmModule.forFeature([User])],
controllers: [AuthController],
providers: [AuthService, AccessTokenStrategy, RefreshTokenStrategy],
})
export class AuthModule {}
생성한 AccessTokenStrategy와 RefreshTokenStrategy를 providers로 주입하고, JwtModule와 TypeORM을 import 합니다.
auth.service.ts 구현
npm install argon2
회원가입을 구현하기 전 비밀번호 암호화를 위해 argon2를 설치 합니다.
auth.service.ts
import {
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
import * as argon2 from 'argon2';
import { JwtService } from '@nestjs/jwt';
import { SignUpDto } from './dto/sign.up.dto';
import { SignInDto } from './dto/sign.in.dto';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly jwtService: JwtService,
) {}
async signUp(body: SignUpDto) {
const userExists = await this.userRepository.findOne({
where: { name: body.name },
});
if (!!userExists) {
throw new ConflictException('User already exists');
}
const hashedPassword = await argon2.hash(body.password);
const user = await this.userRepository.save({
name: body.name,
password: hashedPassword,
});
const tokens = await this.generateTokens(user.id, user.name);
await this.updateRefreshToken(user, tokens.refreshToken);
await this.userRepository.save(user);
return tokens;
}
async signIn(body: SignInDto) {
const user = await this.userRepository.findOne({
where: { name: body.name },
});
if (!user) {
throw new NotFoundException('User does not exist');
}
const isPasswordValid = await argon2.verify(user.password, body.password);
if (!isPasswordValid) {
throw new ConflictException('Invalid password');
}
const tokens = await this.generateTokens(user.id, user.name);
await this.updateRefreshToken(user, tokens.refreshToken);
await this.userRepository.save(user);
return tokens;
}
async generateTokens(userId: number, name: string) {
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(
{
sub: userId,
name,
},
{
secret: 'access-token-secret',
expiresIn: '15m',
},
),
this.jwtService.signAsync(
{
sub: userId,
name,
},
{
secret: 'refresh-token-secret',
expiresIn: '7d',
},
),
]);
return {
accessToken,
refreshToken,
};
}
async updateRefreshToken(user: User, refreshToken: string) {
const hashedRefreshToken = await argon2.hash(refreshToken);
user.refreshToken = hashedRefreshToken;
await this.userRepository.save(user);
}
async refreshTokens(refreshToken: string, userId: number) {
const user = await this.userRepository.findOne({
where: { id: userId },
});
if (!user || !user.refreshToken) {
throw new ForbiddenException('Access Denied');
}
const refreshTokenMatched = await argon2.verify(
user.refreshToken,
refreshToken,
);
if (!refreshTokenMatched) {
throw new ForbiddenException('Access Denied');
}
const tokens = await this.generateTokens(user.id, user.name);
await this.updateRefreshToken(user, tokens.refreshToken);
return tokens;
}
}
signUp
회원가입 후 token 정보를 반환 합니다.
signIn
로그인 후 token 정보를 반환 합니다.
generateTokens
access_token과 refresh_token을 생성 합니다.
updateRefreshToken
유저정보의 refresh_token 정보를 최신화 합니다.
refreshTokens
refresh 토큰을 이용하여 새로운 token을 발급 받습니다.
Guards 구현
src/common/guards 경로에 아래 두 guards를 구현 합니다.
access.token.guard.ts
refresh.token.guard.ts
access.token.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class AccessTokenGuard extends AuthGuard('jwt') {}
refresh.token.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class RefreshTokenGuard extends AuthGuard('jwt-refresh') {}
Controller 구현
auth.controller.ts
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignUpDto } from './dto/sign.up.dto';
import { SignInDto } from './dto/sign.in.dto';
import { Request } from 'express';
import { RefreshTokenGuard } from 'src/common/guards/refresh.token.guard';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('sign-up')
async signUp(@Body() body: SignUpDto) {
return this.authService.signUp(body);
}
@Post('sign-in')
async signIn(@Body() body: SignInDto) {
return this.authService.signIn(body);
}
@UseGuards(RefreshTokenGuard)
@Get('refresh')
async refresh(@Req() req: Request) {
const userId = req.user['sub'];
const refreshToken = req.user['refreshToken'];
return this.authService.refreshTokens(refreshToken, userId);
}
}
마무리
JWT 토큰을 이용한 토큰기반인증을 간단하게 구현해봤습니다. 내용자체도 많이 부실한 것 같고, 내용에 대한 설명도 부실한 것 같습니다...ㅠㅠ 문서작업능력도 중요한 능력이라고 생각하여 한번씩 블로그 글을 작성하려고 노력하고 있는데, 아직 많이 어려운 것 같습니다.
좋게 봐주시고, 도움이 되셨으면 좋겠습니다.
![[til] 프로그래머스 신규아이디 추천](https://cdn.hashnode.com/res/hashnode/image/upload/v1745249371004/97aa7a0b-1b1b-4f81-a5ef-790b9b682f08.png)
![[til] 알고리즘 백준 리그 오브 레전설](https://cdn.hashnode.com/res/hashnode/image/upload/v1745007840153/c6cf7c45-0d8f-4bee-ae9a-55cc454f3c92.png)
![[til] 알고리즘 백준 진우의 달 여행 (Small)](https://cdn.hashnode.com/res/hashnode/image/upload/v1744914681507/e80e8747-d4ff-4fd4-b595-33024a238ee1.png)
![[til] 알고리즘 JadenCase 문자열 만들기](https://cdn.hashnode.com/res/hashnode/image/upload/v1744823424388/57f5c5c1-7e85-4071-88e8-1ec09ed64828.png)
![[til] 알고리즘 백준 포도주 시식](https://cdn.hashnode.com/res/hashnode/image/upload/v1744724798661/286b75a2-50e0-481e-8e3f-2a6cb500d678.png)