최근 nestjs-boilerplate를 참고하여 PassportStrategy를 사용해 인증 로직을 정리해보았다. nestjs-boilerplate를 기반으로 한 auth 중심의 프로젝트 구조를 알아보고 PassportStrategy를 활용해 인증을 구현해보자.
nestjs-boilerplate/
└── src/
├── auth-google/
│ ├── config/
│ │ ├── google-config.type.ts
│ │ └── google.config.ts
│ ├── dto/
│ │ └── auth-google-login.dto.ts
│ ├── auth-google.controller.ts
│ ├── auth-google.module.ts
│ └── auth-google.service.ts
├── auth-twitter/
└── auth/
├── config/
│ ├── auth-config.type.ts
│ └── auth.config.ts
├── dto/
├── strategies/
│ ├── types/
│ │ ├── jwt-payload.type.ts
│ │ └── jwt-refresh-payload.type.ts
│ ├── anonymous.strategy.ts
│ ├── jwt-refresh.strategy.ts
│ └── jwt.strategy.ts
├── auth-providers.enum.ts
├── auth.controller.ts
├── auth.module.ts
└── auth.service.ts
kebab-case.file-type.ts
형식을 따른다.auth/
폴더에 구현되어 있다.
- 사용자 데이터를 제3자 서비스와 동기화하거나, 특정 서비스와의 연결 상태를 관리해야 하는 경우에는 지금처럼 OAuth API의 토큰 재발급, 로그아웃, 회원 탈퇴 등의 기능을 사용하기
- 사용자의 인증과 초기 데이터 획득만 필요한 경우에는 로그인 기능만 사용하기
config/
폴더app.module.ts
파일의 ConfigModule
에서 설정 객체들을 로드하여 환경 변수들을 가져오도록 설정한다.@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
databaseConfig,
authConfig,
appConfig,
googleConfig,
⁝
],
envFilePath: ['.env'],
}),
⁝
google.config.ts
import { registerAs } from '@nestjs/config';
import { IsOptional, IsString } from 'class-validator';
import validateConfig from '../../utils/validate-config';
import { GoogleConfig } from './google-config.type';
class EnvironmentVariablesValidator {
@IsString()
@IsOptional()
GOOGLE_CLIENT_ID: string;
@IsString()
@IsOptional()
GOOGLE_CLIENT_SECRET: string;
}
export default registerAs<GoogleConfig>('google', () => {
validateConfig(process.env, EnvironmentVariablesValidator);
return {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
};
});
EnvironmentVariablesValidator
클래스에서 class-validator
를 사용해 config type들의 검증 방식을 정의한다.registerAs
함수를 사용해 환경 변수를 검증하고 'google'
이라는 이름의 namespaced 설정 객체를 반환한다.const google = configService.get('google');
// 한경 변수를 담은 객체이므로 다음과 같이 사용 가능
google.clientId
google.clientSecret
configService.get('google.clientId');
configService.get('google.clientSecret');
dto/
폴더types/
폴더NestJS에서 인증/인가를 위한 guard를 작성하는 방법은 크게 두 가지이다.
Custom Guard
class CustomGuard implements CanActivate
와 같이 CanActivate 인터페이스를 구현하여 원하는 로직을 작성한다.Passport Strategy
JWT 토큰을 쿠키에 담아 사용하는 경우, 다음과 같은 패키지를 설치해야 한다.
npm install --save @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
기본적으로 class CustomStrategy extends PassportStrategy(Strategy, 'name')
와 같이 passport-jwt의 Strategy를 확장하여 custom strategy를 구현한다. 여기서 name 부분은 따로 지정하지 않으면 'jwt'
가 기본값이다.
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
Custom Strategy를 사용하여 Guard를 구현한다.
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// 추가 로직이 필요한 경우 여기에 작성
return super.canActivate(context);
}
}
Guard는 Controller에서 @UseGuards(JwtAuthGuard)
로 사용하면 된다.
따로 canActivate 추가 로직이 없다면 @UseGuards(AuthGuard('jwt'))
와 같이 바로 사용해도 된다.
https://github.com/do0ori/login-with-OAuth/tree/main/backend
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy as JwtStrategy } from 'passport-jwt';
import { AllConfigType } from '../../config/config.type';
import { TokenValidationHelper } from '../../helpers/token-validation.helper';
import { AccessTokenPayload } from '../../token/interfaces/access-token-payload.interface';
import { extractJwtFromCookie } from '../../utils/cookie-extractor.util';
@Injectable()
export class AuthStrategy extends PassportStrategy(JwtStrategy, 'auth') {
constructor(
private readonly tokenValidationHelper: TokenValidationHelper,
configService: ConfigService<AllConfigType>,
) {
super({
jwtFromRequest: extractJwtFromCookie('access_token'),
secretOrKey: configService.get('token.accessSecret', { infer: true }),
});
}
// TODO: Replace return type any with User type later
async validate(payload: AccessTokenPayload): Promise<any | never> {
return this.tokenValidationHelper.validatePayload(payload);
}
}
ExtractJwt
객체에는 cookie에서 token을 추출하는 method는 존재하지 않아 내가 따로 extractJwtFromCookie
라는 이름의 util 함수를 구현해 사용했다.import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('auth') {}
'auth'
를 AuthGuard에 넘겨주면 JwtAuthGuard가 AuthStrategy를 실행한다.댕댕워크 프로젝트를 할 때에는 passport에 대해 잘 모르기도 했고 어려워 보여서 사용하지 않았었는데 확실히 token을 추출하고 검증한 후 사용자 정보를
request.user
에 담아 반환하는 등의 인증 로직을 strategy를 통해 분리할 수 있어서 더 코드가 깔끔해진 것 같다. 인가는 그대로 기존 Custom Guard를 작성하는 방식으로 진행하면 될 것 같다.