Search
Duplicate

Authentication

Created
2021/07/02 02:55
Tags

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/passportpassport 패키지는 설치해야한다. 그리고 특정 전략 패키지를 설치해아한다. (예를들어, 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 는 디폴트로 usernamepassword 를 리퀘스트 바디에서 기대한다. 우리는 다른 프로퍼티 네임을 옵션 오브젝트로 던질 수 있다. 예를들면 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 에 인젝트 하는 것을 잊지마세요.
우리는 AuthModuleJwtModule 을 새로운 디펜던시로 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
복사
우리는 JwtModule 에 configuration 오브젝트를 던지는 register() 를 이용하여 구성하였다. 더 많은 정보는 알아서 찾아봐라 here, here
이제 우리는 /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 의 만료시간을 자동으로 확인하여, 어플리케이션에서 수행하는 문제를 덜어주기 때문입니다.
이로서 예제가 끝났습니다. 모든 코드는 여기서 확인하실 수 있습니다.