728x90

AWS Client VPN은

  • AWS 리소스 및 온프레미스 네트워크의 리소스에 안전하게 액세스 할 수 있도록 하는 관리형 클라이언트 기반 VPN 서비스이다.
  • Client VPN에서는 OpenVPN 기반 VPN 클라이언트를 사용하여 어떤 위치에서든 리소스에 액세스 할 수 있다.

AWS Client VPN 구성도


Client VPN 시작하기

1. 서버와 클라이언트 인증서 및 키 생성

Client VPN이 인증서를 사용하여 클라이언트와 서버 간의 인증을 수행한다. 서버 인증서를 AWS Certificate Manager(ACM)에 업로드하고 Client VPN 엔트포인트를 생성할 때 지정하기 위해 인증서 및 키를 생성한다.

 

① EasyRSA 다운로드 (https://github.com/OpenVPN/easy-rsa/releases)에서 사용 중인 OS 버전에 해당하는 ZIP 파일을 다운로드한 후 압축을 푼다.

② 명령 프롬프트를 열고 EasyRSA-3.x 폴더가 추출된 위치로 이동한다.

③ 다음 명령으로 EasyRSA3 셸을 연다.

PS D:\EasyRSA-3.0.8> .\EasyRSA-Start.bat

Welcome to the EasyRSA 3 Shell for Windows.
Easy-RSA 3 is available under a GNU GPLv2 license.

Invoke './easyrsa' to call the program. Without commands, help is displayed.

EasyRSA Shell
#
 

④ 새 PKI 환경을 시작한다.

 
# ./easyrsa init-pki
path = D:/EasyRSA-3.0.8/pki/easy-rsa-7056.a47484/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmpED7.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmpED7.tmp
fd = 3

init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: D:/EasyRSA-3.0.8/pki

 

⑤ 아래 명령을 실행하고 Common Name을 지정한다.

 
# ./easyrsa build-ca nopass
Using SSL: openssl OpenSSL 1.1.0j  20 Nov 2018
path = D:/EasyRSA-3.0.8/pki/easy-rsa-17364.a27448/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp8814.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp8814.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-17364.a27448/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp8A85.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp8A85.tmp
fd = 3
Generating RSA private key, 2048 bit long modulus
........+++++
...........+++++
e is 65537 (0x010001)
path = D:/EasyRSA-3.0.8/pki/easy-rsa-17364.a27448/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp8DD1.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp8DD1.tmp
fd = 3
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:bcheck-vpn

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
D:/EasyRSA-3.0.8/pki/ca.crt

 

⑥ 서버 인증서 및 키를 생성한다.

 
# ./easyrsa build-server-full server nopass
Using SSL: openssl OpenSSL 1.1.0j  20 Nov 2018
path = D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp6B85.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp6B85.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp6E44.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp6E44.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp70E4.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp70E4.tmp
fd = 3
Generating a RSA private key
..................................................+++++
....+++++
writing new private key to 'D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.a37600'
-----
path = D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp89BB.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp89BB.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp991D.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp991D.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmpA0BE.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmpA0BE.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmpA3FA.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmpA3FA.tmp
fd = 3
Using configuration from D:/EasyRSA-3.0.8/pki/easy-rsa-27984.a18696/tmp.a18692
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'server'
Certificate is to be certified until Aug  1 01:07:23 2024 GMT (825 days)

Write out database with 1 new entries
Data Base Updated

 

⑦ 클라이언트 인증서 및 키를 생성한다.

 
# ./easyrsa build-client-full client1.domain.tld nopass
Using SSL: openssl OpenSSL 1.1.0j  20 Nov 2018
path = D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp15E9.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp15E9.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp18A8.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp18A8.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp1B39.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp1B39.tmp
fd = 3
Generating a RSA private key
.........................+++++
....................................................................+++++
writing new private key to 'D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.a14580'
-----
path = D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp3141.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp3141.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp3AA7.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp3AA7.tmp
fd = 3
path = D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.XXXXXX
lpPathBuffer = C:\Users\KIMSOR~1\AppData\Local\Temp\
szTempName = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp3D38.tmp
path = C:\Users\KIMSOR~1\AppData\Local\Temp\tmp3D38.tmp
fd = 3
Using configuration from D:/EasyRSA-3.0.8/pki/easy-rsa-30692.a37232/tmp.a20456
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'client1.domain.tld'
Certificate is to be certified until Aug  1 01:09:08 2024 GMT (825 days)

Write out database with 1 new entries
Data Base Updated

 

⑧ EasyRSA 셸을 종료한다.

 
# exit

 

⑨ 인증서 및 키 파일 정리

사용할 파일 목록은 아래와 같다.  편의를 위에 C:\bcheck-vpn 폴더를 생성하여 이동시킨다.

pki\ca.crt
pki\issued\server.crt
pki\private\server.key
pki\issued\client1.domain.tld.crt
pki\private\client1.domain.tld.key

 

 
PS D:\EasyRSA-3.0.8> mkdir C:\bcheck-vpn

    Directory: C:\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----        2022-04-29 오전 10:21                bcheck-vpn

PS D:\EasyRSA-3.0.8> copy pki\ca.crt C:\bcheck-vpn
PS D:\EasyRSA-3.0.8> copy pki\issued\server.crt C:\bcheck-vpn
PS D:\EasyRSA-3.0.8> copy pki\private\server.key C:\bcheck-vpn
PS D:\EasyRSA-3.0.8> copy pki\issued\client1.domain.tld.crt C:\bcheck-vpn
PS D:\EasyRSA-3.0.8> copy pki\private\client1.domain.tld.key C:\bcheck-vpn
PS D:\EasyRSA-3.0.8> cd C:\bcheck-vpn
PS C:\bcheck-vpn>

 

⑩ 서버와 클라이언트 인증서 및 키를 ACM에 업로드한다. Client VPN 엔드포인트를 생성하려는 리전과 동일한 리전에 업로드해야 한다.

 
PS C:\bcheck-vpn> aws acm import-certificate --certificate fileb://server.crt --private-key fileb://server.key --certificate-chain fileb://ca.crt
{
    "CertificateArn": "arn:aws:acm:ap-northeast-2:160270626841:certificate/aeef8187-ddf6-4f2d-b265-86883eb18409"
}

PS C:\bcheck-vpn> aws acm import-certificate --certificate fileb://client1.domain.tld.crt --private-key fileb://client1.domain.tld.key --certificate-chain fileb://ca.crt
{
    "CertificateArn": "arn:aws:acm:ap-northeast-2:160270626841:certificate/e5d819da-734b-4327-9e9b-1b3512486f02"
}

 

⑪ Amazon ACM 콘솔에서 [인증서 나열]에서 생성한 서버와 클라이언트 인증서를 확인한다.

 

728x90
반응형
728x90

로그인 후에 Access Token을 발급하여 쿠키에 저장하는 것까지 구현해 보았습니다.

https://sound-story.tistory.com/19

 

[NestJS] Auth Token을 쿠키에 저장하기

email, password를 통해 로그인하여 access token을 생성하고, 생성된 access token으로 사용자 정보를 조회해 보는 것을 해보았어요. 이어서 token을 쿠키에 저장하는 로직을 구현해 보려고 합니다. 1. cookie-p

sound-story.tistory.com

오늘은 Refresh Token을 구현하기에 앞서 인증의 개념에 대해서 정리하고 시작해 보려고 합니다.

먼저 저희가 acces token발급 시 사용했던 JWT에 관한 얘기입니다.

로그인하면 제일 많이 들어본 거 같아요. 많이 쓰이기 때문에 많이 들어봤겠죠.

 

JWT란?

JSON Web Token의 약어로, 마이크로 서비스의 인증, 인가에 사용할 수 있는 JSON입니다.

서버와 클라이언트 간 정보를 주고받을 때 HTTP Request Header에 JSON토큰을 넣은 후 서버는 별도의 인증과정 없이 헤더에 포함되어 있는 JWT 정보를 통해 인증하게 됩니다.

이렇게 사용되는 JSON 데이터는 URL로 이용할 수 있는 문자 만으로 구성되게 됩니다.

 

JWT는 header, payload, signature로 구성되어 있고, 각 요소는  '.'으로 구분되어 있습니다.

jwt 구성 요소 ( jwt 구조)

실제로 저희가 구현해서 발급했던 토큰을 살펴보니 3개의 구성이 눈에 보입니다.

jwt token

이 토큰을 가지고 공식 홈페이지에서 Decoded를 해보았습니다.

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

 

Encoded에 토큰을 입력하니 Decoded 되어 Header, Payload, Verify Signature 정보가 보입니다.

1. Header : 어떤 알고리즘이 사용되었는지에 대한 "alg"와 JWT의 유형 정보를 가진 "typ"이 있습니다.

  • "alg" : 토큰을 암호화하는 알고리즘 정보입니다. 저는 "HS256"으로 토큰을 암호화했다는 뜻입니다. 알고리즘은 "HS256"외에도 "HS512"(HMAC), "SHA256", "RSA" 등이 있습니다.
  • "typ" : 토큰의 유형 정보입니다. 저는 JWT를 사용했습니다.

 

2. Payload : 클레임(claim) 정보가 저장됩니다. 클레임은 정보 '조각'이라는 의미로, 토큰 또는 사용자에 대한 property를 <key : value>의 형태로 저장할 수 있습니다. 저는 사용자의 id와 이름, 토큰 발급 시간, 토큰 만료 시간을 payload로 사용했는데요. jwt.io에서 보면 registered, public, private claim 3가지가 있습니다.

  • Registered claims : 필수 사항은 아니지만 권장되는 claim입니다. iss(발급자), expiration(유효기간), subject(제목), aud(청취자) 등 이 있습니다. 자세한 정보는 https://datatracker.ietf.org/doc/html/rfc7519#section-4.1에서 확인할 수 있습니다.
  • Public claims : 원하는 대로 정의할 수 있는 claim으로 충돌을 피하려면 IANA JSON Web 토큰 레지스트리에서 정의하거나 충돌 방지 네임스페이스를 포함하는 URI로 정의해야 합니다.
  • Private claims : 사용에 동의한 당사자 간에 정보를 공유하기 위해 작성된 custom claim입니다.

3. Signature : 암호화가 되어 있기 때문에 구조를 보여줍니다. 암호화된 헤더, 암호화된 페이로드, 시크릿(사용자가 정한 시크릿 키), 알고리즘으로 되어 있어요. 저희는 HMAC SHA256 알고리즘을 사용하였네요.

jwt 구성 요소 ( jwt 구조)

JWT 인증과정

그렇다면 발급된 token은 어떻게 쓰일까요?

1. 애플리케이션, 사용자가 인증서버에 인증을 요청합니다. 

2. 인증에 성공하면 인증서버는 액세스 토큰을 반환합니다.

3. 애플리케이션 또는 사용자는 발급된 액세스 토큰으로 API 서버에 접속할 수 있게 됩니다.

jwt process / jwt 프로세스 (https://jwt.io/introduction)

 

JWT 왜 쓸까요?

그렇다면 어떠한 이유에서 JWT를 사용할까요.

JWT의 장단점에 대해 생각해보겠습니다.

 

JSON은 XML보다 장황하지 않기 때문에 암호화 시 사이즈가 작아져 JWT가 SAML보다 콤팩트합니다. 때문에 HTML 및 HTTP 환경에서 JWT를 사용하는 것이 효율적입니다.

보안상 SWT는 HMAC 알고리즘을 사용하여 공유 비밀에 의해서만 대칭적으로 서명할 수 있습니다. 단, JWT 토큰과 SAML 토큰은 X.509 증명서 형식으로 공개 키와 개인 키 쌍을 서명에 사용할 수 있습니다. 불분명한 보안 취약점을 도입하지 않고 XML 디지털 서명을 사용하여 XML에 서명하는 것은 JSON의 간단한 서명에 비해 매우 어렵습니다.

JSON 파서는 오브젝트에 직접 매핑되기 때문에 대부분의 프로그래밍 언어에서 일반적입니다. 반대로 XML에는 문서와 객체의 자연스러운 매핑이 없습니다. 이를 통해 SAML 어설션보다 JWT를 사용하는 것이 쉬워집니다.

이용에 관해서는 JWT를 인터넷 규모로 사용하고 있습니다. 이를 통해 여러 플랫폼(특히 모바일)에서 클라이언트 측 JSON Web 토큰을 쉽게 처리할 수 있습니다.

 

하지만, JWT에도 한계는 있습니다. 개발자의 학습영역? 범위가 끝이 없는 이유는 100% 완벽하다고 끝내기가 없어서이지 않을까 싶습니다.

구현하고 decode를 하면서 든 의문입니다.

저희가 payload에 저장하였던 email과 이름은 민감정보가 아닐까요? 이를 오픈해도 될까요?

token을 발급하고 만약 payload에 저장된 정보가 변경되었다면, payload에서 decode 한 정보와 현재 정보는 일치하지 않네요?

JWT는 토큰의 상태를 저장하지 않습니다. Stateless 이 특성 때문에 한번 만들어진 토큰을 제어할 수 없다는 단점이 있어요. 임의로 삭제할 수 없으니 만료시간이 중요한데, 이를 길게 하면 데이터나 보안이 취약해지고, 짧게 하자니 로그인을 자주 해야 하는 문제가 발생합니다. 이에 대한 대안으로 refresh token, sliding session이 많이 사용되고 있는데요.

 

JWT의 대안

1. Refresh token

JWT를 발급할 때 Access Token과 함께 Refresh Token을 함께 발급하여 Access Token의 짧은 만료시간의 문제를 해결하는 방법입니다. Access Token은 1분, 3분의 짧은 시간을 부여하고 Refresh Token에는 일주일, 2주의 비교적 긴 만료시간을 부여하여 Access Token을 보장해주는 방법입니다. Access Token이 만료되면 Refresh Token으로 서버에게 새로운 Access Token을 발급하도록 합니다.

 

2. Sliding Session

sliding session은 서비스를 사용 중인 유저에 대해서 만료시간을 연장시켜 주는 방법입니다.

쇼핑몰에 접속하여 계속 쇼핑을 하고 있으면 만료시간을 연장하여 쇼핑, 결제 등의 서비스를 지속적으로 이용 가능하도록 하는 방법입니다. 

 

앞으로는 refresh token에 대해서 구현해 보도록 하겠습니다.

 

728x90
반응형
728x90

front인 angular에서 nest를 호출하려고 하니 4200번, 3000번 간의 CORS 정책 문제가 발생하였어요.

이를 해결하기 위해 nest를 수정해보도록 합니다.

먼저 패키지 설치를 해줍니다.

$ npm install --save cors

 

app을 시작하는 부분 저는 'main.ts'에요. 이 소스를 추가해줍니다.

app.enableCors();

전체적인 소스는 아래와 같아요.

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule } from '@nestjs/swagger';
import { SwaggerConfig } from './shared/conf/swagger.config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  const swaggerUiEndpoint = '/swagger';
  const document = SwaggerModule.createDocument(app, SwaggerConfig);
  SwaggerModule.setup(swaggerUiEndpoint, app, document);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

소스코드 한 줄이라 아주 간단하네요.

 

프런트에서 호출한 결과 발생했던 에러 대신 access_token이 정상적으로 넘어옵니다.

 

728x90
반응형

'NestJS' 카테고리의 다른 글

[NestJS] Auth Token을 쿠키에 저장하기  (1) 2022.03.25
[NestJS] swagger에서 테스트하기  (0) 2022.03.23
[NestJS] Authentication 구현  (0) 2022.03.18
[NestJS] CRUD 구현해보기  (0) 2022.03.07
[NestJS] app.controller 살펴보기  (0) 2022.03.02
728x90

email, password를 통해 로그인하여 access token을 생성하고, 생성된 access token으로 사용자 정보를 조회해 보는 것을 해보았어요. 이어서 token을 쿠키에 저장하는 로직을 구현해 보려고 합니다.

 

1. cookie-parser, @types/cookie-parser 설치

먼저 관련 패키지를 다운로드하여 설치합니다.

$ npm install --save cookie-parser
$ npm install --save-dev @types/cookie-parser

 

2. /login 로직 수정

'app.controller.ts'에서 /login 부분을 살펴보면 authSevice의 login을 이용하여 생성된 access_token을 바로 return 해주고 있는데요. 여기서 return 하는 부분을 수정하여 쿠키에 저장하도록 합니다.

async login(
    @Body() user: UserDto,
    @Res({ passthrough: true }) res: Response,
  ) {
    const access_token = await this.authService.login(user);
    res.cookie('Authentication', access_token, {
      domain: 'localhost',
      path: '/',
      httpOnly: true,
    });
    return access_token;
  }

access_token에 생성한 토큰을 저장하고, 이를 쿠키에 저장하도록 수정하였습니다. 전, 후 비교는 아래와 같습니다.

 

3. cookie parser 설정

swagger 사용할 때도 main에서 setup 했던 거 기억나시죠? cookie parser를 사용하기 위해서도 main.ts에서 설정을 해줍니다.

기존에 swagger 설정 했던 부분과 포트 3000 listen 사이에 아래의 코드를 추가해 줍니다.

// cookieyParser
app.use(cookieParser());

 

4. jwt에서 쿠키 사용 설정

그리고 저장된 쿠키를 이용하여 토큰을 확인할 수 있도록 jwtStrategy를 수정합니다.

'jwt.strategy.ts'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        (request) => {
          return request?.cookies?.Authorization;
        },
      ]),
      // Request에서 JWT를 추출하는 방법 중 Header에 Bearer Token 사용
      // ExtractJwt.fromAuthHeaderAsBearerToken()
      ignoreExpiration: false, // jwt 보증을 passport 모듈에 위임함. 만료된 JWT인경우 request거부, 401 response
      secretOrKey: jwtConstants.secret, // token 발급에 사용할 시크릿 키
    });
  }

 

5. Postman 테스트하기

기존의 /auth/login을 실행한 결과 Cookies에 (1)이 생겼어요.

확인해 보니 Authentication이 제대로 저장되어 있네요.  

 

728x90
반응형

'NestJS' 카테고리의 다른 글

[NestJS] Angular에서 Nest 호출, CORS 해결  (0) 2022.03.25
[NestJS] swagger에서 테스트하기  (0) 2022.03.23
[NestJS] Authentication 구현  (0) 2022.03.18
[NestJS] CRUD 구현해보기  (0) 2022.03.07
[NestJS] app.controller 살펴보기  (0) 2022.03.02
728x90

지난번 개발한 부분까지 Postman을 통하여 테스트해보았어요. 이번에는 backend api 테스트 환경에서 많이 쓰이는 swagger를 통해 테스트를 해보도록 합니다.

먼저, 프로젝트에 swagger 설정하는 것 부터 시작해 보겠습니다.

 

1. swagger 설치

$ npm install --save @nestjs/swagger swagger-ui-express

npm을 이용하여 swagger를 설치 합니다.

 

2. swagger 설정

main.ts에서 SwaggerModule을 이용하여 설정해줍니다.

소스코드는 아래와 같은데, SwaggerConfig를 이용하여 SwaggerModule의 도큐먼트를 생성하고 생성한 도큐먼트를 가지고 SwaggerModule을 설정하는 코드입니다.

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule } from '@nestjs/swagger';
import { SwaggerConfig } from './shared/conf/swagger.config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  const swaggerUiEndpoint = '/swagger';
  const document = SwaggerModule.createDocument(app, SwaggerConfig);
  SwaggerModule.setup(swaggerUiEndpoint, app, document);

  await app.listen(3000);
}
bootstrap();

swaggerUiEndpoint를 '/swagger'로 하였기에 swagger의 주소는 'http://localhost:3000/swagger/'가 된다.

설정은 너무도 간단하죠? 그럼 SwaggerConfig부분을 살펴보도록 하겠습니다.

Config를 알아야 원하는 설정을 하여 사용할 수 있으니까요!

 

3. swagger  config

'swagger.config.ts'파일을 열어보면 코드가 가시적으로 잘 되어 있는데요. 저는 jwt의 bearer를 사용하기 위해

.addBearerAuth 부분을 수정했습니다.

import { DocumentBuilder } from '@nestjs/swagger';

export const SwaggerConfig = new DocumentBuilder()
  .setTitle('bcheck back auth')
  .setDescription('bcheck backend auth microservice')
  .setVersion('3.0.0')
  // .setHost('localhost:3000')
  //.setBasePath('/')
  // .setSchemes('http')
  .setExternalDoc('For more information', 'http://swagger.io')
  .addTag('Application', 'bcheck-back-auth')
  .addBearerAuth(
    {
      type: 'http',
      scheme: 'bearer',
      name: 'JWT',
      in: 'header',
    },
    'access_token',
  )
  .build();

그리고 swagger에 api들이 보이도록 설정해 보겠습니다.

 

4. api 설정

'app.controller.ts'를 살펴보면 @ApiOperation, @ApiCreatedResponse, @ApiResponse, @ApiBody가 보입니다.

@ApiOperation({
    summary: '사용자 login',
    description: '추후 cognito 연결, usersService에 등록된 사용자 login',
  })
  @ApiCreatedResponse({
    schema: {
      example: {
        access_token:
          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyRW1haWwiOiJzb3J5NTMzOUBnbWFpbC5jb20iLCJzdWIiOjEsImlhdCI6MTY0NzgyMjQxMCwiZXhwIjoxNjQ3ODIyNDcwfQ.NpWT0a2VxTLPqbl3ZHwqlNYh4Rwwhv0p0WWgUpaY3CE',
      },
    },
  })
  @ApiResponse({ status: 401, description: 'Unauthorized' })
  @ApiBody({ type: UserDto })
  @UseGuards(LocalAuthGuard)
  @Post('/login')
  async login(@Body() user: UserDto) {
    // return req.user;
    return this.authService.login(user);
  }

@ApiOperation : api에 대한 설명

@ApiCreatedResponse : request 성공했을 때 반환될 response

@ApiResponse : request에 대한 repsonse의 직접 설정

@ApiBody : body로 넘겨줄 형태 지정

각각의 설정을 위와 같이 한 경우 아래의 화면처럼 실행됩니다.

 

 

userEmail, password를 입력하여 access_token이 반환되는 /auth/login api에 대한 swagger가 완성되었습니다.

 

5. Try it out

실제로 테스트를 해보겠습니다. 'Try it out'버튼을 클릭하면 'Request body'부분이 확장됩니다.

해당 부분에 body로 넘겨줄 데이터를 json형태로 입력하고 'Execute'하면 아래에 Responses가 보이게 됩니다.

request에 대한 response가 정상적으로 이루어지는 것을 확인할 수 있어요.

앞으로 만들 api에 대해서도 @데코레이터 잘 달아서 swagger로 테스트할 수 있도록 구현해보겠습니다.

728x90
반응형
728x90

MSA(Micro  Software Architecture) 구현 중 사용자 로그인과 관련하여 Authentication 부분을 구현해봅니다.

기존에 nest 개발하기 포스트에 이어서 작업한다고 생각하면 되겠습니다.

https://sound-story.tistory.com/3

 

[NestJS] 개발하기

1. Installation Nest CLI를 통해 쉽게 프로젝트 생성할 수 있다. npm i -g @nestjs/cli npm을 통해 @nestjs/cli를 설치한다. -g 옵션을 통해 글로벌 환경에 설치하였다. 글로벌 환경의 설치 경로를 확인할 수 있..

sound-story.tistory.com

 

먼저 인증과 관련된 모듈 설치를 합니다.

$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
$ npm install --save @nestjs/jwt passport-jwt

다음으로 사용할 service와 module을 생성합니다.

인증 관련 서비스를 구현할 auth와 사용자 정보를 가지고 있을 users입니다.

저희는 users와 관련된 부분은 별도의 MSA구조로 가져갈 것이기에 users에는 email, password 정도의 간단한 정보만 가지고 있을 예정입니다.

$ nest g module auth
$ nest g service auth
$ nest g module users
$ nest g service users

위와 같이 generate 하면 아래와 같은 소스 구조를 갖게 됩니다.

├── src
│   ├── auth
│   │   ├── auth.module.ts
│   │   ├── auth.service.ts
│   ├── users
│   │   ├── users.module.ts
│   │   ├── users.service.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts

 

users.service.ts를 구현해 보도록 하겠습니다.

추후 AWS Cognito를 연결할 예정이기 때문에 사용자 정보를 임시로 저장하여 구현해 보겠습니다.

사용자 정보는 readonly로 저장이 되어 있고, email 정보로 사용자 정보를 찾는 findOne 메서드를 가지고 있습니다.

import { Injectable } from '@nestjs/common';

export type User = any;

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      userEmail: 'sory5339@gmail.com',
      password: 'sory5339',
    },
  ];

  async findOne(userEmail: string): Promise<User | undefined> {
    return this.users.find((user) => user.userEmail === userEmail);
  }
}

 

UsersService에 대하여 users.module.ts를 수정합니다.

exports를 하였는데 그 이유는 UsersModule 외 다른 모듈에서도 UsersService를 사용하기 위함입니다.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

 

다음으로 AuthService를 구현해 보도록 하겠습니다. 먼저 nest의 jwt 사용을 위해 설치해줍니다.

$ npm install --save @nestjs/jwt

auth.service.ts를 살펴보면 UsersService와 JwtService를 사용하여 validateUser와 login메서드를 구현했습니다.

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(userEmail: string, password: string): Promise<any> {
    const user = await this.usersService.findOne(userEmail);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { userEmail: user.userEmail, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
  • validateUser는 사용자의 email, password를 받아와 usersService에 있는 유효한 사용자인지 확인하여 사용자 정보 중 password를 제외한 정보를 result로 return 합니다.
  • login메서드는 사용자의 email과 userId를 payload로 하여 jwtService를 이용하여 token을 생성합니다.

이제 AuthModule을 구현해 봐야겠죠. import부분을 보면 jwtConstants, JwtStrategy, LocalStrategy가 있어요.

Passport strategy를 구현해 봅니다.  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({
      usernameField: 'userEmail',
    });
  }

  async validate(userEmail: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(userEmail, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

constructor내의 super 부분을 보면, 저는 usernameField에 userEmail을 이용하도록 하였습니다.

validate 메서드에서는 email과 password로 user가 아니면 Exception을 user이면 user를 return 해줍니다.

 

다음으로 jwt(jason web token) 구현을 위한 준비입니다. import jwtConstants와 관련된 부분이에요.

auth폴더 아래에 'constants.ts' 파일을 추가하여 아래와 같이 구현합니다. jwt를 생성할 때 사용하는 시크릿 키로 외부에 노출되면 안 되는 부분입니다.

export const jwtConstants = {
  secret: 'secretKey', // token 발급 시 사용되는 시크릿 키. 노출되면 안됨.
};

 

마지막으로 JwtStrategy를 위해 'jwt.strategy'를 생성해줍니다. LocalStrategy처럼  JWT를 위한 PassportStrategy확장형 JwtStrategy입니다.

local에서는 매핑할 filed 정도만 정의해 주었는데 jwt에서는 여러 가지 정보가 있어요. 상세 내용은 주석을 참고하세요.

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(), // Request에서 JWT를 추출하는 방법 중 Bearer Token 사용
      ignoreExpiration: false, // jwt 보증을 passport 모듈에 위임함. 만료된 JWT인경우 request거부, 401 response
      secretOrKey: jwtConstants.secret, // token 발급에 사용할 시크릿 키
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, userEmail: payload.userEmail };
  }
}

여기까지 import 된 부분에 대한 설명이었고, auth.module의 imports를 보면 UsersModule, PassportModule을 가져왔고 JwtModule은 register를 통해 등록하였어요. register() 안쪽 부분이 JwtModule에 대한 Options이에요.

 

auth module까지 끝났고요. 이제 guards에 대한 부분을 구현해 보도록 하겠습니다.

Guards의 역할은 request가 처리될지 말지를 결정합니다. 예를 들어 로그인에 정상적으로 성공한 경우 이후의 프로세스를 실행할 수 있지만, 로그인에 실패한 경우 더 이상 접근할 수 없도록 하는 것이지요.

 

'app.controllers.ts'를 열어 수정해줍니다. app.으로 왔다니 뭔가 끝이 보이는 거 같네요.

import { Controller, Request, Get, Post, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiParam } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { get } from 'http';
import { AppService } from './app.service';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { LocalAuthGuard } from './auth/local-auth.guard';

@Controller('auth')
export class AppController {
  constructor(
    private authService: AuthService,
    private appService: AppService,
  ) {}
  // @UseGuards(AuthGuard('local'))
  @UseGuards(LocalAuthGuard)
  @Post('/login')
  async login(@Request() req) {
    // return req.user;
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('/profile')
  getProfile(@Request() req) {
    return req.user;
  }

  @Get('/helloworld')
  getHello(): string {
    return this.appService.getHello();
  }
  @Get('/health')
  @ApiOperation({
    summary: 'auth health check',
    description: 'auth application 이 정상 상태인지 체크한다.',
  })
  getHealth(): string {
    return this.appService.getHealth();
  }
}

guard에 대한 부분도 strategy와 같이 local, jwt가 있어요.

먼저 'local-auth.guard.ts'를 구현합니다.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

'jwt-auth.guard.ts'

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;
  }
}

 

이제 코딩은 완료되었습니다. 테스트를 진행해 볼까요.

$ yarn start

서비스는 정상적으로 올라왔네요!

이번에는 Postman을 이용하여 사용자 로그인 정보를 넘겨서 테스트해봅니다.

유효한 사용자 정보를 입력하니 access_token이 정상적으로 생성됩니다.

http://localhost:3000/auth/login

이 토큰을 가지고 profile 정보를 조회해 봅니다.

http://localhost:3000/auth/profile

토큰 정보로 사용자 정보를 가져오는 것까지 확인해 보았습니다.

728x90
반응형

'NestJS' 카테고리의 다른 글

[NestJS] Auth Token을 쿠키에 저장하기  (1) 2022.03.25
[NestJS] swagger에서 테스트하기  (0) 2022.03.23
[NestJS] CRUD 구현해보기  (0) 2022.03.07
[NestJS] app.controller 살펴보기  (0) 2022.03.02
[NestJS] 개발하기  (0) 2022.02.25
728x90

aws자원을 이용하면서 어렵게 느껴졌던 부분이 네트워크와 보안이었어요.

오늘은 bastion host를 이용하여 private subnet에 있는 DB server에 접속하는 설정을 해보겠습니다.

 

AWS console 화면에 접속해주세요.

"인스턴스 시작"을 눌러 인스턴스를 생성해 봅니다.

 

업데이트가 빠른 Image 목록이 보이네요. 제일 최신 버전으로 선택해 봅니다.

 

비용에 연연하지 않는 편이지만, 그래도 공짜는 좋으니까요^^ 프리티어로 선택했어요.

 

다음으로 인스턴스 세부 정보를 구성합니다.

네트워크 : 사용할 VPC를 선택하는데요. 새 VPC를 생성해도 되지만 저는 기존에 사용하던 VPC를 선택했어요.

서브넷 : VPC를 선택하면 그와 연결된 서브넷 목록이 보입니다. 그중 퍼블릭 서브넷을 선택해 주면 됩니다.

스토리지는 기본값으로 설정하고 넘어가겠습니다.

 

다음은 태그입니다. aws계정을 여러 명이 사용하다 보면 태그가 중요해요.

누가 어떤 용도로 만들었는지 tag를 달아주는 것이 추후에 관리하기 편리합니다.

 

다음으로 보안 그룹이에요.

기존에 사용 중인 보안 그룹이 없다면 새 보안 그룹을 선택하여 허용할 포트를 설정해 주면 됩니다.

 

 

저는 bastion host용 보안 그룹을 이미 만들어 두어서 그걸로 선택하였습니다.

아래 인바운드 규칙을 보시면 ssh사용을 위한 22번 포트, mongoDB연결을 위한 27017 포트가 허용되어 있어요.

 

새로운 보안 그룹을 생성할 때에도 위의 인바운드 규칙을 참고하셔서 만드시면 됩니다.

 

검토 및 시작을 누르게 되면 7단계 인스턴스 시작 검토 내용이 나오고, 키 페어 선택하는 화면이 나옵니다.

키 페어는 해당 ec2에 연결할 때 사용할 인증서라고 보시면 됩니다.

새 키 페어를 생성하는 경우 키 페어 이름을 입력한 다음 [키 페어 다운로드]를 통하여 보관하면 됩니다.

 

저는 기존에 사용 중인 키 페어를 선택하였어요. 키 페어를 선택하면 

"해당 프라이빗 키 파일에 액세스 할 수 있으며 이 파일이 없으면 내 인스턴스에 로그인할 수 없음을 확인합니다." 이 부분에 동의를 해주고 [인스턴스 시작]을 해주면 됩니다.

인스턴스 시작이 됩니다.

 

728x90
반응형
728x90

드디어 지정한 배송일이 되었어요.

아침에 7시쯤 배송 도착했다고 문자로 알려주셨어요.

쿠*처럼 사진 찍어서 보내주시니 확실하네요.

출근이었음 남편보고 냉장고에 넣어놔줘 하고 쏜살같이 나갔을 텐데, 재택 근무일이라 일어나서 배송 온걸 바로 확인해 보았어요.

 

우선 제 주문목록은 이렇고요.

임산부 친환경 농산물 꾸러미라고 쓰여있어서 박스도 맘에 들어요. 더 배려받는 느낌이랄까요.

박스도 열기 쉽게 되어 있네요.

 

 

배송온 농산품들을 살펴보니 상태가 너무 좋아요!

지역별로 다른 업체에서 담당한다고 하는데 제가 사는 지역은 두레생협에서 오는데 갖 따온 거처럼 아주 신선해요!!

유기농 저지방우유만 먼저 뜯어서 먹어봤는데, 오 흰 우유 특유의 냄새가 안 나고 맛있어요! 900ml짜리인데 다음번 주문엔 더 많이 시켜야겠어요. 금방 먹을 거 같아요.

좀 이따 딸기랑 참외도 먹어봐야겠어요!

728x90
반응형
728x90

월 4회 3만 원 이상 구매 시 사용할 수 있는 임산부 친환경 농산물 꾸러미!

완성형과 선택형 중 저는 제 맘대로 골라서 주문할 수 있는 선택형 꾸러미를 주문해 보았어요.

 

구매할 상품들을 장바구니에 담아야겠죠.

저는 딸기, 참외, 방울토마토, 유기농 저지방 우유 등을 제사랑 과일과 야채 몇 가지를 담았어요.

 

여기서 주의사항!

※ 총 주문금액에서 농산물이 50% 이상 되어야 한다.

이것 말고는 모두 알고 있는 사항이네요.

그럼 결제하는 페이지로 가볼게요.

 

배송지 입력란 밑에 공동현관 출입 번호 입력란이 있어요.

일부 배송기사님들은 입력해도.. 공동현관 앞에 놓고 가셔서 불편할 때가 있는데, 두레생협은 어떻게 배송 올지 배송 오면 후기도 올려볼게요.

 

아무래도 야채, 과일, 육류 등 신선제품이다 보니 배송일이 중요한데요.

배송일을 선택할 수 있어요. (굿굿 칭찬합니다.)

저는 3-15(화)를 선택했어요. 맛있는 건 빨리 받고 싶으니까요.^^

 

3만 원만 넘겨야지 했는데 담다 보니 4만 원이 넘었네요;;;

결제내역을 보면 48,700원 중 포인트 38,960원 사용했고요. 제가 결제한 금액은 9,740원입니다.

 

와.. 이렇게 많은 친환경 농산물들을 9,740원에 집 앞까지 배송해준다니.. 임산부한테 정말 좋은 혜택인 거 같아요.

이런 혜택 많이 많이 만들어주면 좋겠어요. 세금 내는 보람 좀 느껴봅시다.

 

이제 배송이 어떻게 올지! 화요일에 후기로 돌아올게요.

 

 

728x90
반응형
728x90

회사에서 모니터를 32인치로 바꿔주었어요. 이제 노트북 화면은 볼일이 없어지고, 모니터를 나눠서 사용하려고 하는데, 왼쪽은 더 크게 하고 싶었습니다.

삼성에 찾아보니 "Easy Setting Box"라는 프로그램을 이용해서 가능하더군요.

 

1. 분할 프로그램 "Easy Setting Box" 설치하기

https://www.samsung.com/sec/consumer/learningresources/monitor/EasySettingBox/EasySettingBox/

 

Easy Setting Box 프로그램 설치

Easy Setting Box 프로그램 설치

www.samsung.com

 

PC 사양에 맞는 설치 파일을 다운로드합니다. 아쉽게도 window만 지원하네요.

 

2. 분할 프로그램 "Easy Setting Box" 실행 및 설정

프로그램을 화면이에요. 저는 1번이 노트북, 2번이 모니터입니다.

노트북은 그대로 두고 2번 모니터에 적용할 [화면 분할 방식]을 선택하고 [영역 조절]을 클릭합니다.

빨간색으로 영역이 보이고 이를 드래그하여 조절해줍니다.

저는 왼쪽에는 코딩 화면, 오른쪽엔 도큐먼트나 메모장을 주로 열어서 이렇게 세팅하였어요.

[나가기]를 누르면 분할 설정이 완료됩니다.

사용 방법은 창을 이동하면 상단에 어느 분할화면에 위치시킬지 창이 뜹니다.

왼쪽에 위치해 볼께요.

원하는 위치에 놓으면 창크기 조절 완료!

 

아주 편리합니다.

그럼 오늘도 즐거운 개발되세요.

 

 

+ 일부 프로그램에서 적용되지 않을 수 있다더니...

vscode는 되지 않네요ㅠㅠ

728x90
반응형

+ Recent posts