Q> JWT(JSON Web Token)기반 인증 및 권한관리 구현시 보안강화를 위한 방안 문의

개발 프로젝트에서 JWT기반의 인증과 권한관리를 구현하고자 합니다.
JWT(JSON Web Token)기반 인증 및 권한관리 구현시, 클라이언트 정보를 활용하여 보안을 강화할 수 있는 구체적인 구현방안에 대한 도움을 요청합니다.

A>

- 최근 JWT(JSON Web Token)기반으로 사용자 인증 및 권한관리를 구현하는 경우가 많아졌다.
- JWT는 전통적인 session 방식과 다르게, 서버에 인증정보를 갖지 않아도 되는 특장점들이 있기 때문에 기존의 전통적인 session기반의 인증 처리와 함께 사용범위가 확대되고 있다.
- JWT 토큰 생성하는 부분을 개발할때, 클라이언트의 특징정보들을 활용하여 JWT보안 강화하는 방법들이 몇 가지있으며, 서비스 환경과 목적에 따라 선택적으로 사용하는 것이 바람직하다.

<> JWT생성시 클라이언트의 특징정보들을 활용하여 클라이언트를 식별할수 있는 방법
1. 클라이언트 IP주소 활용 방법

- 클라이언트의 IP 주소를 JWT의 Payload에 포함시키는 방법이다.
- 클라이언트의 IP 주소는 클라이언트를 식별하는 가장 기본적인 방법 중 하나이다.
- 서버에서 요청을 받을 때 마다, JWT의 IP 주소와 요청한 클라이언트의 IP 주소를 비교하여 일치하는지 검증한다.
- 이 방법은 클라이언트의 IP 주소가 변경되지 않을 경우에는 유용하지만, IP 주소가 변경될 가능성이 있거나 여러 IP 주소를 사용하는 경우에는 적합하지 않을 수 있습니다.

2. 클라이언트의 지문(Fingerprint)를 활용하는 방법

- 디바이스 지문은 클라이언트 디바이스의 여러 속성(예: 사용자 에이전트, 화면 해상도, 설치된 폰트, 브라우저 플러그인 등)을 종합하여 생성한 고유 식별자이다.
- 이러한 정보들은 클라이언트의 디바이스를 특정할 수 있기 때문에 Payload를 이용하여 유용하게 사용될수 있다.

3. HTTP 헤더를 이용하는 방법

- HTTP 요청과 함께 전송되는 헤더 정보(예: User-Agent, Accept-Language, Accept-Encoding 등)는 클라이언트의 브라우저, 운영 체제, 언어 설정 등이 있다.
- 이 정보를 Payload에 적용하면 이용하여 클라이언트를 식별하는 데 도움이 될 수 있다.

- 웹 애플리케이션은 사용자의 세션을 유지하기 위해 쿠키나 세션 ID를 사용한다.
- 이 정보는 클라이언트의 특정 세션을 식별하는 데 사용될 수 있다.

5. OAuth/OpenID 토큰을 이용하는 방법

- SNS 로그인이나 외부 인증 제공자를 통한 인증 과정에서 발급받은 토큰은 사용자의 신원을 확인하는 데 사용될 수 있다.
- 이 토큰은 JWT 클레임에 포함시켜 클라이언트를 식별하는 데 사용할 수 있다.

6. Custom 클라이언트 ID를 이용하는 방법

- 애플리케이션 내에서 사용자 또는 디바이스에 고유하게 할당된 식별자(ID)를 사용할 수 있다.
- 이는 API 키나 애플리케이션에서 생성한 고유한 사용자 ID일 수 있으며, 애플리케이션 마다 고유한 식별정보들이 많이 있기 때문에 이를 활용하는 방법이다.

7. 지리적 위치 정보를 이용하는 방법

- 클라이언트의 지리적 위치정보(예: 국가, 도시, 지역 코드)는 IP 주소나 GPS 정보를 통해 얻을 수 있다.
- 사용자의 위치를 기반으로 한 식별에 사용될 수 있다.

8. 시간대(Time Zone)를 이용하는 방법

- 클라이언트의 시간대 정보는 사용자의 지리적 위치와 연관된 식별 정보로 사용될 수 있다.
- 이 식별자는 어떤 경우에는 보안 검증이나 사용자 경험 개선에 활용될 수 있다.

<> 예제코드 - 클라이언트의 IP 주소를 JWT의 클레임에 포함하는 방법

- 아래의 Python 예제코드는 클라이언트의 IP주소를 이용하는 방법으로써, Pyjwt 라이브러리와 Flask 웹 프레임워크를 이용한 예제코드이다.
- '/create_token'는 사용자 ID를 받아 JWT를 생성하고, /verify_token 엔드포인트는 제공된 토큰을 검증한다.
- 클라이언트의 IP 주소는 토큰 생성 시 포함되며, 토큰 검증 시에도 이 IP 주소가 일치하는지 확인한다.

 import jwt
 import datetime
 from flask import Flask, request, jsonify
 
 app = Flask(__name__)
 
 # 비밀키 설정 - 실제 환경에서는 보다 안전하게 관리해야함
 SECRET_KEY = 'your_secret_key'
 
 # JWT 생성 함수
 def create_token(user_id, client_ip):
     try:
         # 현재 시간 기준으로 토큰의 만료 시간을 설정
         expiration_time = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
         # JWT 페이로드에 사용자 ID와 클라이언트 IP, 만료 시간을 포함
         payload = {
             'user_id': user_id,
             'ip': client_ip,
             'exp': expiration_time
         }
         # JWT 생성
         token = jwt.encode(payload, SECRET_KEY, algorithm='RS256')
         return token
     except Exception as e:
         # 오류 발생 시 처리
         print(f"Token creation failed: {e}")
         return None
          
 # JWT 검증 함수
 def verify_token(token, client_ip):
     try: 
         # 토큰 디코드
         payload = jwt.decode(token, SECRET_KEY, algorithms=['RS256'])
         # 클라이언트 IP 주소 검증
         if payload['ip'] != client_ip:
             raise jwt.InvalidTokenError("IP address mismatch")
         return payload
     except jwt.ExpiredSignatureError:
         # 토큰 만료 오류 처리
         print("Token has expired")
         return None
     except jwt.InvalidTokenError as e:
         # 기타 JWT 오류 처리
         print(f"Invalid token: {e}")
         return None
          
 @app.route('/create_token', methods=['POST'])
 def create_token_route():
     user_id = request.json.get('user_id')
     client_ip = request.remote_addr  # 클라이언트의 IP 주소를 Flask를 통해 얻음
     token = create_token(user_id, client_ip)
     if token:
         return jsonify({'token': token}), 200
     else:
         return jsonify({'error': 'Token creation failed'}), 500
          
 @app.route('/verify_token', methods=['POST'])
 def verify_token_route():
     token = request.json.get('token')
     client_ip = request.remote_addr  # 클라이언트의 IP 주소를 다시 확인
     payload = verify_token(token, client_ip)
     if payload:
         return jsonify({'payload': payload}), 200
     else:
         return jsonify({'error': 'Token verification failed'}), 500

 if __name__ == '__main__':
     app.r0un(debug=True)


<> 예제코드 - 클라이언트 디바이스 지문을 활용하는 방법

- 아래의 Python 예제코드는 사용자 ID와 디바이스 지문, 만료 시간을 활용하여 클라이언트를 식별하는 예제코드이다.

from flask import Flask, request, jsonify
import jwt
import datetime
 
app = Flask(__name__)

 # 비밀키 설정 - 실제 환경에서는 보다 안전하게 관리해야 함
SECRET_KEY = 'your_secret_key'

# JWT 생성 함수
def create_token(user_id, device_fingerprint):
    """
    JWT를 생성하는 함수
    user_id: 사용자 식별자
    device_fingerprint: 클라이언트 디바이스 지문
    """
    try:
        # 현재 시간 기준으로 토큰의 만료 시간을 설정
        expiration_time = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
        # JWT 페이로드에 사용자 ID와 디바이스 지문, 만료 시간을 포함
        payload = {
            'user_id': user_id,
            'device_fingerprint': device_fingerprint,
            'exp': expiration_time
        }
        # JWT 생성
        token = jwt.encode(payload, SECRET_KEY, algorithm='RS256').decode('utf-8')
        return token
    except Exception as e:
        # 오류 발생 시 처리
        print(f"Token creation failed: {e}")
        return None

# JWT 검증 함수
def verify_token(token, device_fingerprint):
    """
    JWT를 검증하는 함수
    token: 검증할 JWT
    device_fingerprint: 클라이언트 디바이스 지문
    """
    try:
        # 토큰 디코드
        payload = jwt.decode(token, SECRET_KEY, algorithms=['RS256'])
        # 디바이스 지문 검증
        if payload['device_fingerprint'] != device_fingerprint:
            raise jwt.InvalidTokenError("Device fingerprint mismatch")
        return payload
    except jwt.ExpiredSignatureError:
        # 토큰 만료 오류 처리
        print("Token has expired")
        return None
    except jwt.InvalidTokenError as e:
        # 기타 JWT 오류 처리
        print(f"Invalid token: {e}")
        return None

@app.route('/create_token', methods=['POST'])
def create_token_route():
    """
    토큰 생성 요청을 처리하는 라우트
    """
    user_id = request.json.get('user_id')
    device_fingerprint = request.json.get('device_fingerprint')  # 클라이언트의 디바이스 지문을 요청에서 가져옴
    token = create_token(user_id, device_fingerprint)
    if token:
        return jsonify({'token': token}), 200
    else:
        return jsonify({'error': 'Token creation failed'}), 500

@app.route('/verify_token', methods=['POST'])
def verify_token_route():
    """
    토큰 검증 요청을 처리하는 라우트
    """
    token = request.json.get('token')
    device_fingerprint = request.json.get('device_fingerprint')  # 검증 시 사용할 디바이스 지문을 요청에서 가져옴
    payload = verify_token(token, device_fingerprint)
    if payload:
        return jsonify({'payload': payload}), 200
    else:
        return jsonify({'error': 'Token verification failed'}), 500

if __name__ == '__main__':
    app.run(debug=True)