Add comprehensive remote authentication system allowing users to log in from one device by authenticating from another trusted device. Features include: - Proof of Work (PoW) protection using PBKDF2-SHA512 to prevent abuse - Simple pairing codes (3 words) protected by dynamic PoW difficulty - Autocomplete pairing code input with error checking - Real-time WebSocket communication between devices Unlike device addition links and reset links with QR codes that only allow adding an authentication method, and that work offline over the duration of several days, this mechanism is strictly online, with 5 minute time limit.
77 lines
2.3 KiB
Python
77 lines
2.3 KiB
Python
"""Utilities for determining the auth UI host and base URLs."""
|
|
|
|
import json
|
|
import os
|
|
from functools import lru_cache
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _load_config() -> dict:
|
|
"""Load PASKIA_CONFIG JSON."""
|
|
config_json = os.getenv("PASKIA_CONFIG")
|
|
if not config_json:
|
|
return {}
|
|
return json.loads(config_json)
|
|
|
|
|
|
def is_root_mode() -> bool:
|
|
return _load_config().get("auth_host") is not None
|
|
|
|
|
|
def dedicated_auth_host() -> str | None:
|
|
"""Return configured auth_host netloc, or None."""
|
|
auth_host = _load_config().get("auth_host")
|
|
if not auth_host:
|
|
return None
|
|
from urllib.parse import urlparse
|
|
|
|
parsed = urlparse(auth_host if "://" in auth_host else f"//{auth_host}")
|
|
return parsed.netloc or parsed.path or None
|
|
|
|
|
|
def ui_base_path() -> str:
|
|
return "/" if is_root_mode() else "/auth/"
|
|
|
|
|
|
def auth_site_url() -> str:
|
|
"""Return the base URL for the auth site UI (computed at startup)."""
|
|
cfg = _load_config()
|
|
return cfg.get("site_url", "https://localhost") + cfg.get("site_path", "/auth/")
|
|
|
|
|
|
def reset_link_url(token: str) -> str:
|
|
"""Generate a reset link URL for the given token."""
|
|
return f"{auth_site_url()}{token}"
|
|
|
|
|
|
def normalize_origin(origin: str) -> str:
|
|
"""Normalize an origin URL by adding https:// if no scheme is present."""
|
|
if "://" not in origin:
|
|
return f"https://{origin}"
|
|
return origin
|
|
|
|
|
|
def reload_config() -> None:
|
|
_load_config.cache_clear()
|
|
|
|
|
|
def normalize_host(raw_host: str | None) -> str | None:
|
|
"""Normalize a Host header preserving port (exact match required)."""
|
|
if not raw_host:
|
|
return None
|
|
candidate = raw_host.strip()
|
|
if not candidate:
|
|
return None
|
|
# urlsplit to parse (add // for scheme-less); prefer netloc to retain port.
|
|
parsed = urlsplit(candidate if "//" in candidate else f"//{candidate}")
|
|
netloc = parsed.netloc or parsed.path or ""
|
|
# Strip IPv6 brackets around host part but retain port suffix.
|
|
if netloc.startswith("["):
|
|
# format: [ipv6]:port or [ipv6]
|
|
if "]" in netloc:
|
|
host_part, _, rest = netloc.partition("]")
|
|
port_part = rest.lstrip(":")
|
|
netloc = host_part.strip("[]") + (f":{port_part}" if port_part else "")
|
|
return netloc.lower() or None
|