6989a98d75
- 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
133 lines
4.1 KiB
Python
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() |