(DRF) JWT 인증

duo2208·2022년 2월 12일
0

Django

목록 보기
23/23
post-thumbnail

Token VS JWT


DRF의 Token

DRF 에서 지원하는 기본 Token 은 단순한 랜덤 문자열 입니다.

  • 각 User 와 1:1 관계를 이루며
  • 유효 기간이 없습니다.
""" Token Example """

>> import binascii
>> import os
>> binascii.hexlify(os.urandom(20)).decode()

ff199ba3d83de440a31e55c48494f906c8cfae2

쉘에서 Token 을 출력해보면 랜덤 문자열임을 확인해 볼 수 있습니다. 그러나 이를 이용해 의미 있는 데이터를 다루기는 어려워 보입니다.

JWT (JSON Web Token)

반면 JWT 는 토큰 자체가 데이터를 가지고 있어, 데이터베이스를 조회하지 않아도 로직만으로 인증이 가능합니다.

  • 포맷 : Header(헤더).PayLoad(내용).VerifySignature(서명)

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFza2RqYW5nbyIsImV4cCI6MTUxNTcyMTIxMSwiZW1haWwiOiIifQ.Zf_o3S7Q7-cmUzLWlGEQE5s6XoMguf8SLcF-2VdokJ

    Header를 base64 인코딩
    alg : 해싱 알고리즘
    typ : 토큰 유형
    Payload를 base64 인코딩
    sub/name : 토큰 제목
    iat : 토큰 발급 시간. (iuused_at)
    Signature = Header/Payload를 조합하고, 비밀키로 서명한후, base64 인코딩
    exp : 토큰 만료 시간 (expiration)
    aud : 토큰 대상자 (audience)
    iss : 토큰 발급자 (issuer)

  • 서버에서 토큰 발급 시에 SECRET_KEY(비밀키)로 서명을 하고, 발급시간을 저장합니다.

  • SECRET_KEY(비밀키) 서명은 암호화가 이니므로, 보안성 데이터는 넣지 말고 최소한의 필요한 정보만 넣도록 합니다. SECRET_KEY 에서 논의된 주의점은 데이터베이스 비밀번호, AWS 키, OAuth 토큰 등이 있습니다.

    • 이러한 민감 정보 보호를 위해 Django 에서는 settings.SECRET_KEY 를 활용하거나, 별도의 JWT_SECRET_KEY 설정을 합니다.
  • 토큰에 담을 정보, 정보의 한 조각을 뜻하는 claim 이라는 key/value 형식의 exp 를 사용합니다.

    • djangorestframework-jwt 에서는 Payload 에 user_id, user_name, email 이름의 claim 을 사용합니다.
  • 갱신 (Refresh) 메커니즘을 지원합니다. Token 유효기간 내에 갱신하거나, username/password 를 통해 재인증을 요구합니다.

  • 이미 발급된 Token 을 폐기 (Revoke) 하는 것은 불가능합니다.

""" JWT Example """

>> from bse64 import b64decode
>> b64decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')
b'{"typ":"JWT","alg":"HS256"}'
>> b64decode('eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFza2RqYW5nbyIsImV4cCI6MTUxNTcyMTIxMSwiZW1haWwiOiIifQ==')
b'{"user_id":1,"username":"askdjango","exp":1515721211,"email":""}'

🚀 (Django) SECRET_KEY
🚀 (JWT) Introduction



JWT


JWT의 Life cycle

JWT는 만료시간이 있고, Refresh를 지원합니다.

Token을 안전하게 보호하자

일반 Token 인지 JWT Token 인지의 여부에 상관없이 Token 들은 반드시 안전하게 보관되어야 합니다. 스마트폰 앱의 경우 설치된 앱 별로 안전한 저장공간이 제공되지만, 웹브라우저에서는 그런 저장공간이 없습니다.

  • Toekn은 앱 환경에서만 권장 됩니다.
  • 웹 클라이언트 환경에서는 세션 인증이 나은 선택일 수 있습니다. 단, 장고/웹클라이언트가 같은 호스트명을 가져야 합니다.
  • 통신은 필히 https 를 사용합니다.



djangorestframework-simplejwt


🚀 (SimpleJWT) Getting started

DRF에서 JWT를 기본으로 제공하지 않기때문에, simplejwt 서드파티 라이브러리를 이용합니다. 가이드 라인을 참고하여 이용해보겠습니다.

1. 설치

$ pip install djangorestframework-simplejwt

2. 셋업

패키지 및 url을 등록합니다.

# settings.py

INSTALLED_APPS = [
	...
    'rest_framework_simplejwt',
]

REST_FRAMEWORK = {
	...
	'DEFAULT_AUTHENTICATION_CLASSES' : [
    	...
        'rest_framework_simplejwt.authentication.JWTAuthentication',
	]
}
# accounts/urls.py

from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView

urlpatterns = [
	path('api-jwt-auth/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api-jwt-auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api-jwt-auth/verify/', TokenVerifyView.as_view(), name='token_verify'),
]

3. HTTPie 를 통한 JWT 발급 및 검증

HTTPie 를 이용하여 POST 하여 JWT Token 을 발급 받습니다.

>> http POST http://localhost:8000/accounts/api-jwt-auth/ username="username password="password
""" 인증에 성공할 경우, 토큰 응답이 옵니다. """

{
    "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQ0NjU2NjcyLCJpYXQiOjE2NDQ2NTYzNzIsImp0aSI6IjRjM2Y4Yzc3NmJhNDQ0MjRiNGFjYmVhOWMyYmMyNGQ3IiwidXNlcl9pZCI6MX0.720rWqd1MJD8Gc_0Xr2OgqercCIa0d6q1AnchDL3M6s",
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY0NDc0Mjc3MiwiaWF0IjoxNjQ0NjU2MzcyLCJqdGkiOiI3YmQ0MjU1MjlhMTE0ZWNjYjZhZmVhMzM0ODMwZTMzMSIsInVzZXJfaWQiOjF9.gAjTKuVY91PZwL5dlLP04KfDlXvwk8-e6sP45Y2ujjM"
}
  • 인증에 실패할 경우, 404 Bad Request 응답을 받습니다.
  • 발급 받은 JWT Token은 https://jwt.io 에서 어떤 의미를 가지고 있는지 알 수 있습니다.

HTTPie 를 이용하여 발급받은 JWT Token 을 확인합니다.

>> http http://localhost:8000/accounts/api-jwt-auth/verify/ token="token"
  • 인증이 잘 되면 입력한 Token이 그대로 반환됩니다.
  • exp 가 만료된 경우, 401 Unauthorized Signature has expired 응답을 받습니다.

4. 발급받은 JWT Token 으로 포스팅 목록 API 요청

  • DRF Token 에서는 인증헤더 시작 문자열로 'Token' 을 사용했지만, JWT 에서는 'JWT'를 사용합니다.
  • 매 API 요청시 마다 인증을 수행하며, 성공할 경우 응답을 받습니다.
# djangorestframework-jwt
>> http http://localhost:8000/app/post "Authorization: JWT {{토큰}}"
# djangorestframework-simplejwt
>> http http://localhost:8000/app/post "Authorization: Bearer {{토큰}}"

5. JWT Token 유효기간이 지났다면?

>> http http://localhost:8000/accounts/api-jwt-auth/verify/ token="token"
HTTP/1.0 401 Unauthorized
{
	"detail": "Signature has expired."
}

유효기간이 지났다면, 유효기간 내에 갱신을 해야만 합니다.

  • exp 가 만료된 경우, 401 Unauthorized Signature has expired 응답을 받습니다.
  • 유효 기간 내에는 Token 만으로 갱신이 가능하지만,
  • 유효 기간이 지났다면 username/password 를 통해 인증을 받아야 합니다.
  • djangresframework-jwt에서 JWT Token 의 유효기간은 settings.JWT_AUTHJWT_EXPIRATION_DELTA 를 참조 하며 default 는 5분입니다.

6. JWT Token 을 갱신 받는다

  • 반드시 Token 유효 기간 내에 갱신하도록 합니다.
  • djangresframework-jwt 의 settings.JWT_AUTH default 값은 False 인데, 이 상태에서 갱신을 요청하면 orig_iat 필드를 찾을 수 없다는 응답을 반환합니다. True로 설정해야 갱신이 가능하다고 합니다.
  • djangoresframework-simplejwt 의 경우는 default 로 갱신을 지원하고 있으니 직접 바꿔줘야 할 설정값은 없어보입니다. 만료 시간 정도만 5분은 너무 짧으니 더 길게 설정하면 될 것 같습니다.
>> http POST http://localhost:8000/accounts/api-jwt-auth/refresh/ token="token"



restframework-simplejwt 의 주요 settings


🚀 (SimpleJWT) Settings
# settings.py

from datetime import timedelta
...

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

📌 참고 출처

0개의 댓글