Bcrypt & JWT

Gaeun·2022년 12월 26일
0

wecode TIL

목록 보기
12/24

1. Bcrypt

단방향 암호화에 사용되는 해시 알고리즘은 동일한 평문에 대해 항상 동일한 해시 값을 갖기 때문에 특정 해시 알고리즘에 대해 어떠한 평문이 어떠한 해시 값을 갖는지 알 수 있다. 해시 함수는 빠르게 데이터를 검색하기 위해 탄생되었고, 이는 공격자가 매우 빠른 속도로 임의의 문자열의 해시값과 해킹할 대상의 해시 값을 비교하여 대상자를 공격할 수 있다. 이러한 문제를 보안하기 위해 단방향 암호화를 진행할 때 솔팅과 키 스트레칭을 적용시키고, 이에 대한 내용은 이 링크에 있다.

1-1. Bcrypt란?

키(key) 방식의 대칭형 블록 암호에 기반을 둔 암호화 해시 함수.
레인보우 테이블 공격을 방지하게 위해 솔팅과 키 스트레칭을 적용한 대표적인 예이다.

1-1-1. 구조

$2b$12$76taFAFPE9ydE0ZsuWkIZexWVjLBbTTHWc509/OLI5nM9d5r3fkRG
 \/ \/ \____________________/\_____________________________/
Alg Cost       Salt                        Hash
  • 2b: 해시 알고리즘 식별자
  • 12: Cost Factor로 Key Stretching의 수 (2의 12승번)
  • 76taFAFPE9ydE0ZsuWkIZe: 16Byte 크기의 Salt, Base64로 인코딩된 22개의 문자
  • xWVjLBbTTHWc509/OLI5nM9d5r3fkRG: 24Byte의 해시 값, Base64로 인코딩된 31개의 문자

1-1-2. 검증

Bcrypt는 단방향 해시 알고리즘이다. 따라서 복호화가 불가능하다.

Bcrypt의 검증은 암호화된 값이 가지고 있는 알고리즘, Cost Factor, Salt를 이용한다. 비교하고 싶은 평문을 암호화된 값이 가지고 있는 알고리즘, Cost Factor, Salt을 이용하여 해시를 진행한 후 암호화된 값과의 비교를 통해 검증을 진행한다.

2. Node.js에서 bcrypt 사용하기

2-1. Bcrypt 설치

Bcrypt 모듈을 npm을 통해 설치

$ npm install bcrypt --save

2-2. Bcrypt로 비밀번호 암호화 (Async)

const bcrypt = require("bcrypt"); 					// (1)

const password = 'password'; 						// (2) 
const saltRounds = 12; 								// (3)

const makeHash = async (password, saltRounds) => {
    return await bcrypt.hash(password, saltRounds); // (4)
}

const main = async () => { 
    const hashedPassword = await makeHash(password, saltRounds); 
    console.log(hashedPassword);
}

main()
=> b'$2b$12$76taFAFPE9ydE0ZsuWkIZexWVjLBbTTHWc509/OLI5nM9d5r3fkRG'
  • (1): bcrypt 모듈 import
  • (2): 암호화 할 평문 변수에 할당, 실제 서비스에서는 사용자가 입력한 비밀번호가 할당 되어야 함
  • (3): Cost Factor, 8-12의 값을 사용하는 것이 적당함
  • (4): hash() method로 암호화
    • 첫 번째 인자: 암호화 하고 싶은 평문
    • 두 번째 인자: Cost Factor

2-3. Bcrypt 검증

const checkHash = async (password, hashedPassword) => {
    return await bcrypt.compare(password, hashedPassword) 	// (1)
}

const main = async () => {
    const hashedPassword = await makeHash("password", 12);
    const result = await checkHash("password", hashedPassword);
    console.log(result);
};

main()
=> true(or false) 											// (2)
  • (1): compare() method로 평문과 암호화된 값 비교
    • 첫 번째 인자: 비교하고 싶은 평문
    • 두 번째 인자: 암호화된 값
  • (2): 비교 결과, 평문과 암호화된 값을 비교해서 같으면 true를 다르면 false가 return 된다.

3. JWT

3-1. JWT란?

JWT(JSON Web Token)는 클라이언트(사용자)와 서버 간에 정보를 JSON 개체로 안전하게 전송하기 위한 개방형 표준(RFC 7519)이다. SAML (Security Assertion Markup Language Token) 보다 크기가 작아 더 컴팩트하게 사용할 수 있다.
JWT는 JSON 개체에 기본정보, 전달할 정보, 검증 정보를 모두 담고 있다. 또한, JWT는 전자 서명이 되어있기 때문에 검증 과정을 거쳐 확인하고 신뢰할 수 있으며 Secret Key 또는 Public/Private Key Pair를 사용하여 서명할 수 있다.

JWT는 일반적으로 Base64로 인코딩된 데이터와 전자 서명으로 구성되어 있다. JWT 역시 데이터를 암호화 할 수 있지만, JWT가 전자 서명이 되어 있다는 점이 중요하다. 전자 서명된 JWT의 목적은 데이터를 숨기는 것이 아니라 데이터의 신뢰성을 보장하는 것이다. 그렇기 때문에 서명된 JWT와 함께 HTTPS를 사용하는 것이 좋다.

JWT을 이용한 인증 과정은 사용자 측에 사용자의 정보를 관리하는 토큰 기반 인증 메커니즘이다. 따라서 서버에서 세션 정보를 저장하기 위해 세션 스토리지 또는 데이터베이스에 완전히 의존할 필요가 없다. 또한, 서버의 확장성과 멀티 기기 및 도메인에서 활용에서도 이점을 가지고 있다.

3-2. JWT의 구조

JWT는 3가지 구성 요소(Header, Payload, Signature)로 이루어져 있으며, 각 구성 요소들은 dot(.)으로 구분이 되어 있다.

3-2-1. Header

Header는 JWT의 첫 번째 구성요소로서, 일반적으로 2가지 정보를 담고 있다.

  • alg: Signature을 만드는데 사용한 알고리즘 정보
  • typ: Token의 타입

예를 들어 아래와 같은 Header가 있다고 하면 Signature을 만드는데 사용한 알고리즘은 HS256이고, Token의 타입은 JWT라는 의미이다.

// Header
{
    "alg" : "HS256",
    "typ" : "JWT" 
}

3-2-2. Payload

Payload는 JWT의 두번째 구성 요소로서 실질적으로 전달해야 하는 정보들을 가지고 있다. Payload에 담긴 정보 하나 하나를 Claim이라고 하는데, 3가지 종류의 Claim이 존재한다.

Registered Claims

이미 JWT 표준으로 지정된 Claim. 총 7가지의 Registered Claim이 존재하며, 해당 Claim을 무조건 전부 사용해야 되는 것은 아니고 적절히 상황게 맞게 사용하면 된다.

  • iss: 토큰 발급자
  • sub: 토큰 제목
  • aud: 토큰 대상자
  • exp: 토큰 만료시간
  • iat: 토큰 발급 시간
  • nbf: 토큰 활성화 시간
  • jti: JWT의 고유 식별자

Public Claims

JWT를 사용하는 사람들이 공개적으로 정의할 수 있다. 그러나 기존에 이미 등록되어 있는 Claims와 충돌을 방지하려면 IANA JSON Web Token 레지스트를 참고하거나 UUID, OID, 도메인 이름 등을 사용해야 한다.

Private Claims

Public Claims과 달리 오직 사용자와 서버 사이에서만 합의하여 사용하는 Claim.

// Payload 구성 예시
{
    "exp": "1245678900", 								//Registered Claims
    "https://velopert.com/jwt_claims/is_admin": true, 	//Public Claims
    "user_id" : 12345123 								//Private Claims
}

3-2-3. Signature

Signature는 JWT의 세번째 구성 요소이며, 문자 그대로 JWT의 서명 부분이다. Header의 인코딩된 내용과 Payload의 인코딩된 내용을 더한 뒤에 Secret Key와 알고리즘을 이용하여 암호된 값을 나타낸다.

전달 받은 토큰의 Header와 Payload를 서버의 Secret Key를 이용해서 암호화를 진행한다. 그리고 해당 값이 전달 받은 Signature와 같은지 비교하여 JWT의 신뢰성을 확인 할 수 있다. 서버에서 관리하고 있는 Secret Key가 아닌 다른 Key로 JWT를 발급 한다면 Signature가 달라지기 때문에 해당 JWT는 신뢰 할 수 없는 토근이다.

아래와 같이 HS256(HMAC SHA256) 알고리즘으로 암호화된 Signature을 살펴보면, Headerd와 Payload가 합쳐진 내용을 secret과 HMACSHA256을 이용하여 암호화 되어 있다는 것을 확인 할 수 있다. 즉, secret을 모른다면 암호화된 Sinature의 내용은 확인 할 수 없다.

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

4. Express에서 JWT 사용하기

4-1. jsonwebtoken 모듈 설치

jsonwebtoken 모듈을 npm을 통해 설치

$ npm install jsonwebtoken --save

4-2. JWT 발급 (HS256)

const jwt = require('jsonwebtoken'); 				// (1)

const payLoad = { foo: 'bar' }; 					// (2)
const secretKey = 'mySecretKey'; 					// (3)
const jwtToken = jwt.sign(payLoad, secretKey); 		// (4)

console.log(jwtToken)
=> 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTA1NTYxMzZ9.YAMgUMLhiVUwkRTr2rpOrIyWN0cTGLxsxZBqLAaKWUU'
  • (1): jsonwebtoken 라이브러리 import
  • (2): 실제로 전달할 내용인 Payload 정의, 객체의 형태로 할당.
    • 실제 서비스에서는 노출이 되어도 상관은 없지만 사용자의 정보를 알 수 있는 값으로 전달
  • (3): Secret Key, 실제로 Secret Key는 노출되면 안 되기 때문에 환경변수로 관리해 주어야 함.
    • 유추할 수 없도록, 규칙이 없는, 적당히 긴 문자열로 만들어야 함.
    • 지금과 같이 코드에 노출시켜서는 안 됨
  • (4): sign() method로 JWT 발급
    • 첫 번째 인자: Payload
    • 두 번째 인자: Secret Key
    • 세 번째 인자(옵션): Algorithm, 단 HS256가 default

4-3. JWT 확인

const decoded = jwt.verify(jwtToken, secretKey); 	// (1)

console.log(decoded)
=> { foo: 'bar', iat: 1650555667 } 					// (2)
  • (1): verify() method로 JWT의 Payload 확인
    • 첫 번째 인자: JWT
    • 두 번째 인자: 토큰을 만들 때 사용한 Secret Key. 토큰을 만들 때 사용한 Key가 아니라면 에러가 발생.
  • (2): JWT가 가지고 있던 Payload
profile
🌱 새싹 개발자의 고군분투 코딩 일기

0개의 댓글