JWT

Ajisai·2024년 2월 17일
0

etc

목록 보기
5/8

참고한 내용

https://jwt.io/introduction (공식 문서)

https://velopert.com/2389 (Velog 만드신 분의 블로그)

구조

{{HEADER}}.{{PAYLOAD}}.{{SIGNATURE}}
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Hashing algorithm
    • "HS256"HMAC SHA256
  • "typ": Token type
    • JWT 쓸 거니까 "JWT"
  • Base64로 인코딩된다.
    • 공백, 개행 문자는 모두 삭제한 후 인코딩된다.
    • 즉 위 예제 기준으로 '{"alg": "HS256","typ": "JWT"}'가 인코딩되는 것.

{{PAYLOAD}}

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
  • Base64로 인코딩된다.
    • 이런 인코딩이나 해싱은 JWT 관련 라이브러리에 의해 자동으로 수행된다.
  • Data
    • 토큰에 담을 정보
    • key-value 쌍 하나를 claim이라 한다.

Claim

Predefined(registered) claim(의무는 아니고 권장)

  • iss: issuer
    • 토큰 발급한 놈
  • sub: subject
    • 토큰 제목
  • iat: issued at
    • 토큰 발급한 때
  • exp: expiration time
    • 토큰 만기
    • 1480849147370NumericDate 형식이어야 한다.
    • 당연히 현재 시간 이후로 설정되어야 한다.
  • nbf: not before
    • 이 날까지는 토큰 처리하지 말어
    • NumericDate 형식
  • aud: audience
    • 토큰 대상
  • jti: jwt id
    • JWT 식별자
    • 중복 처리 방지용으로 쓰는 게 보통
    • 한 번만 쓸 토큰에 쓰면 좋음
  • 그 외 여러가지

Public claim

{
  "http(s)://specific.uri/@unique_recommended_namespace/additional_path": true
}
  • 사용자가 정의할 수 있는 공개 클레임
    → 충돌할 수 있으므로 위와 같이 정의한다.
  • 이름은 IANA JSON web token registry에 정의된 URI 형식
    • Ex)
      {
        "https://velopert.com/jwt_claims/is_admin": true
      }
  • 충돌이 방지된 namespace(@unique_recommended_namespace)를 포함하는 URI

Private claim

{
  "username": "ajisai"
}
  • 정보 공유를 위해 custom된 클레임
  • 양쪽 (보통 클라이언트와 서버) 간 협의 하에 쓰이는 이름
  • 충돌 가능하므로 유의해서 사용해야 한다.

클레임은 섞어도 됩니다

{
    "iss": "velopert.com",
    "exp": "1485270000000",
    "https://velopert.com/jwt_claims/is_admin": true,
    "userId": "11028373727102",
    "username": "velopert"
}

Predefined, Public, Private을 혼용해도 된다.

Base64 인코딩 시 주의점

  • 뒤에 =가 붙기도 하고 안 붙기도 한다.
    • 보통은 2개가 붙고 1개가 붙을 때도 있다.
  • 그런데 JWT는 URL 파라미터로 전달되는 경우도 있다
    • URL에서 =는 query string에 쓰이므로 URL-safe하지 않다.
    • 따라서 전부 지워야 한다(디코딩에서는 상관없음).

{{SIGNATURE}}

HMACSHA256( //Pseudocode
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
  • 위 슈도코드는 hashing까지만 된 상태
    • hex 값으로 간주해 암호화한다.
  • secret
    • 사용자가 지정하는 비밀코드
    • hashing에 쓰이는 salt 값
    • private key로 서명한 전자 서명
  • 이렇게 만들어진 값을 다시 Base64 인코딩하면 서명이 완성된다.
  • Base64(ENCRYPT(hash(base64(HEADER).base64(PAYLOAD), SECRET))

요약하면

//Header
const header = {
  "typ": "JWT",
  "alg": "HS256"
};
const encodedHeader = new Buffer(JSON.stringify(header))
                            .toString('base64')
                            .replace('=', ''); //remove padding

//Payload
const payload = {
  "iss": "velopert.com",
  "exp": "1485270000000",
  "https://velopert.com/jwt_claims/is_admin": true,
  "userId": "11028373727102",
  "username": "velopert"
};
const encodedPayload = new Buffer(JSON.stringify(payload))
                            .toString('base64')
                            .replace('=', '');

//Signature
const crypto = require('crypto');
const signature = crypto.createHmac('sha256', 'SECRET')
             .update(encodedHeader + '.' + encodedPayload)
             .digest('base64')
             .replace('=', '');

const jwt = encodedHeader + "." + encodedPayload + "." + signature;

중간중간에 해시나 인코딩은 라이브러리가 해준다면 직접 할 필요는 없다.

Access token & Refresh token

Access token

  • API 통신 시 쓰이는 인증 토큰
  • 요청과 함께 왔다갔다 하므로 탈취 위험이 크다. → 유효 기간을 짧게 한다.

Refresh token

  • Access token의 유효기간 만료 시 쓰인다.

어떻게 쓰이냐면

  1. 만료된 access token으로 요청하면 401 Unauthorized가 응답으로 돌아온다.
    • 클라이언트는 내 access token이 invalid token이 되었음을 알 수 있다.
  2. Access token이 아닌 Refresh token으로 다시 요청한다.
  3. 응답 header에 새로운 access token을 포함해 응답한다.
    • refresh token마저 invalid token이 되었다면 동일하게 401이 응답된다.
      • 이 경우 클라이언트는 다시 로그인을 해야 한다.

왜 이렇게 하냐면

  • access token은 탈취 가능성이 높다.
    • 만기를 짧게 하면 탈취되더라도 곧 만료된다.
    • 이 경우 다시 탈취해야 한다.

만기 조작하면 그만이잖아요

  • JWT는 Header, Payload, Signature로 구성된다.
  • Payload
    • 여기에 expired date가 포함된다.
  • Signature
    • header.payload를 암호화한 것
    • payload의 expired date를 조작해도 signature는 그대로다.
    • signature에서 복호화된 payload에 나온 expired date는 조작 전의 값이다. 이 정보는 private key를 가진 서버만 알 수 있다.

결론적으로 현재 payload에 포함된 expired date(조작됨)와 signature에 포함된 expired date(조작되지 않았음)가 맞지 않으므로 서버는 access 권한을 주지 않는다.

Refresh token 탈취하면 그만이잖아요

  • 통신 빈도가 적어서 어렵긴 하지만 완전 불가능한 건 아니다.
  • OAuth에서는 Refresh Token rotation을 제시한다.
    • Access token 발급을 재요청할 때 Refresh token도 새로 받는다.
    • 즉 Refresh token도 더이상 만기가 긴 token이 아니게 된다.

Spring에서 JWT를 쓰려면

여기를 참고 바랍니다.

profile
Java를 하고 싶었지만 JavaScript를 하게 된 사람

0개의 댓글

관련 채용 정보