Files
boc/iom/quixzoom-auth-service/utils/tokens.py
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
2026-07-07 07:11:50 +00:00

133 lines
4.1 KiB
Python

"""
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()