ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Web] JSON Web Token
    programing/Web 2020. 12. 3. 10:28

    JWT

    JSON Web Token은 IETF(Internet Engineering Task Force)의 RFC(Request for Comment) 7519로 제안된 기술이다. (인터넷 표준은 특별한 RFC 또는 RFC의 집합을 가리킨다.) 해당 RFC는 현재 Proposed Standard 로써, 표준으로 채택된 듯 하다.

    해당 RFC를 보면, JWT에 대해 이렇게 설명하고 있다.

    JWT은 두 당사자 간 전송될 클레임을 나타내는 간결하고 URL-안전한 방법이다.

    소개

    JWT은 HTTP 요청 헤더나 URI 쿼리 파라미터와 같이, 제한된 환경을 가진 공간에서 사용하기 위한 간결한 클레임 표현 방법이다. 조금 더 쉽게 말하자면, 두 기기 간에 정보를 JSON 형태로 교환하는 간결하고 자가수용적인 방법이다.

    JWT은 클레임을 JSON Web Signature(JWS) 구조의 페이로드로서 사용되는 JSON 객체의 형태 혹은 JSON Web Encryption(JWE) 구조의 평문의 형태로 전송가능하게 인코딩한다. 이 때, 클레임은 전자적으로 서명되거나 무결성이 Message Authentication Code(MAC)를 통해 암호화된다. JWT은 항상 JWS 간결 직렬화 혹은 JWE 간결 직렬화로 표현된다. 조금 더 쉽게 말하자면, JWT은 시크릿(암구호) 혹은 공개/비밀 키를 이용해 서명된다.

    JWT 자체를 암호화 하여 두 당사자 간 비밀을 공유할 수 있지만, 서명된 토큰에 더 중점을 둔다. (서명된 토큰은 그 안에 담긴 클레임의 무결성을 확인할 수 있으며, 암호화된 토큰은 클레임 자체를 암호화한다.) 비대칭 키를 이용해 서명한 토큰은 비공개 키를 보유한 당사자만이 서명을 한 사람임을 보장한다.

    참고로 발음은, jot 이다. 😱

    클레임

    클레임(claim)은 주체에 저장된 정보 조각이다. 클레임은 JSON 객체와 유사하게, 키와 값으로 구성되어 있다. 여기서 키는 claim name, 값은 claim value라고 한다. 클레임 네임은 항상 string 형이며, 클레임 밸류는 어떠한 JSON 타입이 될 수 있다.

    클레임 네임은 간결함을 위해, 3글자로 구성되도록 정해져 있다. (아닌 것도 있다.)

    활용

    인증

    기존의 세션 및 쿠키와 같은 인증 수단 중 하나로써 사용된다. 유저가 한번 로그인하면, 이후 인증이 필요한 요청에 JWT을 포함하므로써 인증을 처리할 수 있다. 토큰 기반 인증 방식은 오버헤드가 적고 여러 도메인에 걸쳐서 사용하기 용이하다.

    정보 교환

    JWT은 서명되기 때문에, 두 파티 간 정보를 교환하는 데 사용하기도 좋다. 또한 헤더와 페이로드를 이용해 서명을 하기 때문에, 변조 탐지도 할 수 있다.

    구조

    JWT은 크게 header, payload, signature세가지 부분으로 나눌 수 있다.

    Header

    헤더는 전형적으로 두가지 정보를 담는다.

    • 토큰의 타입
    • 서명에 사용된 알고리즘 (HMAC, SHA256, RSA)
    {
      "alg": "HS256",
      "typ": "JWT"
    }

    그리고 위 JSON은 base64를 통해 인코딩되어 JWT의 첫 부분에 들어가게 된다.

    base64는 8비트 이진 데이터(예를 들어 실행 파일이나, ZIP 파일 등)를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식이다.

    Payload

    페이로드는 클레임을 담는다. 클레임은 전형적으로 엔티티(보통 유저) 및 기타 정보들을 담는다.

    클레임에는 세가지 종류가 있다.

    • registered claims
      • 유용하고 상호 운용이 가능한 클레임 집합을 제공하기 위해, 강제는 아니지만 권장되는, 미리 정의된 클레임 집합이다.
      • 여기엔 iss(발행자), iat(발행 시간), exp(만료 시간), sub(주체), aud(청중) 등이 포함된다.
      • 각 클레임에 대한 자세한 정보는 RFC 7519 4.1에 나와 있다.
    • public claims
      • JWT을 사용하는 사용자라면 누구나 마음대로 정의할 수 있다.
      • 단, 클레임 네임 충돌을 피하기 위해 IANA JSON Web Token Registry에 클레임을 등록하거나 충돌을 피할수 있는 네임스페이스를 가진 네임을 사용해야 한다. 보통은 URI를 사용하는 듯.
      • { "https://velopert.com/jwt_claims/is_admin": true }
    • private claims
      • 해당 토큰의 생성자와 소비자가 서로 동의한 클레임이다.
      • 토큰 레지스트리에 등록되지 않고, 퍼블릭 클레임에 포함되지 않는 이름이어야 한다.

    서명된 토큰은 무결성은 보장하지만 누구나 읽을 수 있다. 암호화 되지 않는 경우, 헤더 혹은 페이로드에 민감한 정보를 넣지 말아야 한다.

    {
        "iss": "einere.com", // 등록된 클레임 네임
        "exp": "1485270000000", // 등록된 클레임 네임
        "https://einere.com/jwt_claims/is_admin": true, // 공개된 클레임 네임
        "userId": "11028373727102", // 비공개 클레임 네임
        "username": "einere" // 비공개 클레임 네임
    }

    페이로드도 마찬가지로 base64를 통해 인코딩되어 JWT의 두번째 부분에 들어가게 된다.

    Signature

    서명을 만들기 위해서는 인코딩된 헤더와 인코딩된 페이로드, 시크릿(혹은 비밀 키), 헤더에 명시한 암호화 알고리즘이 필요하다. 에를 들어, HMAC SHA256 알고리즘을 이용한다면 다음과 같을 것이다.

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

    단, base64로 인코딩 하는 경우, padding을 의미하는 = 문자가 포함될 수 있는데, 해당 문자는 URL-unsafe 하므로 제거해줘야 한다.

    = 문자는 제거해도 복호화에 문제가 없다.

    이 서명은 토큰의 내용이 전송 과정에서 변조되지 않았음을 보증한다. 만약 비밀 키로 서명된 토큰이라면, JWT의 전송자가 발행자임을 보증할 수 있습니다.

    위 세가지 부분을 합치면 위 그림과 같이 토큰이 완성되며, JWT 공식 사이트의 디버거를 통해 검증할 수 있다.

    차이점

    서버 기반 인증 방식

     

    HTTP는 기본적으로 stateless하기 때문에, 유저 정보를 저장하고 로그인 이후에 유저를 인증하는 과정이 필요하다. 대부분은 유저의 세션 id를 쿠키에 저장하는 방식을 사용한다.

    이러한 방식은 여러가지 단점을 가지고 있다.

    • Hard to scale : 서버는 유저 세션 정보를 메모리 혹은 DB에 유지하고 있어야 한다. 분산 시스템의 경우, 각 서버간 세션을 저장하고 확인하고 동기화하는 작업이 복잡하다.
    • CORS : 기본적으로 HTTP 프로토콜은 요청에 쿠키를 포함하지 않기 때문에, 다른 출처에 자원을 요청할 경우 CORS 문제가 발생할 수 있다. (사실 이건 JWT 써도 마찬가지 아닌가..?)
    • Coupling to web framework : 웹 프레임워크와 커플링이 생긴다. 만약 다른 언어나 프레임워크를 사용한 서버 세션 데이터를 공유하는 것은 매우 어렵다.

    cookie나 Authentication 등 자격증명을 포함한 요청은 credentialed request라고 합니다. 이러한 요청은 크로스 사이트 요청일 때 자격증명을 포함해서 보내지 않습니다.
    만약 자격 증명을 포함한 통신을 원한다면 요청 측에서는 credentials와 관련된 처리를 해야 하며, 응답 측은 Access-Control-Allow-Credentialstrue 로 설정해야 합니다.
    추가로 응답 측은 Access-Control-Allow-Origin 를 와일드카드(*)를 제외한 출처를 명시해야 합니다.

    자격증명에는 쿠키, Authorization 헤더, TLS 클라이언트 인증서가 있습니다.

    토큰 기반 인증 방식

     

    토큰 기반 인증 방식은 stateless하기 때문에 유저 인증 정보를 별도로 저장할 필요가 없다. 즉, 유저가 어느 서버에서 로그인에 성공했는지 신경쓰지 않아도 된다. 단순히 토큰 값을 재활용 하면 된다.

    동작 방식

    인증

    유저가 로그인에 성공하면, access token이 반환된다. 이 토큰 또한 신원 증명서이기 때문에 유효기간(exp)을 최대한 짧게 설정해야 한다. 토큰 값을 저장할 위치는 크게 세 가지가 있다.

    • local storage
    • session storage
    • cookie

    modern storage API는 개발자 도구에서 다 조회가 가능하기 때문에, 추천되지는 않는다. 따라서 보통은 httpOnly 설정이 된 쿠키에 토큰값을 저장한다.

    유저가 권한이 필요한 페이지나 자원에 접근할 때, 인증정보를 포함한 요청 방식은 두가지가 있다.

    • Authorization 헤더에 Bearer 인증 타입(스킴)과 토큰 값
    • 쿠키 (서버에서 쿠키에 access token을 설정한 경우)
    Authorization: Bearer <token>

    서버는 토큰 값을 확인해서 유효성 검증을 한다.

    인증 타입에 대해 더 알고 싶다면 Hypertext Transfer Protocol (HTTP) Authentication Scheme Registry를 참고하자.

    장점

    • Stateless & Scalable
      • JWT은 자가수용적이기 때문에, 어느 서버에 귀속될 필요 없이 어느 서버에 전달할 수 있다.
    • Reusability
      • 같은 토큰을 여러 서버에 재사용 할 수 있다. 따라서 다른 앱과 권한을 공유하는 앱을 만들기 쉽다.
    • JWT Security
      • 쿠키를 사용하지 않는다면 CRSF(cross-site request fergery) 공격을 따로 대비할 필요가 없다.
      • 만약 민감한 정보를 담아야 한다면 JWE로 암호화 할 수 있으며, HTTPS를 통해 토큰을 전송하여 중간 공격을 막을 수 있다.
      • 보안적 결함 없이 XML을 서명하는 것은 매우 어려운 반면, JSON을 서명하는 것은 훨씬 간편하다.
    • Performance
      • JWT은 XML에 비해 덜 서술적이기 때문에 더 작고 가볍다.
      • 매 요청마다 세션을 찾고 역직렬화 할 필요가 없기 때문에, 성능이 좋다.
    • Cross-platform
      • JWT은 인터넷에 규모에서 사용되기 때문에, 모바일과 같은 다른 플랫폼에서도 잘 작동한다.

    JWT 활용

    sign

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

    verify

    var jwt = require('express-jwt');
    
    app.get('/protected',
      jwt({ secret: 'shhhhhhared-secret', algorithms: ['HS256'] }),
      function(req, res) {
        if (!req.user.admin) return res.sendStatus(401);
        res.sendStatus(200);
      });

    jwt 미들웨어와 익명 함수 미들웨어를 체이닝할 수 있다. jwt 함수가 요청(req) 의 user 속성에 디코딩된 JWT 페이로드를 설정한다. (물론 옵션을 통해 저장할 속성을 변경할 수 있다.)

    express-jwt 은 기본적으로 토큰이 유효하지 않다면 에러를 던진다.

    app.use(function (err, req, res, next) {
      if (err.name === 'UnauthorizedError') {
        res.status(401).send('invalid token...');
      }
    });

    위와 같이 미들웨어를 통해 에러 핸들링을 커스터마이징할 수 있다.

    참고

    JWT 공식 홈페이지

    RFC 7519

    JWT에 대해 알아보자!

    [JWT] JSON Web Token 소개 및 구조

    JSON Web Token Tutorial: An Example in Laravel and AngularJS

    교차 출처 리소스 공유 (CORS)

    HTTP 인증

    댓글

Designed by black7375.