Request
객체에 첨부@nestjs/passport
모듈로 Nest와 함께 사용 가능Passport 설정을 할 때 다음의 두 가지가 필요하다.
// passport-local
declare class Strategy extends PassportStrategy {
constructor(options: IStrategyOptions, verify: VerifyFunction);
}
// passport-jwt
declare class Strategy extends PassportStrategy {
constructor(opt: StrategyOptions, verify: VerifyCallback);
}
local strategy
는 가장 기본적인 email
과 password
를 가지고 인증하는것이다. (로그인할 때 사용됨)nest
에서도 @nest/passport
와 passport-local
을 사용하여 local strategy
를 구현할 수 있다.
들어가기전 일단 local strategy가 동작하는 흐름을 보자.
id
, password
로 로그인 요청AuthGuard
를 통해 사용자 인증AuthGuard
가 호출되면 LocalStrategy
의 validate
호출 AuthGuard
의 canActivate
가 true
리턴$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
$ npm install bcrypt
$ npm install @types/bcrypt -D
local.strategy.ts
구현auth
에 local.strategy.ts
구현import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
// 내부에 필드 작성하는거 잊지말기
// 문서 예시에는 안써있는데 안써주면 validate가 호출되지 않습니다.
super({ usernameField: 'email', passwordField: 'password' });
}
async validate(email: string, password: string): Promise<any> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
auth
의 service
에 위에서 사용할 validateUser
구현import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../user/entities/user.entity';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User) private usersRepository: Repository<User>,
) {}
async validateUser(email: string, pass: string): Promise<any> {
const user = await this.usersRepository.findOne({
where: { email },
});
const password = await bcrypt.compare(pass, user.password);
if (password) {
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
}
return null;
}
}
auth.module.ts
에 PassportModule
, localStrategy
추가import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../user/entities/user.entity';
@Module({
imports: [UserModule, PassportModule, TypeOrmModule.forFeature([User])],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
@nestjs/passport
안에 AuthGuard
라는 내장 가드가 있다.
이 가드는 Passport strategry를 호출하고, 인증에 필요한 과정들을 시작하도록 한다.
app.controller
에 가서 /auth/login
라우트를 만든다.@UseGuards(AuthGuard('local'))
라고 작성한다.//app.controller.ts
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local')) // 2번
@Post('auth/login') // 1번
async login(@Request() req) {
return req.user;
}
}
이렇게 해주기만 하면 passport가 알아서 다 해준다. (validate() 메서드로 user object를 생성, Request object에 user 할당 등...)
잘 하는지 보자
curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
잘 나온다
{"userId":1,"username":"john"}
그리고 직접 넣는 대신에 클래스를 만들어 import할 수도 있다.
// auth/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
그럼 이렇게 바꿔야한다.
//app.controller.ts
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
// import { AuthGuard } from '@nestjs/passport'; 아까 한 거
import { LocalAuthGuard } from './auth/local-auth.guard';
@Controller()
export class AppController {
// @UseGuards(AuthGuard('local')) 아까 한 거
@UseGuards(LocalAuthGuard)
@Post('auth/login') // 1번
async login(@Request() req) {
return req.user;
}
}
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
auth
의 service
에 위에서 사용할 login
구현import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
// auth/constants.ts
export const jwtConstants = {
secret: 'secretKey',
};
auth.module.ts
에 JwtModule
추가import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
Guard를 사용하여 JWT를 발행해보자
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}
잘 나온다
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
$ # Note: above JWT truncated
// auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
jwtFromRequest
: Request
에서 jwt를 추출하는 방법ignoreExpiration
: JWT 만료시간 체크401 Unauthorized
response로 보냄secretOrKey
: token에 발급에 사용될 secret key async validate(payload: any) {
try {
const user = await UserModel.findOne(payload.sub)
if (lodash.isNil(user) { throw new Error(Unauthorized, ~~~~) }
return { user };
catch (err) {
throw new Error
}
}
stateless와 stateful
stateful : 기존 방식으로 로그인시 서버에서 유저의 session을 저장하는 형태
stateless : session 없이 인증 하는 방식으로 유저와 서버가 서로를 인식하는 수단을 공유하는 형태, 대표적으로 사용하는 JWT
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' }, // expiration : 60초
}),
],
providers: [AuthService, LocalStrategy, **JwtStrategy**],
exports: [AuthService, **JwtModule**],
})
export class AuthModule {}
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// @nestjs/passport 모듈이 자동으로 프로비저닝한 모듈 사용
import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
@UseGuards(JwtAuthGuard)
를 데코레이팅 한 곳에서는 req.user
객체를 통해 유저 정보를 가져올 수 있게 된다.import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const can = await super.canActivate(context);
if (can) {
const request = context.switchToHttp().getRequest();
console.log('login for cookie');
await super.logIn(request);
}
return true;
}
}
// AuthGuard의 모양새
import { CanActivate } from '@nestjs/common'; // CanActivate 메소드를 가진 Interface
import { Type } from './interfaces';
import { IAuthModuleOptions } from './interfaces/auth-module.options';
export declare type IAuthGuard = CanActivate & {
logIn<TRequest extends {
logIn: Function;
} = any>(request: TRequest): Promise<void>;
handleRequest<TUser = any>(err: any, user: any, info: any, context: any, status?: any): TUser;
getAuthenticateOptions(context: any): IAuthModuleOptions | undefined;
};
export declare const AuthGuard: (type?: string | string[]) => Type<IAuthGuard>;
canActivate
함수를 구현해주면 된다!AuthGuard
클래스를 사용하는 것으로 충분import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
export class JwtAuthGuard extends AuthGuard(['strategy_jwt_1', 'strategy_jwt_2', '...']) { ... }
모든 엔드포인트를 기적으로 보호해야 하는 경우 각 컨트롤러 상단에서 @UseGuarlds()
데코레이터를 사용하는 대신 전역 가드로 인증 가드를 등록할 수 있다.
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
SetMetadata
데코레이터 팩토리 함수를 사용하여 커스텀 데코레이터를 만들 수 있음import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@Public()
@Get()
findAll() {
return [];
}
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
// reflector 으로 IS_PUBLIC_KEY 의 메타데이터 값을 가져옴
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
Passport는 Default-Scope 즉, 글로벌 인스턴스에 전략을 등록하는 것을 기반으로 한다.
따라서, 요청 종속 옵션을 갖거나 요청마다 동적으로 인스턴스화하지 않는다.
공식문서에서 예로써 등장하는 AuthService
는 글로벌 Default- Scope이다.
🤔만약 해당 프로바이더가 Request-Scope로 설정되어있다면, 이를 어떻게 참조할 수 있을까?
🖐모듈 참조 기능을 활용하면 된다.
먼저 local.strategy.ts
파일의 생성자에 다음과 같이 ModuleRef
를 참조하고, passReqToCallback
속성을 true
로 설정한다.
constructor(private moduleRef: ModuleRef) {
super({
passReqToCallback: true,
});
}
그 다음 validate()
메서드를 다음과 같이 정의한다.
async validate(
request: Request,
username: string,
password: string,
) {
const contextId = ContextIdFactory.getByRequest(request);
// "AuthService" is a request-scoped provider
const authService = await this.moduleRef.resolve(AuthService, contextId);
...
}
일반적으로 모듈 참조 기능을 이용할 때, 범위가 지정된 프로바이더를 참조했던 것처럼 하면 된다.
요청 객체를 이용해 컨텍스트 ID를 생성하고, 이를 이용해 resolve()
를 호출하면 된다.
위 예에서 resolve()
메서드는 AuthService
프로바이더의 요청 범위 인스턴스를 비동기적으로 반환한다.
register()
메서드를 이용해 Passport를 커스텀하게 사용할 수 있다.
PassportModule.register({ session: true });
또한, 전략을 구성하기 위해 생성자에 옵션 객체를 전달할 수 있다.
아래는 local strategy의 예이다.
constructor(private authService: AuthService) {
super({
usernameField: 'email',
passwordField: 'password',
});
}
기본적으로 local strategy는 username과 password 필드로 검사하도록 설정되어 있다.
email과 password 필드로 검사하고자 한다면, 위와 같이 해주면 된다.
전략의 이름을 지정해 줄수도 있다.
전략을 구현할 때 PassportStrategy
함수의 두번째 인수에 지정하고픈 이름을 전달하면 된다.
만약 따로 지정해주지 않는다면, 기본 이름이 지정된다.(ex: jwt-strategy -> jwt
)
export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')
그런 다음 @UseGuards(AuthGuard('myjwt'))
와 같이 참조하면 된다.
잘 읽었씁니다 !