개발일지
[NestJS] 로그인 구현 #2 - Passport, JWT 본문
개념 정리
Passport
Passport는 Node.js에서 제공해주는 인증 미들웨어입니다. NestJS에서는 @nestjs/passport
모듈로써 NestJS 환경에 맞게 사용할 수 있도록 합니다.
Passport는 인증 방식에 따른 다양한 전략(Strategy)이 존재하며, 다양한 인증 단계를 표준 패턴으로 추상화하여 제공합니다. 먼저 @nestjs/passport
를 사용하기 전 vanilla Passport 작동방식에 대해 알아봅시다.
1. 해당 전략에 적용할 옵션. 예를 들어 jwt strategy에서 token 서명에 필요한 secret key를 적용할 수 있습니다.
2. verify callback. 사용자의 존재여부 또는 자격 증명 유효성 검증 등이 이뤄집니다. 유효성 검증에 성공하면 user 객체를 반환하고 실패하면 null을 반환합니다.
@nestjs/passport
는 PassportStrategy class를 확장하여 Passport 전략을 구성합니다.
- sub class에서
super()
메소드 호출 시 선택적으로 옵션 객체를 전달 (1) - sub class의
validate()
메소드를 구현하여 verify callback 제공 (2)
Guard
Guard를 사용해 인증되지 않은 사용자에 대한 접근을 제한할 것입니다. @nestjs/passport
모듈에서는 AuthGuard
를 제공합니다. 이 Guard는 적절한 Passport Strategy를 호출합니다.
JWT 패키지 설치
jwt 인증 방식과 passport를 사용하기 위해 필요한 패키지를 설치합니다.
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
JWT 인증 구현하기
// users/jwt/jwt.strategy.ts
@Injectable()
export class JwtAccessStrategy extends PassportStrategy(
Strategy,
'jwt-access-token',
) {
constructor(private readonly usersRepository: UsersRepository) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'access-secret-key', // 임시
});
}
async validate(payload: JwtPayload) {
...
}
}
위의 항목1에 해당하는 전략 옵션입니다.
jwtFromRequest
: Request에서 JWT를 추출하는 방법. API 요청의 Authorization 헤더에 Bearer Token을 제공하는 표준 방식 사용.ignoreExpiration
: false 경우 JWT가 Passport 모듈에 만료되지 않았는지 확인하는 책임을 위임함. 즉, 경로에 만료된 JWT가 제공되면 요청이 거부되고 401 Unauthorized응답 전송함.secretOrKey
: token 서명 및 인증시 필요한 secret key. (대칭키 방식)
verify callback validate()
를 구현합니다.
jwt-strategy
는 jwt를 검증하고 json으로 디코딩합니다. 그 후 validate 메소드의 매개변수(payload)로 전달합니다. payload 형태(JwtPayload)는 다음과 같습니다.
export type JwtPayload = {
sub: number;
nickname: string;
};
사용자의 존재여부를 판단한 뒤, 존재할 경우 해당 user 객체를 반환합니다.
async validate(payload: JwtPayload) {
const whereOption: WhereOptionByUserId = {
id: payload.sub,
};
const user: Users = await this.usersRepository.findUserByWhereOption(
whereOption,
);
if (!user) {
throw new CustomException(UsersException.USER_NOT_EXIST);
}
const { password, ...userInfo } = user;
return userInfo;
}
}
JwtAccessGuard
JwtAccessStrategy를 호출 하는 guard를 생성합니다. 저는 jwt 검증 에러 처리를 더 세분화 하고 싶어 canActivate를 재정의했습니다.
// users/jwt/users.guard.ts
@Injectable()
export class JwtAccessGuard extends AuthGuard('jwt-access-token') {
constructor(private readonly jwtService: JwtService) {
super();
}
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const { authorization } = request.headers;
if (authorization === undefined) {
throw new CustomException(UsersException.TOKEN_NOT_EXISTS);
}
const accessToken: string = authorization.split(' ')[1];
try {
this.jwtService.verify(accessToken, {
secret: 'access-secret-key',
});
} catch (error) {
switch (error.message) {
case 'jwt expired':
throw new CustomException(UsersException.EXPIRED_TOKEN);
default:
throw new CustomException(UsersException.UNVERIFIED_ACCESS_TOKEN);
}
}
return super.canActivate(context);
}
}
JWT 생성하기
이제 users module에 PassportModule과 JwtModule을 등록해줍니다. JwtModule.register의 secret은 토큰 서명시 사용되는 secret key로 위에서 설정한 key와 동일해야합니다.
// users/users.module.ts
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt-access-token' }),
JwtModule.register({
secret: 'access-secret-key',
signOptions: {
expiresIn: '3m', //임시
},
}),
],
...
})
다시 service로 돌아가 access token, refresh token 생성하는 메소드를 구현합니다. jwt 관련된 클래스 TokenService
class를 생성했습니다. JwtService
는 @nestjs/jwt
에 내장된 서비스로 jwt 생성 .sign()
, 검증 .verify()
등의 기능을 제공해줍니다.
// users/users.service.ts
import { JwtService } from '@nestjs/jwt';
class TokenService {
private readonly jwtService: JwtService;
constructor(private readonly usersRepository: UsersRepository) {
this.jwtService = new JwtService();
}
createAccessToken(payload: JwtPayload): string {
const accessToken: string = this.jwtService.sign(payload, {
secret: 'access-secret-key', // 임시
expiresIn: '3m', // 임시
});
return accessToken;
}
async createRefreshToken(payload: JwtPayload): Promise<string> {
const refreshToken: string = this.jwtService.sign(payload, {
secret: 'refresh-secret-key',
expiresIn: '10m',
});
// 암호화, 후에 설명
const encryptRefreshToken: string = this._encryptRefreshToken(refreshToken);
await this.usersRepository.createRefreshToken(
payload.sub,
encryptRefreshToken,
);
return encryptRefreshToken;
}
}
refresh token은 생성 후 db에 저장해야합니다. users.repository에 token을 저장하는 메소드를 구현합니다. connect
옵션으로 user와 일대일 매핑해줍니다.
// users/users.repository.ts
async createRefreshToken(
userId: number,
encryptedRefreshToken: string,
): Promise<RefreshTokens> {
try {
return await this.prisma.refreshTokens.create({
data: {
token: encryptedRefreshToken,
user: {
connect: {
id: userId,
},
},
},
});
} catch (error) {
throw new CustomException(UsersException.REFRESH_TOKEN_EXIST);
}
}
완성된 signIn 서비스는 다음과 같습니다. 보안성을 위해 refresh token은 암호화한 뒤 response 해줍니다.
// users/users.service.ts
@Injectable()
export class UsersService {
private readonly tokenService: TokenService;
constructor(private readonly usersRepository: UsersRepository) {
this.tokenService = new TokenService(usersRepository);
}
...
async signIn(data: SignInUserDto): Promise<SignIn> {
const whereOption: WhereOptionByUserEmail = {
email: data.email,
};
const user: Users = await this.usersRepository.findUserByWhereOption(
whereOption,
);
if (!user) {
throw new CustomException(UsersException.USER_NOT_EXIST);
}
await this._validatePassword(data.password, user.password, 'signIn');
// 토큰 생성
const payload: JwtPayload = { sub: user.id, nickname: user.nickname };
const accessToken: string = this.tokenService.createAccessToken(payload);
const encryptRefreshToken: string =
await this.tokenService.createRefreshToken(payload);
return { accessToken, refreshToken: encryptRefreshToken };
}
}
'NestJS, Node.js > #01 Project - 투표 커뮤니티' 카테고리의 다른 글
[NestJS] Refresh token으로 Access token 재발급하기 (0) | 2022.10.28 |
---|---|
[NestJS] Custom decorators (0) | 2022.10.28 |
[NestJS] 로그인 구현 #1 - 유효성 검증 (0) | 2022.10.27 |
[NestJS] 회원가입 구현 #2 - 유효성 검증 (0) | 2022.10.27 |
[NestJS] 회원가입 구현 #1 - Pipe 적용 (0) | 2022.10.27 |