Authentication
•
Authentication 은 대부분의 어플리케이션의 필수 부분이다.
•
인증을 핸들링하는데는 다양한 다른 접근과 전략이 있다.
•
모든 프로젝트에 대한 인증 방식은 특정 어플리케이션 요구사항에 따라 다르다.
•
이 쳅터는 다양한 다른 요구사항에 대해 적용 시킬 수 있는 몇몇 접근을 제공한다.
•
Passport 는 많은 노드 인증 라이브러리중 가장 많이 사용되는 라이브러리이다.
•
네스트에서는 @nestjs/passport 로 모듈화 시켜 사용 할 수 있다.
•
하이레벨 에서는 passport 는 다음스탭을 실행한다.
•
자격증명(username/password 나, JSON Web Token 등) 을 사용하여 사용자를 인증한다.
•
인증된 상태를 관리한다. (JWT 와 같은 휴대용 토큰을 발급하거나, express 새션을 생성하여)
•
인증된 사용자에 대한 정보를 라우트핸들러에서 사용 할 수 있도록 request 객체에 첨부한다.
•
Passport 는 여러 인증 메카니즘을 implement 한 strategies 라는 풍부한 생태계를 가지고 있다.
•
개념에서는 단순하지만, 선택할수 있는 passport 전략 세트는 크고 다양하다.
•
passport 는 이러한ㅇ다양한 단계를 스탠다드 패턴으로 추상화 한다.
•
그리고 @nestjs/passport 는 이것을 모듈로 감싼다.
•
이 쳅터에서는, 우리는 RESTful API 서버를 위한 완벽한 end-to-end 인증 솔루션을 우리의 파워풀하고 유연한 모듈을 이용하여 만들어낼 것이다.
•
너는 여기서 묘사한 컨셉을 이용하여 너의 커스터마이즈 인증 스키마 전략을 실행할 수 있다.
•
Authentication requirements
•
우리의 요구사항을 구체화 해보자. 이 케이스에서, 클라이언트는 username 과 password 로 인증 받기를 시작할 것이다.
•
한번 인증되면, 서버는 JWT(인증 후속 요청에서 인증 헤더에 포함되어 있는 bearer 토큰) 을 발행할 것이다.
•
우리는 오로지 valid JWT 를 가진 리퀘스트만 접근 할 수 있는 protected 된 route 를 만들 것이다.
•
우리의 첫번째 요구사항 : 유저인증
•
우리는 이것을 JWT 인증으로 해결 할 것이다.
•
우리는 required 패키지를 인스톨 해야한다.
•
passport 는 username/passowrd 인증 메커니즘을 implement 하는 passport-local 이라는 전략을 제공한다.
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
Plain Text
복사
NOTICE : 당신이 어떤 패스포트 전략을 사용하더라도, 항상 @nestjs/passport 와 passport 패키지는 설치해야한다. 그리고 특정 전략 패키지를 설치해아한다. (예를들어, passport-jwt or passport-local) 그리고, 패스포트에 대한 타입이 정의된 패키지를 인스톨해야한다. 위의 예로는 @types/passport-local 이다.
Implementing Passport strategies
•
우리는 인증 기능을 implement 할 준비가 되었다.
•
모든 passport 전략에 사용되는 프로세스에 대한 개요부터 시작하겠다.
•
passport 는 그 자체로 mini framework 라고 생각하는 것이 좋다.
•
이 프레임워크의 개쩌는건 인증절차를 몇몇 당신이 커스터마이즈(당신이 실행하고자하는 전략을 기반으로) 한 몇몇 기본적인 스탭으로 춧아화 한다는 것이다. 뭔 개소리야 진짜
•
이것은 프레임워크와 같다. 왜냐하면, 당신이 이것을 passport 가 적절할때 호출할 커스터마이징된 파라미터(as plain JSON objects) 와 커스텀 코드(콜백 함수) 를 제공함으로서 구성하기 때문이다.
•
우리는 @nestjs/passport 를 사용하여 모듈로서 이 프레임웤을 사용 할 것이지만, 우선은 vanilla Passport 가 어떻게 동작하는지 고려해보자.
•
In vanilla Passport, 당신은 다음 두가지를 제공함으로서 전략을 구성 한다.
•
해당 전략에 대한 특정한 옵션 세트. 예를 들어, JWT 전략에서 당신은 토큰을 sign 하기 위한 시크릿을 제공해야한다.
•
"verify callback" (패스포트가 어떻게 유저 스토어(user account 를 매니지하는 곳)과 상호작용 할지). 여기서, 너는 유저가 존재하는지 아닌지, 그리고 그들의 credentials 가 valid 한지 아닌지를 verify(검증) 해야 한다. Passport 라이브러리는 이 콜백이 유저인증을 성공했을 경우 full user 를, 실패했을 경우 null 을 리턴하기를 예상한다. (실패란 유저 is not found 거나, passport-local 에서의 경우 password 가 맞지 않았을 때 이다.)
•
@nestjs/passport 와 함께, 너는 Passport 전략을 PassportStrategy 클레스를 상속하는 것으로 구성 한다.
•
서브클레스에서 super() 메서드를 호출하여 전략옵션 (위 항목 1)을 전달합니다. (선택적으로 옵션객체를 전달합니다.)
•
당신의 서브클레스에서 validate() 를 implement 하는 것으로 검증 콜백 (위 항목 2) 를 제공합니다.
•
아래와 같이 생성하면서 시작합니다.
$ nest g module auth
$ nest g service auth
Plain Text
복사
•
그리고 users.service.ts 를 아래와 같이 입력합니다.
•
UsersService 는 단순하게 하드코딩된 유저리스트를 메모리에 유지합니다. 그리고 username 으로 그중 하나를 발견하는 메소드를 가집니다.
•
진짜 어플리케이션에서는, db 에 저장해야합니다.
import { Injectable } from '@nestjs/common';
// This should be a real class/interface representing a user entity
export type User = any;
@Injectable()
export class UsersService {
private readonly users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'maria',
password: 'guess',
},
];
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}
Plain Text
복사
•
우리의 AuthService 는 유저를 발견하고 패스워드를 검증하는 책임이 있다.
•
우리는 validateUser() 메소드를 만드는 것으로 이 목적을 달성 할 수 있다.
•
아래코드에서, ES6 스프레드 오퍼레이터를 사용해서 개쩔게 유저 오브젝트에서 각각의 프로퍼티를 분리한다.
•
우리는 잠시후 우리의 passport local strategy 에서 validateUser() 메소드를 호출 할 것이다.
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
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;
}
}
Plain Text
복사
WARNING : 실제 어플리케이션에서는 password 를 plain text 로 저장하면 안된다. 솔트를 사용하는 단방향 해쉬 알고리즘인 bcrypt 를 사용해라. 이 접근에서, 당신은 해쉬된 password 만 저장하면 된다.(솔트도 저장해야함)
Implementing Passport local
•
이제 우리는 local authentication strategy passport를 implement 할 수 있다.
•
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) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Plain Text
복사
•
모든 passport 전략에 대해 앞에 설명한 레시피를 따라야 한다.
•
지금 configuration 옵션이 없는 passport-local 의 경우, constructor 가 단순히 아무 옵션 오브젝트 없이 super() 만 부르면 된다.
HINT : 패스포트 전략의 커스텀 행동을 위해 우리는 super() 에 옵션 오브젝트를 넣을 수 이있다. 예를들어 passport-local strategy 는 디폴트로 username 과 password 를 리퀘스트 바디에서 기대한다. 우리는 다른 프로퍼티 네임을 옵션 오브젝트로 던질 수 있다. 예를들면 super({usernameField: 'email' }) 더 많은 정보는 여기
•
우리는 또한 validate() 메소드를 implement 한다. 각각의 전략에는, 패스포트는 verify function 을 호출할 것이다. (@nestjs/passport 안에 validate() 메소드) strategy-specific 파라미터 세트를 적절하게 사용하여. local-strategy 의 경우 패스포트가 기대하는 validate() 메소드는 validate(username:string, password:string):any 이다.
•
대부분의 유효성 검사는 AuthService 에서 실행됩니다. (UsersService 의 도움을 받아), 이 방법은 아주 간단합니다.
•
모든 passport 전략에 대한 validate() 메서드는 세부사항만 다른 유사한 패턴을 따릅니다.
•
만약 유저가 발견되고 credentials 가 valid 하면, user 가 리턴되고, passport 는 완료되니다. (Request 오브젝트로 부터 user 프로퍼티를 만들어 냅니다.), 그리고 리퀘스트 핸들링 파이프라인은 계속될 수 있습니다.
•
발견되지 않을 경우 exception layer 로 가게 됩니다.
•
일반적으로, 전략별로 다른점은 validate() 함수에서 어떻게 user 의 존재와 valid 를 판단할 것인지 입니다.
•
방금정의한 passport 기능을 사용하려면 AuthModule 을 구성해야 합니다.
•
auth.module.ts 를 아래와 같이 구성합니다.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
Plain Text
복사
Built-in Passport Guards
•
우리는 Guards 를 사용할 것이다.
•
@nestjs/passport 를 사용하는 context 에서는, 우리는 약간 새로운것을 소개합니다.
•
app 이 두가지 states 로 존재할수 있다는 것을 고려해봅시다. (인증관점에서 : )
1.
the user/client is not logged in (is not authenticated)
2.
the user/client is logged int (is authenticated)
•
첫번째 케이스에서 (유저가 로그인되지 않은 케이스에서), 우리는 두가지 다른 기능을 수행해야 합니다.
•
인증되지 않은 사용자가 엑세스할 수 있는 경로를 제한합니다.(예를들어, 제한된 경로에 대한 엑세스 거부)
•
보호되는 라우트에 Guard 를 놓음으로서 이 기능을 합니다.
•
예상했겠지만, 이 Guard 에서, 유효한 JWT 가 있는지도 확인합니다.
•
그래서 이후에 이 가드에서 작업할 것입니다. 만약 JWT 가 한번이라도 발급되면
•
이전에 인증되지 않은 사용자가 로그인을 시도 할 때 인증 단계 자체를 시작합니다.
•
이 스텝에서 유효한 사용자에게 JWT 가 발급됩니다.
•
잠시 생각해보면, 우리는 username/password 를 받기 위해서 POST 요청이 필요하다는 것을 알수있습니다.
•
따라서 이것을 핸들링하기 위해 POST /auth/login 을 만듭니다.
•
우리는 의문이 생깁니다 : 정확히 어떻게 우리는 이 라우트에서 passport-local strategy 를 호출하는 것일까?
•
대답은 간단합니다. 약간 다른 타입의 Guard 를 사용하는 것입니다.
•
@nestjs/passport 모듈은 이 작업을 수행하는 Guard 를 제공합니다.
•
이 Guard 는 passport 전략을 호출하고 위에서 설명한 단계를 시작합니다. (자격 증명 검색, 검증 함수 호출, user 프로퍼티 생성 등)
•
두번째(로그인된 유저) 의 경우 간단한 가드를 사용한다.
Login route
•
전략을 수립했으므로, 이제 bare-bone /auth/login 라우트를 구현하고, 내장 Guard 를 적용해서 passport-local 흐름을 시작 할 수 있습니다.
•
app.controller.ts 파일을 여러 아래와 같이 입력한다.
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
}
Plain Text
복사
•
@UseGuards(AuthGuard('local')) 에서, 우리가 passport-local strategy 를 확장할 때, 우리를 위해 자동으로 제공된 AuthGaurd 를 사용하였다.
•
이것을 차근차근 살펴보자.
•
우리의 passport local strategy 는 기본적으로 'local' 을 가진다.
•
@UseGuards() 데코레이터에서, 해당 이름을 참조하여 passport-local 패키지에서 제공하는 코드와 연결합니다.
•
이것은 우리 앱이 여러 패스포트 전략을 가지고 있을 때 정확히 무엇을 실행해야 하는지 명확하게 합니다.
•
우리는 오직 하나의 전략만을 가지고 있지만, 곧 추가할 예정이므로 명확성을 위해 필요합니다.
•
우리의 라우트를 테스트하기 위해 우리는 /auth/login 라우트가 단순하게 현재 user 를 반환하게 했습니다.
•
이를 통해 우리는 패스포트의 또다른 기능을 시연 할 수 있습니다 : 패스포트는 자동으로 사용자 객체를 만듭니다. (validate()) 메소드에서 리턴하는 벨류를 기본으로, 그리고 그것을 Request 객체에 req.user 오브젝트로 등록합니다.
•
이후에 우리는 이것을 JWT 를 리턴하도록 고칠 것입니다.
JWT functionality
•
우리는 우리의 auth system 의 일부를 JWT 기능으로 옮길 준비가 되었다.
•
요구사항을 검토하고 수정해보겠습니다 :
•
사용자가 username/password 로 인증하도록 하고, 보호된 API endpoint 에 대한 후속 호출에 대해서 사용 할 수 있는 JWT 를 리턴하도록 한다.
•
이렇게 만들기 위해서는, JWT 을 발급하도록 만들어야 한다.
•
유효한 JWT 의 존재를 기반으로 보호되는 API 라우트를 생성
•
우리는 JWT 요구사항을 서포트해줄 새로운 패키지를 설치해야 한다.
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
Plain Text
복사
•
@nestjs/jwt 패키지는 JWT 조작을 도와주는 유틸리티패키지 입니다.
•
POST /auth/login 요청이 어떻게 핸들링 되는지 조금더 디테일하게 봅시다.
•
우리는 AuthGuard 데코레이터를 사용했습니다. 이것의 의미는 다음과 같습니다
•
라우트 핸들러는 정보가 validated 할때만 호출될 것입니다.
•
req 매개변수는 user 프로퍼티를 포함 할 것입니다. (패스포트 인증 흐름에서 채워집니다.)
•
이를 염두에 두고, 우리는 드디어 진짜 JWT 을 만들어 낼수 있다. 그리고 그것을 라우트에 리턴한다.
•
우리의 서비스를 깔끔하게 모듈화 하기 위해, authService 에서 JWT 을 만들어내는 것을 핸들링 할것이다.
•
auth.service.ts 파일을 열고 login() 메서드를 추가해라 (JwtService 를 임포트 해야 한다.)
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),
};
}
}
Plain Text
복사
•
우리는 @nestjs/jwt 라이브러리를 사용했습니다. 이것은 user object properties 의 하위집합으로 부터 JWT 을 생성할 수 있는 sign() 함수를 제공합니다. 우리는 그러곤 그것을 access_token 이라는 프로퍼티를 가진 간단한 오브젝트로 리턴합니다.
•
JWT 표준과 일치하도록 userId 값을 유지하기 위해 sub 의 속성 이름을 선택합니다.
•
JwtService 제공자를 AuthService 에 인젝트 하는 것을 잊지마세요.
•
우리는 AuthModule 에 JwtModule 을 새로운 디펜던시로 import 해야 합니다.
WARNING : key 를 노출 시키지 마라. 실제 서비스에서는 반드시 이 키를 보호해야 한다.
이제 auth.module.ts 를 열어서 아래와 같이 작성한다.
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: 'SECRET!!!!',
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
Plain Text
복사
•
이제 우리는 /auth/login 라우트가 JWT 을 리턴하도록 업데이트 할 수 있다.
•
테스트해보자. 결과는 아래와 같다.
Implementing Passport JWT
•
우리는 우리의 마지막 요구사항에 이르렀다. : 요청에 유효한 JWT 을 요구하여 엔드포인트를 보호합니다.
•
passport 는 여기서도 우리를 도와준다.
•
JSON 웹 토큰으로 RESTful 엔드포인트를 보호하기 위한 passport-jwt 전략을 제공한다.
•
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 };
}
}
Plain Text
복사
•
우리의 JwtStrategy 을 사용하여, 모든 passport 전략에 대해 앞에 설명했던 것과 동일한 레시피를 따랐다.
•
이 전략은 약간의 초기화를 요구한다. 그래서 super() 를 호출 할 옵션 오브젝트를 넣어줬다.
•
옵션에 대해서는 here
•
jwtFromRequest : Request 에서 JWT 를 추출하는 방법을 제공합니다. API request 의 Authorization 헤더에 제공된 bearer token 에 저근하는 표준 방식을 사용합니다.
•
ignoreExpiraion : 명시적으로, 우리는 기본적 false 로 세팅(JWT 이 만료되었는지를 판단하는 책임을 passport module 에 위임하는 것)을 선택했다. 이것은 만료된 JWT 일 경우 passport 가 자동으로 401 Unauthorized 를 뱉는 것을 의미합니다.
•
secretOrKey : 우리가 사인하는데 사용하는 키
•
validate() 메소드는 토론할 가치가 있다.
•
passport 우선 JWT 의 서명을 확인하고, JSON 으로 디코딩 합니다.
•
그것은 그리고 우리의 validate() 메소드를 디코딩된 JSON 을 단일매개변수로 전달하여 호출합니다.
•
JWT 서명 방식을 기반으로 하여, 우리는 이전에 서명하여 유효한 사용자에게 발급한 유효한 토큰을 받고 있음을 보장합니다.
•
모든것에 대한 결과로, validate() 콜백에 대한 우리의 응답은 간단합니다. : 우리는 userId 와 username 프로퍼티를 포함하는 하나의 간단한 로브젝트를 리턴합니다.
•
passport 는 validate() 메서드의 반환값을 기반으로 사용자 개체를 빌드한다는 사실을 잊지마십시오!
•
그리고 그것을 Request 객체에 포함시킵니다.
•
또한 이 접근방식은 다른 비지니스 로직을 프로세스에 삽입할수 있는 여지를 남긴다는 점을 지적할 가치가 있습니다.
•
예를들어, validate() 메서드에서 데이터베이스 검색을 수행하여, 사용자에 대한 더 많은 정보를 추출하여 request 에 더 풍부한 사용자 객체를 추가 해 줄수 있습니다.
•
이곳은 또한 우리가 '취소된 토큰을 사용하는 userId 를 찾아내는 것' 과 같은 추가적인 토큰 검증을 할 수 있는 곳입니다.
•
우리가 여기서 구현한 모델은 빠릅니다. 하지만 작은 정보를 가지고 있죠
•
AuthModule 에 새로운 JwtStrategy 를 프로바이더로서 더합니다.
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' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
Plain Text
복사
•
마지막으로 우리는 AuthGuard 를 상속받아서 JwtAuthGuard 를 정의 합니다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Plain Text
복사
Implement protected route and JWT strategy guards
•
Open the app.controller.ts file and update it as shown below :
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;
}
}
Plain Text
복사
•
다시 한번, passport-jwt 모듈 구성 할 때, @nestjs/passport 모듈이 자동으로 제공한 AuthGuard 를 적용하고 있습니다.
•
이 jwt 라는 Guard 는 그것의 기본 이름으로 참조됩니다.
•
우리의 GET /profile 라우트가 히트될때, JWT 을 검증하고, user 프로퍼티를 Request 오브젝트에 할당하기 위해 Guard 가 우리가 만든 passport-jwt 로직을 호출합니다.
•
AuthModule 에서, 우리는 JWT 만료기한을 60s 로 설정하였다.
•
이것은 아마도 너무 짧다. 그리고 토큰 만료와 리프래쉬는 이 문서의 범위를 벗어났다.
•
60초가 지난 후 다시 protected 된 API 에 요청하게 되면, 그것은 401 Unauthorized 를 응답할 것이다.
•
이것은 passport 가 JWT 의 만료시간을 자동으로 확인하여, 어플리케이션에서 수행하는 문제를 덜어주기 때문입니다.
•