mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-06 04:13:48 +00:00
137 lines
3.9 KiB
Python
137 lines
3.9 KiB
Python
"""
|
|
Simple JWT Implementation
|
|
A minimal JWT implementation using only Python standard library.
|
|
Supports HS256 algorithm without requiring cryptography or PyJWT.
|
|
This ensures compatibility across all Python versions and systems.
|
|
"""
|
|
|
|
import hmac
|
|
import hashlib
|
|
import base64
|
|
import json
|
|
import time
|
|
from typing import Optional, Dict, Any
|
|
|
|
|
|
class ExpiredSignatureError(Exception):
|
|
"""Token has expired"""
|
|
pass
|
|
|
|
|
|
class InvalidTokenError(Exception):
|
|
"""Token is invalid"""
|
|
pass
|
|
|
|
|
|
def _base64url_encode(data: bytes) -> str:
|
|
"""Encode bytes to base64url string (no padding)"""
|
|
return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')
|
|
|
|
|
|
def _base64url_decode(data: str) -> bytes:
|
|
"""Decode base64url string to bytes"""
|
|
# Add padding if needed
|
|
padding = 4 - len(data) % 4
|
|
if padding != 4:
|
|
data += '=' * padding
|
|
return base64.urlsafe_b64decode(data.encode('utf-8'))
|
|
|
|
|
|
def encode(payload: Dict[str, Any], secret: str, algorithm: str = "HS256") -> str:
|
|
"""
|
|
Encode a payload into a JWT token.
|
|
|
|
Args:
|
|
payload: Dictionary containing the claims
|
|
secret: Secret key for signing
|
|
algorithm: Algorithm to use (only HS256 supported)
|
|
|
|
Returns:
|
|
JWT token string
|
|
"""
|
|
if algorithm != "HS256":
|
|
raise ValueError(f"Algorithm {algorithm} not supported. Only HS256 is available.")
|
|
|
|
# Header
|
|
header = {"typ": "JWT", "alg": "HS256"}
|
|
header_b64 = _base64url_encode(json.dumps(header, separators=(',', ':')).encode('utf-8'))
|
|
|
|
# Payload
|
|
payload_b64 = _base64url_encode(json.dumps(payload, separators=(',', ':')).encode('utf-8'))
|
|
|
|
# Signature
|
|
message = f"{header_b64}.{payload_b64}"
|
|
signature = hmac.new(
|
|
secret.encode('utf-8'),
|
|
message.encode('utf-8'),
|
|
hashlib.sha256
|
|
).digest()
|
|
signature_b64 = _base64url_encode(signature)
|
|
|
|
return f"{header_b64}.{payload_b64}.{signature_b64}"
|
|
|
|
|
|
def decode(token: str, secret: str, algorithms: list = None) -> Dict[str, Any]:
|
|
"""
|
|
Decode and verify a JWT token.
|
|
|
|
Args:
|
|
token: JWT token string
|
|
secret: Secret key for verification
|
|
algorithms: List of allowed algorithms (ignored, only HS256 supported)
|
|
|
|
Returns:
|
|
Decoded payload dictionary
|
|
|
|
Raises:
|
|
InvalidTokenError: If token is malformed or signature is invalid
|
|
ExpiredSignatureError: If token has expired
|
|
"""
|
|
try:
|
|
parts = token.split('.')
|
|
if len(parts) != 3:
|
|
raise InvalidTokenError("Token must have 3 parts")
|
|
|
|
header_b64, payload_b64, signature_b64 = parts
|
|
|
|
# Verify signature
|
|
message = f"{header_b64}.{payload_b64}"
|
|
expected_signature = hmac.new(
|
|
secret.encode('utf-8'),
|
|
message.encode('utf-8'),
|
|
hashlib.sha256
|
|
).digest()
|
|
|
|
actual_signature = _base64url_decode(signature_b64)
|
|
|
|
if not hmac.compare_digest(expected_signature, actual_signature):
|
|
raise InvalidTokenError("Signature verification failed")
|
|
|
|
# Decode payload
|
|
payload = json.loads(_base64url_decode(payload_b64).decode('utf-8'))
|
|
|
|
# Check expiration
|
|
if 'exp' in payload:
|
|
if time.time() > payload['exp']:
|
|
raise ExpiredSignatureError("Token has expired")
|
|
|
|
return payload
|
|
|
|
except (ValueError, KeyError, json.JSONDecodeError) as e:
|
|
raise InvalidTokenError(f"Invalid token format: {e}")
|
|
|
|
|
|
# Compatibility aliases for PyJWT interface
|
|
class PyJWTCompat:
|
|
"""Compatibility class to mimic PyJWT interface"""
|
|
ExpiredSignatureError = ExpiredSignatureError
|
|
InvalidTokenError = InvalidTokenError
|
|
|
|
@staticmethod
|
|
def encode(payload, secret, algorithm="HS256"):
|
|
return encode(payload, secret, algorithm)
|
|
|
|
@staticmethod
|
|
def decode(token, secret, algorithms=None):
|
|
return decode(token, secret, algorithms)
|