본문 바로가기
SW 개발/REST API

JWT(JSON Web Token) Encoding 방법 (Python sample code)

by Kibua20 2020. 7. 8.

글을 시작하기 전에 JWT에 대한 이해는 아래 동영상을 확인해주세요. 개념적으로 쉽게 설명이 되어있습니다. JWT에 대한 개념을 듣고 아래 내용을 확인하시면 실무적으로 도움이 될 것 같습니다.   출처: https://www.youtube.com/watch?v=MUUqogMpGiA


Gmail REST API는 Service side Web application으로 Access token을 받아 Gmail API를 사용하는 과정을 설명하였고 이 경우에는  API의 권한(scope) 승인을 위해서 계정 사용자의 로그인과 명시적 권한 승인 과정이 필요하였다.  JWT (Json Web Token) 방식은 서버-서버 인증 방식으로 웹 로그인 없이 서비스의  인증을 받을 수 있는 방법으로 서버에 공개를 키를 미리 등록하여 이에 상응하는 Client에 Private 키로 인증받는 방식이다.  서버-서버 Application 기반으로 Google API를 사용하기 위한 사전 준비 내용이다.

 

이전 글 (웹 서버 Application, Gmail 기반):

[모바일/REST API] - Google Gmail API 사용 방법 (1) - Sample code

[모바일/REST API] - Google Gmail API 사용 방법 (2) - Sample code

[모바일/REST API] - Google gmail API 사용 방법 (3) - Sample code


JWT 방식 장단점

  • 사용자 로그인 필요 없다.  한번 인증된 서비스 계정의 JWT은 Refresh token과 동일하게 Access token을 계속 발급받을 수 있다.  
  • 사용자 인증 정보가 JWT에 모두 포함되기 때문에 추가적인 저장소가 필요 없다. 
  • 서비스 Private key 기반으로 한반 발급된 Private 은 유출되는 경우 보안상 문제가 될 수 있다.
  • 구글의 경우 별도의 서비스 계정을 생성해야 한다.  사용자 계정자 계정은 연동은 가능하지만 마치 별도의 계정처럼 일부 제약 사항이 존재한다.

JWT 구성 

JWT는 3개의 파트로 1) Header 2) Claim set, 3) Signature로 구성되며,  각각  점로 구분된다.  Header와 Clamin Set은 JSON을 Base64URL로 encoding 한 UTF-8 스트링이다.  일반적인 Base64 인코딩은 “+”, “/”, “=”를 포함될 수 있지만, URI에서 파라미터로 사용할 수 있도록 URL-Safe 한 Base64url 인코딩을 사용한다. Signature는 Private 키를 사용해서 Base64url로 encoding 된 스트링이다.  

 

  • Header는 해쉬 암호화 알고리즘(e.g. RS256)과 Token의 타입(JWT)을 지정한다. 구글의 경우에는 RS256 알고리즘만 지원하고, Private Key ID 값을 추가해야 한다. 
  • Claim set은 name / value의 한 쌍으로 구성되어 있고  여러 개의 클레임을 추가할 수 있다. 각 서버마다 claim set에 필수 조건을 명시하고 있으며, 구글 API에서는 iss, scope, aud, exp, iat의 클레임 값은 필수이다.  
  • Signature는 secret key를 포함하여 암호화 (RSA256 Base64url safe)


{Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature}​

 

 

JWT은 Header, Claim set, Signature로 구성됨

JWT Website 에서는 다양한 알고리즘에 따른 JWT encoding 값을 확인할 수 있다. 

JWT Web site: JWT 으로 encoding 결과를 확인할 있음



Python으로 JWT encoding

구글에서는 pyJWT 패키를 사용해서  Python JWT encoding 샘플 코드를 제공한다.  Private key ID와 key 값은 구글 사이트에서 받아서 저장을 해야 하고 API 인증의 고유한 정보로 보안을 유지해야 한다.  구글은 RS256 해쉬 알고리즘만 지원하기 때문에 'RS256'으로 설정해야 한다. 

 

구현 시 JWT encoding의 결과가 맞는지 확인할 수 있는 방법이 없어 디버깅에 나름(?) 고생을 했습니다.  제 아이디의 서비스 계정과 private key 값과 JWT 결과를 그대로 공개했으며, 아래 python code로 API 인증 및 동작이 확인된 값입니다. 파이썬이나 Java, Java script로 구현하시는 분은 JWT encoder의 reference로 사용하셔도 좋습니다. 본 게시글이 공개되는 시점에서는 제 서비스 계정은 삭제 예정이라, JWT 값은 유효하지만 API 인증은 에러가 발생할 것입니다.

 

Header

  alg: RS256 해쉬 알고리즘

  kid: Private key ID (구글 API에서는 필수 사항)

 

Claim set

  iss: issuer, 토근 발급자, 구글의 서비스 계정을 입력한다.

  sub: subject 토큰 제목 (Optional)

  scope: 사용하고자 하는 API 권한 (API reference 참고)

  aud: 토큰 인증 서버 (https://oauth2.googleapis.com/token)

  iat: 인증 요청 시간 (Numeric date 형식으로 표시)

  exp: 토큰 만료 시간 (인증 요청 시간 +1시간)

 

Signatrue: 

 Private key값 

# sudo pip3 uninstall JWT
# sudo pip3 install PyJWT (https://pyjwt.readthedocs.io/en/latest/)

import jwt

def getSignedJWT():
    PRIVATE_KEY_ID_FROM_JSON ="e1eba38c4d7fef671273a6f62c46cbb15db3f854"
    PRIVATE_KEY_FROM_JSON= "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCQvQNoYwOz6sHO\nfPHVVVM+5qXJKvSq/OBcuom8L58O0SN5rgNeGGINHuegrTtTMoC6xgk6nvcuq9uh\noQy4gy2Rzqj+3fKRR3J94NO3gYXyILz1Uj5mzqEDJ0xyFMEfUQXkuRm9G8Y0EweR\noiQyA0sSRtegZLJInu9ELDVHqegjQsztaYjh9amjI9i5DeRQCOiTiYTfsk1Ecg1J\nqCHy3GiquHuuttFQe+HEKTuLAZQtSTd10u6aIlJ1Dxu1sj3aol1i2OfHPAoFvnx/\nbRIgJlDWzUJ2x664rVquqLDhkIw+2GrmV4DtNJujITjZnZt2ojlrCo4GMRzd7qAm\nCYtF+QZdAgMBAAECggEAOHvCwjBtx/8zvejNmWLQd0ocZZqhW78OsbFMQgfVizs7\nnGc3wjdCwHsQiohAEBIz4W+aN2nE7c48imFmrPToSi/7jGbCHEblG9Gq3cCqrJhx\nFU2Qs58sf6YM87I8wYNliOJhdIbLvRO2DvPXKztUxx+lU18ooiWAGzsjWcGpKWT4\nagxYGmlimmSme2OuIA9Qw1z9tloeNB8hgAN8CDWpT9VlBwfbiLWeDokf6sMAhJ/+\nbWEK+NTvY+GBHb+/VSiB4rhPOpmV5gtKmgvngeEvYGV8HYAojFObuhHMepAVeoPe\nGUhhStJNxrFn3lrnuoPFkL/L/QZreMZSuAIrufDT8wKBgQDH7ZSAUaAPiUKzJiYI\ndgOvblywKfC0Snjvqm/w+oHdFoEuTjwVZ5NyFkJQny53sd0ANLLkwJtklGe8zMKv\nJBQAzKK0hyvfekv9hcn+VlURrAPkb/xn+Jq25VhM3ukRs56wbBLIV1VJ3uL26VIx\nkhjE4a1DpBxL0q8auYLJH9PihwKBgQC5VO0PV72GuYosvs0T+RNiMA2QsktshYIg\nKG5T4Gip3O21TVUOrF+eHgHrQfU4mk6hyumNhaCxD6oyz2cXT03LYCINKin+9OqR\nx558xBcH7a87bv78QQ9H8lupNT3zyK0Egw/jxQ55OrhCOzfy/V7mEKUv3pn1gwt5\n+DswUDW0+wKBgQDHKMdm8GkXMO/t0JHQmedf6fuRTaZHo2xHqywqDRIysIltHGhE\nFlLOMphLAddjSx5RZy3SLIBfuGqCrCNAHxuCFFf8qC6vR3/NhGpM36mMmiOie2Ag\nHonYqizFHsVkad8p9e7b/gurM8o6lwDW+qeL8RgNqry5V54xbB15xyfmnwKBgEdB\n3fveMmLQh834doVNaSSBcVXHF7TcCFIw+WqKh/N3nHXvC9seb40t4HMB4zUmL0GJ\n8Q6W6Ffru/bZQ7v0o+akSbNiGM+Mf3wZklhKVMiZnJxvat62bReumYuPiwhmig+I\nDN34cD4wU5QzjKmCvbAbikfDgNKi1hDJXoiO7ndtAoGBALauTA1J3Xg7Duoe/iFJ\nmFkgY4qbYjrkOlmTIumqAJ6j3CbUyiMCA4NBqXq36bDsxsvnGMeeTlsnR1ZfZ+oq\n/dB8oAwIBxMBPUjprzyRv/UYpDa85Zhbi2o2jC48z1h6+ETrFbNffkxdreMwm/TD\nUSnDktVvKZ+w01kk4EcMKWPm\n-----END PRIVATE KEY-----\n"

    iat = time.time()
    exp = iat + 3600
    payload = {
                'iss': 'kibua20-service@calendarapi-282612.iam.gserviceaccount.com',
                'scope': 'https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.events.readonly',
                'aud':'https://oauth2.googleapis.com/token',
                'iat': iat,
                'exp': exp
            }
    additional_headers = {'kid': PRIVATE_KEY_ID_FROM_JSON}
    signed_jwt = jwt.encode(payload, PRIVATE_KEY_FROM_JSON, headers=additional_headers,algorithm='RS256')
    return signed_jwt.decode('utf8')

 

* 실행 결과 (JWT encoding 결과)

yJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImUxZWJhMzhjNGQ3ZmVmNjcxMjczYTZmNjJjNDZjYmIxNWRiM2Y4NTQifQ.eyJpc3MiOiJraWJ1YTIwLXNlcnZpY2VAY2FsZW5kYXJhcGktMjgyNjEyLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwic2NvcGUiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9hdXRoL2NhbGVuZGFyLnJlYWRvbmx5IGh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvY2FsZW5kYXIgaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9jYWxlbmRhci5ldmVudHMgaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9jYWxlbmRhci5ldmVudHMucmVhZG9ubHkiLCJhdWQiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImlhdCI6MTU5NDIxMjg2Ni4wMzYyODM1LCJleHAiOjE1OTQyMTY0NjYuMDM2MjgzNX0.M5KCpndywJ0KxL2DQ7jsuIolFUIKxpY7Hlag9OqnEk9VTttJ-S2QAbdcq3k42SLHig7zoEaybN-qPS-SDFzMVL4HPQ17P4NWwE-JvFJx4Q-O3PE5VVO00q3o7caeMYjo8zuqBUiHehRuNcCTF4cEGqFgRXEh9eYvk9jROrgAT498Vk7E-PEPoqNDEGzNA5acR8y4JxIeQPCflN0hxEt7sWvAoIxuG2ATmFO3sXbtQj1t0JDhhHq-e010TKBH5I-HZ-zpsLY9H0SCrCTMBso4NWhlo7rOXM1ghF91uo0exAd5UkDBcQ0wraoXxzdL4MUZW2-sKXkTDNpfTlc8bGoCsg

 

* 에러 발생:      Error:AttributeError: module 'jwt' has no attribute 'encode'

파이썬 JWT와 PyJWT가 같이 설치되는 경우로 pyJWT 대시 JWT가 실행되어 에러가 발생한다.  pip로 JWT은 삭제하고 pyJWT으로 설치하면 정상적으로 동작한다. (Python 3.8 기준)

 

$ sudo pip3 uninstall JWT
$ sudo pip3 install PyJWT

 

참고 사이트) https://stackoverflow.com/questions/33198428/jwt-module-object-has-no-attribute-encode

 

 JWT C/C++ lib

C/C++ 로도 JWT encoding을 구현할 수 있다. Python이나 Java, Java script에서도 JWT encoding이 가능하지만 코드가 노출 되어 Private Key ID와 Key를 보안을 유지할 수 없다. 이 경우에는 C/C++로 JWT encoding 부분만 JWT으로 구현할 수 있다.

 

libjwt 설치 및 사이트:

$ sudo apt-get install -y libjwt-dev 

site:  https://github.com/benmcollins/libjwt

 

libjwt-cpp 사용:

https://github.com/benmcollins/libjwt




댓글