본문 바로가기

Django/Authentication

[Django] DRF의 JWT Authentication

 

Session based authentication

JWT의 장점을 알기 위해서는 먼저 session based authentication을 알아야 한다.

JWT를 사용하기 이전에는 주로 session을 이용한 인증이 이루어졌다.

 

How session authentication works

 

session 인증 과정

  1. 클라이언트에서 사용자의 인증 정보를 서버에 전달합니다.
  2. 서버는 인증을 처리한 뒤 해당 user에 대해 session을 생성합니다.
  3. session 정보는 서버에 저장되고, 클라이언트는 session id를 받아 브라우저에 저장합니다.
  4. 클라이언트는 이후 이루어지는 요청에 session id를 이용합니다.
  5. 서버는 전달 받은 session id를 이용하여 저장 중인 session 정보로 인증을 처리합니다.
  6. 만약 session id가 만료되었을 경우에는 1번 과정부터 다시 이루어집니다.

 

여기서 중요한 점은 session 정보를 서버에서 관리한다는 사실이다.

클라이언트도 브라우저에 session id를 저장하여 사용하지만 session id 자체는 중요한 정보가 담겨있지 않은 일종의 임시 비밀번호이며 실제 session의 정보를 관리하는 것은 전적으로 서버의 역할이다.

Django에서 기본적으로 제공하는 session 인증을 사용해보면 django_session 테이블이 생성되고 그 안에 session_key, session_data, expire_date와 같은 필드로 session 정보가 저장된다.

 

Disadvantages

session 인증의 단점은 매 요청마다 인증을 위해 데이터베이스를 탐색해야 한다는 점이다.

캐시 등을 이용하여 탐색 과정을 최적화 할 수 있지만 기본적으로 session 정보가 어딘가에 저장되는 구조이다.

또 session 정보를 저장하는 데이터베이스가 분산되어 있을 경우에는 각각의 데이터베이스를 탐색해야 하고, MSA로 설계된 경우 각 어플리케이션 마다의 session 정보를 통합으로 관리하는 것도 쉽지 않다.

이런 배경에서 탄생한 것이 바로 JWT이다.

 

 

JWT

Json Web Token의 약자로 웹 또는 모바일 클라리언트의 사용자 인증을 위해 사용하는 암호화된 토큰을 의미한다.

JWT의 가장 큰 차별점은 토큰 자체에 유저의 정보가 담겨져있다는 점이다.

session 인증모든 유저의 정보와 session 정보를 서버에서 관리하지만 JWT 인증에서는 토큰에 유저의 정보를 담습니다.

 

How JWT works

JWT 인증 과정

  1. (session 인증과 마찬가지로) 클라이언트에서 사용자의 인증 정보를 서버에 전달합니다.
  2. 서버는 인증 정보로 인증을 처리하고 (session 대신) JWT를 생성하여 클라이언트에 전달합니다.
  3. 클라이언트는 JWT를 브라우저에 저장합니다.
  4. 클라이언트는 이후 이루어지는 요청에 JWT를 이용합니다.
  5. 서버는 JWT를 검증하여 인증을 처리합니다.
  6. JWT가 만료되면 토큰을 refresh 합니다.
위의 과정이 session 인증과의 차이점을 비교해보면 다음과 같다.

1. 3번 과정에서 session 정보가 서버에 저장되는 반면 JWT 인증에서는 서버에 아무것도 저장되지 않는다.
2. 5번 과정에서 session 확인을 위해 데이터 베이스를 탐색하지만, JWT 인증에서는 토큰을 바로 검증한다.

session 인증과 JWT 인증의 가장 큰 차이점은 서버에 인증 정보를 저장하지 않는다는 점입니다.
그렇기때문에 클라이언트의 요청마다 인증을 위해 데이터베이스를 탐색하는 과정이 필요하지 않습니다.

그렇기때문에 클라이언트의 요청마다 인증을 위해 데이터베이스를 탐색하는 과정이 필요하지 않습니다.

그렇기때문에 클라이언트의 요청마다 인증을 위해 데이터베이스를 탐색하는 과정이 필요하지 않습니다.

 

 

JWT Structure

JWT는 아래의 예시처럼 . 을 이용하여 크게 세 개의 영역으로 구분되고 각각의 영역은 고유의 역할이 있다.

xxxxx.yyyyy.zzzzz

 

먼저 xxxx 부분은 Header 영역입니다. 쉽게 말해 JWT의 메타 정보를 나타낸다.

token의 타입을 정의하고(typ), 어떤 signing 알고리즘이 쓰였는지(alg)를 나타낸다.

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

위의 JSON 정보는 Base64Url로 인코딩 되어 JWT의 첫 번째 영역이 된다.

 

 

그 다음 yyyy 부분은 Payload로 불리는 영역이다.

이 부분에 토큰이 만료되는 시간, 유저의 정보와 같은 실질적인 데이터를 담는 영역이다.

{
  "token_type": "access",
  "exp": 1649145719,
  "jti": "1foo2jwt3id4",
  "user_id": 123
}

payload 역시 Base64Url로 인코딩 되어 JWT의 두 번째 영역이 된다.

 

 

JWT Signature

마지막 zzzzz 부분은 JWT의 핵심인 Signature 영역이다.


JWT는 signing을 통해 토큰 안에 유저 정보를 담으면서도 이를 안전하게 처리한다.

Payload 영역에 담긴 유저 정보는 인코딩만 되어 있지 별도의 암호화 처리가 되어 있지 않는다
누군가에 의해 쉽게 디코딩 될 수 있고 변조 될 수 있다는 말이다.

그런데 payload에는 유저를 특정할 수 있는 user id만 담았기 때문에 위의 토큰이 디코딩되어 user id가 노출되는 것은 보안상 큰 문제가 되지 않는다.
(user id를 통해 유저의 숫자가 노출될 수 있기 때문에 uuid를 쓰는 것이 조금 더 나은 방법이다)

 

그렇다면 변조의 문제만 해결하면 된다.

토큰의 변조는 signature 영역에서 해결된다.

Signature가 만들어지기 위해서는 인코딩 된 header, 인코딩 된 payload 그리고 secret 이 필요하다.
(Simple JWT에서는 secret으로 Django 프로젝트마다 사용하는 secret_key를 기본으로 이용한다)


Sinature는 인코딩 된 header, payload, secret을 합친 뒤 이를 header에 지정한 알고리즘으로 해싱한다.
header와 payload, secret 값 중 어느 하나라도 일치하지 않으면 signature는 완전히 다른 값을 갖게 된다.


이렇게 생성된 JWT는 클라이언트에 전달 되었다가 이후 요청에 HTTP Header에 담겨서 서버로 전달된다.

서버가 JWT를 검증하는 과정은 JWT가 생성될 때와 마찬가지로 header, payload 그리고 secret을 이용하여 signature를 해싱한 뒤 전달받은 JWT의 signature와 같은지 확인한다.

만약 payload가 변조 되었다면 클라이언트에서 받은 signature와 서버에서 해싱한 signature가 다를 것이다.

 

 

 

JWT 인증 체계를 사용한 이유

1. 프론트와 백의 분리

  • 클라이언트와 서버 간의 완전한 분리가 되어 사용자 로그인 상태를 브라우저에서 간단하게 저장할 수 있어 서버의 부담이 적다.

2. 보안 문제 예방

  • XSS와 CSRF 공격을 예방할 수 있다.
    • XSS : Cross Site Scripting의 약자로 Code Injection Attack이라고도 한다. 이는 공격자가 의도하는 악의적인 js 코드를 목표 웹 브라우저에서 실행시키는 것으로 요약할 수 있다
    • CSRF : Cross Site Request Forgery의 약자로 정상적인 request를 가로채 피해자인 척하고 백엔드 서버에 변조된 request를 보내 악의적인 동작을 수행하는 공격을 뜻한다. 예를 들어 피해자의 정보 수정정보 삭제무단 열람 등의 공격이 있을 수 있다특히 대표적인 예로내가 작성하지 않은 해로운 글이 특정 사이트에 게시되는 경우가 있다.

 

이러한 문제를 예방하기 위해서 JWT 토큰의 만료 시간을 짧게 정하고 (ex. 5분, 7분 등..), refresh 토큰을 추가적으로 생성하는 방법을 사용한다. Refresh 토큰을 httpOnly 쿠키로 설정하고, url이 새로고침 될 때마다 기존의 refresh 코튼을 가지고 새로운 JWT 토큰을 요청한다.

그리고 발급받은 JWT 토큰을 js내의 private 변수에 저장한다.

 

 

JWT Logic

## authenticate.py



from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication, CSRFCheck

from django.conf import settings
from django.contrib.auth import get_user_model


User = get_user_model()


class SafeJWTAuthentication(BaseAuthentication):
    """
    JWT Authentication
    헤더의 jwt 값을 디코딩해 얻은 user_id 값을 통해서 유저 인증 여부를 판단한다.
    """
    
    def authenticate(self, request):
        authorization_header = request.headers.get('Authorization')
        
        if not authorization_header:
            return None
            
        try:
            prefix = authorization_header.split(' ')[0]
            if prefix.lower() != 'jwt':
                raise exceptions.AuthenticationFailed('Token is not jwt')

            access_token = authorization_header.split(' ')[1]
            payload = jwt.decode(
                access_token, settings.SECRET_KEY, algorithms=['HS256']
            )
        except jwt.ExpiredSignatureError:
            raise exceptions.AuthenticationFailed('access_token expired')
        except IndexError:
            raise exceptions.AuthenticationFailed('Token prefix missing')
        
        return self.authenticate_credentials(request, payload['user_id'])
    
    def authenticate_credentials(self, request, key):
        user = User.objects.filter(id=key).first()
        
        if user is None:
            raise exceptions.AuthenticationFailed('User not found')
        
        if not user.is_active:
            raise exceptions.AuthenticationFailed('User is inactive')
        
        self.enforce_csrf(request)
        return (user, None)

    def enforce_csrf(self, request):
        check = CSRFCheck()
        
        check.process_request(request)
        reason = check.process_view(request, None, (), {})
        if reason:
            raise exceptions.PermissionDenied(f'CSRF Failed: {reason}')