Files
boc/iom/quixzoom-auth-service/utils/tokens.py
T

133 lines
4.1 KiB
Python
Raw Normal View History

"""
Token generation and validation utilities
"""
import secrets
import string
from datetime import datetime, timedelta
from typing import Optional
class TokenGenerator:
"""
Secure token generator for authentication flows
Generates various types of tokens:
- Request tokens (for auth flow)
- Session tokens
- One-time tokens
- Recovery codes
"""
ALPHANUMERIC = string.ascii_letters + string.digits
URLSAFE = string.ascii_letters + string.digits + "-_"
@classmethod
def generate_request_token(cls, length: int = 32) -> str:
"""Generate secure request token"""
return f"req_{secrets.token_urlsafe(length)}"
@classmethod
def generate_session_token(cls, length: int = 32) -> str:
"""Generate secure session token"""
return f"sess_{secrets.token_urlsafe(length)}"
@classmethod
def generate_refresh_token(cls, length: int = 48) -> str:
"""Generate secure refresh token"""
return f"rt_{secrets.token_urlsafe(length)}"
@classmethod
def generate_nonce(cls, length: int = 32) -> str:
"""Generate cryptographic nonce"""
return secrets.token_urlsafe(length)
@classmethod
def generate_pin(cls, length: int = 6) -> str:
"""Generate numeric PIN code"""
return ''.join(secrets.choice(string.digits) for _ in range(length))
@classmethod
def generate_recovery_code(cls, segments: int = 4, segment_length: int = 4) -> str:
"""Generate human-readable recovery code"""
segments_list = [
''.join(secrets.choice(cls.ALPHANUMERIC) for _ in range(segment_length))
for _ in range(segments)
]
return '-'.join(segments_list).upper()
@classmethod
def generate_api_key(cls, prefix: str = "qk", length: int = 32) -> str:
"""Generate API key with prefix"""
return f"{prefix}_{secrets.token_urlsafe(length)}"
@classmethod
def generate_device_id(cls) -> str:
"""Generate unique device identifier"""
return f"dev_{secrets.token_urlsafe(16)}"
@classmethod
def generate_qr_payload(
cls,
request_token: str,
nonce: str,
expires_at: datetime,
base_url: str = "quixzoom://auth"
) -> str:
"""
Generate QR code payload
Format: quixzoom://auth?token=<token>&nonce=<nonce>&exp=<timestamp>
"""
exp_timestamp = int(expires_at.timestamp())
return f"{base_url}?token={request_token}&nonce={nonce}&exp={exp_timestamp}"
@classmethod
def parse_qr_payload(cls, payload: str) -> Optional[dict]:
"""
Parse QR code payload
Returns:
dict with token, nonce, exp or None if invalid
"""
try:
# Remove scheme
if '://' in payload:
payload = payload.split('://', 1)[1]
# Parse query parameters
if '?' not in payload:
return None
query_string = payload.split('?', 1)[1]
params = {}
for param in query_string.split('&'):
if '=' in param:
key, value = param.split('=', 1)
params[key] = value
return {
"token": params.get("token"),
"nonce": params.get("nonce"),
"exp": int(params.get("exp", 0)) if params.get("exp") else None
}
except Exception:
return None
@classmethod
def is_token_format_valid(cls, token: str, prefix: Optional[str] = None) -> bool:
"""Check if token has valid format"""
if not token or len(token) < 10:
return False
if prefix and not token.startswith(prefix):
return False
# Check for valid characters
valid_chars = set(cls.URLSAFE + '_')
return all(c in valid_chars for c in token.replace(prefix + '_', '') if prefix else token)
# Global token generator
token_generator = TokenGenerator()