[포스코x코딩온] KDT-Web-8 8주차 회고1 - JSON 웹 토큰 (JWT) 이해하기

Yunes·2023년 8월 23일
2

[포스코x코딩온]

목록 보기
22/47
post-thumbnail

서론

JSON 웹 토큰이 무엇이고, 현대 웹 애플리케이션에서 JWT 가 왜 중요할까?

JWT 토큰이란?

JSON Web Token (JWT) 는 정보를 JSON 개체로 안전하게 전송하기 위한 간결하고 독립적인 방법을 정의하는 개방형 표준 (RFC 7519) 이다. 이 정보는 디지털 서명되어 있으니 확인하고 신뢰할 수 있다. JWT 는 헤더, 페이로드, 서명의 세 부분으로 구성된다.

JWT 는 디지털 서명되어 있어 확인하고 신뢰할수 있다. 이는 발신자가 토큰에 서명하고 수신자는 토큰의 진위 여부를 확인할 수 있음을 의미한다. JWT 는 비밀키 (HMAC 알고리즘 사용 ) 혹은 RSA, ECDSA 를 사용하는 공개 / 개인키 쌍을 사용하여 서명할 수 있다.

HMAC (해시 기반 메시지 인증코드)
HMAC 은 공유 비밀 키를 사용해서 JWT 에 서명하고 확인하는 대칭 알고리즘이다. 송/수신자 모두 동일한 비밀 키를 사용한다. 보낸 사람은 비밀 키를 사용하여 서명을 만들고, 받는 사람은 동일한 키를 사용하여 서명을 확인한다.

  • 토큰위 위변조가 있었는지 확인하여 위변조를 방지하는 기법중 하나
  • 대칭키 알고리즘

MAC (Message Authenticate Code)
원본 메시지와 전달된 메시지를 비교하여 변조 여부를 확인하는 방식

  • HMAC 은 Hash 와 Mac 을 사용하여 변조 여부를 확인한다.

RSA (Rivest - Shamir - Adleman) & ECDSA (타원 곡선 디지털 서명 알고리즘)
RSA & ECDSA 는 한 쌍의 키인 확인용 공개 키와 서명용 개인 키를 사용하는 비대칭 알고리즘이다. 보낸 사람은 개인 키를 사용하여 서명을 만들고 받는 사람은 보낸 사람의 공개 키를 사용하여 서명을 확인한다.

JWT.io 에서 Node.js 를 사용하는 JWT 라이브러리로 jsonwebtoken 을 소개한다.

기본적인 기능은 jose 가 더 좋아보이지만 ⭐️ 수가 jsonwebtoken 이 훨씬 많아서 jswonwebtoken 을 사용하는 것이 더 신뢰가 간다.

jsonwebtoken github repo

JWT 를 언제 사용해야 할까?

인가(Authorization)의 경우

가장 흔히 JWT 를 사용하는 경우이다. 한번 사용자가 로그인한 뒤로는 그 뒤의 요청들은 JWT 를 포함할 것이고 이는 사용자를 토큰에게 허가된 특정 경로, 서비스, 자원들로의 접근을 가능하게 한다.

Single Sign On 은 오버헤드가 작고 다른 도메인을 넘어 쉽게 사용하기 쉽기 때문에 오늘날에 JWT 를 가장 흔히 사용하는 기능이다.

Signle Sign On
각 애플리케이션에서 별도로 로그인할 필요 없이 단일 자격 증명 세트 (사용자 이름 및 비밀번호)를 사용해 여러 애플리케이션이나 서비스에 액세스할 수 있도록 하는 중앙 집중식 인증 및 권한 부여 매커니즘을 말한다.

정보교환의 경우

JWT 는 당사자간에 정보를 안전하게 전송하는 좋은 방법이다. 예를 들어 공개키 / 개인키 쌍을 사용해서 JWT 에 서명할 수 있으니 보낸 사람이 누구인지 확인할 수 있다. 또한 헤더와 페이로드를 사용해 서명을 계산하여 콘텐츠가 변조되지 않았는지 확인할 수 있다.

JWT 구조

압축된 형태의 JWT 는 . 으로 구분된 세 부분으로 구성된다.

header.payload.signature

헤더는 토큰의 타입과 암호화 알고리즘에 대한 정보를 JSON 형태로 갖고 있다.

암호화 알고리즘으로는 HMAC SHA256 / RSA 등을 사용한다.

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

JWT 토큰은 Base64 로 인코딩되어 있어서 eyJ 로 시작하는 아주 긴 문자열로 구성된다.

payload

페이로드는 클레임(Claim)이라는 것을 포함한다. 클레임은 사용자와 같은 엔티티. 그리고 부가적인 데이터 등을 담는 정보다. 클레임은 registered claim, public claim, private claim 3가지 타입이 있다.

registered claim

Registered claim 은 IANA 에 등록되어 있는 클레임이다. 이들은 모든 경우에 필수적으로 사용해야 하는 것은 아니나 이 클레임들은 유용하고 상호 운용 가능한 클레임들의 집합의 시작점을 제공한다. JWT 를 사용하는 애플리케이션은 사용하는 특정 클레임과 필수 혹은 선택 사항을 정의해야 하며 JWT 의 핵심 목표는 표현을 간결하게 하는 것이므로 3글자로 축약되어 있는 것을 확인할 수 있다.

iss

  • issuer claim. JWT 를 발급한 주체를 식별한다. OPTIONAL

sub

  • subject claim. JWT 의 주체를 식별한다. OPTIONAL

aud

  • audience claim. JWT 의 수신자를 식별한다. OPTIONAL

exp

  • expiration time claim. JWT 의 만료시간을 식별한다. OPTIONAL

nbf

  • not before claim. JWT 가 처리를 위해 승인되어서는 안되는 이전 시간을 식별한다. OPTIONAL

iat

  • issued at claim. JWT 가 발급된 시간을 나타낸다. OPTIONAL

jti

  • JWT ID claim. JWT 를 위한 유일한 식별자를 제공한다. OPTIONAL

public claim

JWT 를 사용하는 사람들이 마음대로 정의할 수 있다. 충돌을 방지하기 위해 IANA JSON 웹 토큰 레지스트리에 정의하거나 충돌 방지 네임스페이스를 포함하는 URI 로 정의해야 한다.

{
    "http://domain.com/is_domain": true
}

private claim

사용자가 동의한 당사자 간에 정보를 공유하기 위해 생성된 클레임으로 등록되거나 공개된 클레임이 아니다. 서버와 클라이언트 사이에서만 협의된 클레임으로 공개 클레임과 충돌이 일어나지 않게 사용한다.

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

페이로드는 Base64Url 로 인코딩되며 JWT 의 두번째 부분을 구성한다.

서명한 토큰은 변조로부터 보호되나 누구나 읽을 수 있다. 암호화되지 않은 경우 JWT 의 페이로드나 헤더 요소에 비밀 정보를 넣으면 안된다.

signature

서명 부분을 생성하려면 인코딩된 헤더, 인코딩된 페이로드, secret, 헤더에 명시된 알고리즘 등을 사용해서 암호화하여 서명한다.

HMAC SHA256 알고리즘은 다음과 같은 방식으로 생성한다.

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

서명은 메시지가 중간에 변경되었는지를 확인하는데 사용하며 개인키로 서명된 경우 JWT 를 누가 보냈는지 확인할 수 있다.

JWT 가 어떻게 동작하나?

웹에서 보통 Authorization HTTP 헤더를 Bearer <토큰> 형태로 설정하여 클라이언트에서 서버로 전송한다.

Authorization: Bearer <token>

JWT 토큰을 HTTP 헤더에 담아 전송하기에 너무 용량이 커져서는 안되는데 일부 서버는 헤더에 8KB 보다 더 큰 용량을 받지 않기도 한다.

만약 Authorization 헤더에 담겨 토큰이 전송되면 쿠키를 사용하지 않으니 CORS 가 발생하지 않는다.

JWT 는 API 나 리소스에 접근하기 위해 획득되고 다음과 같은 방식으로 사용된다.

이미지 출처: jwt.io

  1. 애플리케이션이나 클라이언트는 인가 서버에 인가를 요청한다. 이는 인가 흐름중 하나를 통해 수행된다. 예를 들어 OpenID Connect 호환 웹 애플리케이션은 인증 코드 흐름을 /oauth/authorize 엔드포인트를 사용하여 처리한다.
  2. 인가가 승인되면 인가 서버는 애플리케이션에 access token 을 반환한다.
  3. 애플리케이션은 보호된 리소스, API 에 접근하는데 access token 을 사용한다.

예시

카카오 인가코드 받을때


참고 - 카카오 developer

애플 인가

참고 - apple developer

이처럼 /oauth/authorize 엔드포인트를 사용하는 사례들을 찾아볼 수 있었다.

OpenID Connect 란?

OpenID Conenct 는 OAuth 2.0 사양 프레임워크를 기반으로 하는 상호 운용 가능한 인증 프로토콜이다. Authorization Server 에서 수행한 인증을 기반으로 사용자의 신원을 확인하고 상호 운용 가능하며 REST 와 유사한 방식으로 사용자 프로필 정보를 얻는 방법을 단순화한다.

개발자에게 현재 연결된 브라우저나 모바일 앱을 사용하는 사람의 신원에 대해 안전하고 검증 가능한 답을 제공한다.

OpenID Connect 를 사용하면 자격 증명 기반 데이터 침해와 자주 관련된 비밀번호 설정, 저장 및 관리 책임이 없어진다.

OpenID Connect 작동방식

  1. 사용자는 브라우저를 통해 웹사이트나 웹 애플리케이션으로 이동
  2. 사용자가 로그인을 클릭하고 이름과 비밀번호를 입력
  3. Relying Party (클라이언트, RP)는 OpenID Provider (OP) 에게 요청을 보낸다.
  4. OP 는 사용자를 인증하고 인가한다.
  5. OP 는 Identity Token 으로 응답하는데 일반적으로 Access Token 을 사용한다.
  6. RP 는 액세스 토큰이 포함된 요청을 사용자 장치에 보낼 수 있다.
  7. UserInfo 엔드포인트는 사용자에 대한 클레임을 반환한다.

용어

Authentication : 앱, 브라우저를 사용중인 사람이 본인임을 입증하는 보안 프로세스

OpenID Provider (OP) : OpenID Connect 및 OAuth 2.0 프로토콜을 구현한 엔티티.

Client : 사용자를 인증하거나 리소스에 접근하기 위해 토큰을 요청하는 소프트웨어. OP 에 등록되어 있어야 한다. 클라이언트는 웹 애플리케이션, 모바일 및 데스크톱 애플리케이션 등을 말한다.

Relying Party : 사용자 인증 기능을 OP 에 요청하는 애플리케이션 또는 웹사이트를 말한다.

Identity Token : 인증 프로세스의 결과를 나타내는 토큰을 말한다. 사용자에 대한 식별자 (클레임이라 불림)와 사용자 인증 방법, 시기에 대한 정보를 포함한다.

User : 등록된 클라이언트를 사용하여 리소스에 액세스하는 사람을 말한다.

OpenID Connect 는 서명이 필요할때 JWT 데이터 구조를 사용한다.

OpenID Connect 는 인증(Authentication)을 위해 사용한다. 즉, 다른 플랫폼을 통해 사용자가 누구인지 확인하기 위해 사용한다.
OIDC 는 사용자의 개인 정보가 담긴 ID token 을 확보하기 위해 사용한다.

OAuth 는 인가(Authorization) 을 위해 사용한다. 즉, 해당 플랫폼에 저장된 사용자의 데이터에 접근하기 위해 사용한다.
OAuth 는 다른 플랫폼의 다른 API 를 호출하기 위해 access token 을 확보하기 위해 사용한다.

JWT 를 사용하는 이유

JWT 는 SWT (Simple Web Token), SAML (Security Assertion Markup Language Tokens) 과 비교시 여러 이점이 있다.

  • 간결하고 효율적인 포맷을 제공한다. JSON 은 XML 보다 더 간결하고 인코딩시 크기가 작아 JWT는 SAML 보다 더 압축적이라 HTML 및 HTTP 환경에서 전달하기 용이하다.

  • SWT 는 공유된 비밀키를 사용해 대칭적으로 서명되나 JWT 와 SAML 토큰은 공개 / 비공개 key pair 를 사용해서 서명할 수 있다. 복잡한 XML 서명과 비교시 보안 측면에서 간단하고 용이하다.

  • JSON 은 직접 객체에 매핑되므로 JSON 파서가 흔히 사용된다. 반면 XML 은 자연스러운 문서 - 객체 매핑이 없어 처리하기가 어렵다.

즉, JWT 는 간결하며 유연한 보안 옵션, 파싱에 용이한 이점들을 가지고 있어 다양한 환경에서 인증과 데이터 교환에 사용되고 있다.

JWT 의 취약점

만약 누군가 토큰을 탈취한다면 누구나 토큰을 탈취당한 사람의 계정에 접근할 수 있게 된다. 그런데 서버에서는 이미 탈취당한 JWT 에 어떤 조치도 취할 수 없다. 이를 막기 위해 access token 과 refresh token 을 함께 사용하는 방법일 사용한다. access token 으로 사용자를 인증하나 만료시간을 짧게 설정하고 이 access token 이 만료시 refresh token 을 사용하여 access token 을 재발급받을 수 있다.

Access Token & Refresh Token

OIDC 에서도 등장했던 access token 과 JWT 의 취약점을 해결하기 위해 같이 사용하는 refresh token 은 무엇일까?

access token 만을 통한 인증 방식은 탈취당할 경우 보안에 취약하다. 그래서 토큰에 유효시간을 부여하는 식으로 탈취 문제에 대응할 수 있다. 그런데 유효 시간을 짧게 한다면 너무 자주 로그인을 요청하여 불편함을 초래할 수 있다.

access token 은 접근에 관여하는 토큰이며 refresh token 은 재발급에 관여하는 토큰이다. 둘다 JWT 토큰이나 하는 역할이 다르다.

access token, refresh token 동작 순서

사용자가 로그인을 요청하면 서버는 로그인 성공시 refresh token 과 access token 을 발급하여 반환한다.

서버는 이때 refresh token 을 db에 저장한다.

클라이언트는 refresh token 을 쿠키나 세션, 스토리지에 저장하고 요청할 때마다 access token 을 헤더에 담아서 보낸다.

만일 access token 이 만료되면 서버는 access token이 만료되었음을 확인하고 토큰 만료로 인해 권한이 없다는 것을 클라이언트에 알린다.

사용자는 access token 과 refresh token 을 함께 서버로 보내고 서버는 db 에 저장된 refresh token 을 비교한다. refresh token 이 동일하고 만료기한이 지나지 않았다면 서버는 새로운 access token 을 발급한다.

이미지 출처 : 그랩의 블로그

jsonwebtoken 사용법

npm i jsonwebtoken

sign

jwt.sign(payload, secretOrPrivateKey, [option, callback])

  • 비동기 일때 콜백함수가 제공되면 콜백은 err 혹은 JWT 와 함께 호출된다.

  • 동기 일때 JWT 토큰을 문자열로 반환한다.

payload 는 유효한 JSON 을 나타내는 object literal, buffer, 문자열이 될 수 있다.

단, payload 가 객체 리터럴일 때만 exp 를 포함한 다른 클레임이 설정되며 buffer, 문자열 payload 는 JSON 유효성을 확인하지 않는다.

secretOrPrivateKey 는 문자열 (utf-8 인코딩), buffer, object, HMAC 알고리즘이나 RSA, ECDSA 를 위해 개인키를 인코딩한 PEM 과 같은 비밀키를 포함하는 KeyObject 등을 말한다.

option

  • algorithm : default 는 HS256
  • expiresIn : 초단위 혹은 vercel/ms 를 나타내는 문자열로 나타낸다.
    • 60, "2 days", "10h", "7d"
  • notBefore : 초단위 혹은 vercel/ms 를 나타내는 문자열로 나타낸다.
  • audience
  • issuer
  • jwtid
  • subject
  • noTiemstamp
  • header
  • keyid

expiresIn, notBefore, audience, subject, issuer 는 기본값이 없다. 이들 클레임은 페이로드에 바로 exp, nbf, aud, sub, iss 로 명시할 수 있으나 두군데에 동시에 사용해서는 안된다.

exp, nbf, iat 는 NumericDate 이다.
NumericDate : 날짜와 시간을 숫자 값으로 표시하는 특정 방식으로 1970 년 1월 1일 이후 경과된 초 수를 나타낸다.

Synchronous Sign with RSA SHA256

// sign with RSA SHA256
var privateKey = fs.readFileSync('private.key');
var token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' });

Sign asynchronously

jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }, function(err, token) {
  console.log(token);
});

페이로드에 담은 object literal 이 그대로 jwt 의 두번째 파트인 payload 에 들어가니 option 에 exiresIn 을 명시하거나 payload 에 exp 로 만료 시간을 넣어 줄 수 있다.

만료시간이 1시간인 토큰

jwt.sign({
  exp: Math.floor(Date.now() / 1000) + (60 * 60),
  data: 'foobar'
}, 'secret');

jwt.sign({
  data: 'foobar'
}, 'secret', { expiresIn: 60 * 60 });

//or even better:

jwt.sign({
  data: 'foobar'
}, 'secret', { expiresIn: '1h' });

verify

jwt.verify(payload, secretOrPrivateKey, [options, callback])

  • 비동기 일때 콜백함수가 제공되면 비동기로 동작한다. 콜백은 서명이 유효할 때 복호화된 payload와 선택적으로 expiration, audience, 혹은 issuer 가 유효한지에 대해 호출된다. 그렇지 않으면 에러와 함께 호출된다.

  • 동기 일때 콜백은 제공되지 않으며 동기로 동작한다. 서명이 유효할 때 복호화된 payload와 선택적으로 expiration, audience, 혹은 issuer 가 유효한지에 대해 반환한다. 그렇지 않으면 에러를 던진다.

token 은 JWT 토큰을 말한다.

secretOrPrivateKey 는 문자열 (utf-8 인코딩), buffer, object, HMAC 알고리즘이나 RSA, ECDSA 를 위해 개인키를 인코딩한 PEM 과 같은 비밀키를 포함하는 KeyObject 등을 말한다.

jwt.verify 가 비동기로 호출될때 secretOrPrivateKey 는 개인키나 공개키를 가져와야 하는 함수일 수 있다.

options

  • algorithms : 허가된 알고리즘 문자열의 리스트.
    • secret - ['HS256', 'HS384', 'HS512']
    • rsa - ['RS256', 'RS384', 'RS512']
    • default - ['RS256', 'RS384', 'RS512']
  • audience : 수신자를 확인하고 싶다면 값을 여기에 넣는다.
  • complete : 페이로드의 일반적인 콘턴츠 대신 복호화된 { payload, header, signature } 를 반환한다.
  • issuer : iss 필드에 유효한 문자열이나 문자열 배열
  • ignoreExpiration : true 일때 토큰의 만료기간을 확인하지 않는다.
  • subject : 토큰의 sub 를 확인하고 싶을때 사용한다.
// verify a token symmetric - synchronous
var decoded = jwt.verify(token, 'shhhhh');
console.log(decoded.foo) // bar

// verify a token symmetric
jwt.verify(token, 'shhhhh', function(err, decoded) {
  console.log(decoded.foo) // bar
});

// invalid token - synchronous
try {
  var decoded = jwt.verify(token, 'wrong-secret');
} catch(err) {
  // err
}

// invalid token
jwt.verify(token, 'wrong-secret', function(err, decoded) {
  // err
  // decoded undefined
});

// verify a token asymmetric
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, function(err, decoded) {
  console.log(decoded.foo) // bar
});

// verify audience
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo' }, function(err, decoded) {
  // if audience mismatch, err == invalid audience
});

// verify issuer
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer' }, function(err, decoded) {
  // if issuer mismatch, err == invalid issuer
});

// verify jwt id
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid' }, function(err, decoded) {
  // if jwt id mismatch, err == invalid jwt id
});

// verify subject
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid', subject: 'subject' }, function(err, decoded) {
  // if subject mismatch, err == invalid subject
});

// alg mismatch
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) {
  // if token alg != RS256,  err == invalid signature
});

// Verify using getKey callback
// Example uses https://github.com/auth0/node-jwks-rsa as a way to fetch the keys.
var jwksClient = require('jwks-rsa');
var client = jwksClient({
  jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json'
});
function getKey(header, callback){
  client.getSigningKey(header.kid, function(err, key) {
    var signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

jwt.verify(token, getKey, options, function(err, decoded) {
  console.log(decoded.foo) // bar
});

레퍼런스

docs
datatracker - jwt
jwt.io
OpenID Conenct
blog
daleseo - jwt
inpa dev - jwt
그랩의 블로그 - access token & refresh token

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

2개의 댓글

comment-user-thumbnail
2023년 8월 26일

정리 정말 최고네요 .. jwt 랑 restapi 써주신 글로 공부해 보려고 합니다 좋은 내용 감사합니다!

1개의 답글