Removed Experimental Engine

This commit is contained in:
2025-10-25 23:37:02 -06:00
parent 05b72f17a6
commit e16746d407
117 changed files with 0 additions and 16887 deletions

View File

@@ -1,63 +0,0 @@
"""Authentication services for the Borealis Engine."""
from __future__ import annotations
from .device_auth_service import DeviceAuthService, DeviceRecord
from .dpop import DPoPReplayError, DPoPVerificationError, DPoPValidator
from .jwt_service import JWTService, load_service as load_jwt_service
from .token_service import (
RefreshTokenRecord,
TokenRefreshError,
TokenRefreshErrorCode,
TokenService,
)
from .operator_account_service import (
AccountNotFoundError,
CannotModifySelfError,
InvalidPasswordHashError,
InvalidRoleError,
LastAdminError,
LastUserError,
OperatorAccountError,
OperatorAccountRecord,
OperatorAccountService,
UsernameAlreadyExistsError,
)
from .operator_auth_service import (
InvalidCredentialsError,
InvalidMFACodeError,
MFAUnavailableError,
MFASessionError,
OperatorAuthError,
OperatorAuthService,
)
__all__ = [
"DeviceAuthService",
"DeviceRecord",
"DPoPReplayError",
"DPoPVerificationError",
"DPoPValidator",
"JWTService",
"load_jwt_service",
"RefreshTokenRecord",
"TokenRefreshError",
"TokenRefreshErrorCode",
"TokenService",
"OperatorAccountService",
"OperatorAccountError",
"OperatorAccountRecord",
"UsernameAlreadyExistsError",
"AccountNotFoundError",
"LastAdminError",
"LastUserError",
"CannotModifySelfError",
"InvalidRoleError",
"InvalidPasswordHashError",
"OperatorAuthService",
"OperatorAuthError",
"InvalidCredentialsError",
"InvalidMFACodeError",
"MFAUnavailableError",
"MFASessionError",
]

View File

@@ -1,237 +0,0 @@
"""Device authentication service copied from the legacy server stack."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Mapping, Optional, Protocol
import logging
from Data.Engine.builders.device_auth import DeviceAuthRequest
from Data.Engine.domain.device_auth import (
AccessTokenClaims,
DeviceAuthContext,
DeviceAuthErrorCode,
DeviceAuthFailure,
DeviceFingerprint,
DeviceGuid,
DeviceIdentity,
DeviceStatus,
)
__all__ = [
"DeviceAuthService",
"DeviceRecord",
"DPoPValidator",
"DPoPVerificationError",
"DPoPReplayError",
"RateLimiter",
"RateLimitDecision",
"DeviceRepository",
]
class RateLimitDecision(Protocol):
allowed: bool
retry_after: Optional[float]
class RateLimiter(Protocol):
def check(self, key: str, max_requests: int, window_seconds: float) -> RateLimitDecision: # pragma: no cover - protocol
...
class JWTDecoder(Protocol):
def decode(self, token: str) -> Mapping[str, object]: # pragma: no cover - protocol
...
class DPoPValidator(Protocol):
def verify(
self,
method: str,
htu: str,
proof: str,
access_token: Optional[str] = None,
) -> str: # pragma: no cover - protocol
...
class DPoPVerificationError(Exception):
"""Raised when a DPoP proof fails validation."""
class DPoPReplayError(DPoPVerificationError):
"""Raised when a DPoP proof is replayed."""
@dataclass(frozen=True, slots=True)
class DeviceRecord:
"""Snapshot of a device record required for authentication."""
identity: DeviceIdentity
token_version: int
status: DeviceStatus
class DeviceRepository(Protocol):
"""Port that exposes the minimal device persistence operations."""
def fetch_by_guid(self, guid: DeviceGuid) -> Optional[DeviceRecord]: # pragma: no cover - protocol
...
def recover_missing(
self,
guid: DeviceGuid,
fingerprint: DeviceFingerprint,
token_version: int,
service_context: Optional[str],
) -> Optional[DeviceRecord]: # pragma: no cover - protocol
...
class DeviceAuthService:
"""Authenticate devices using access tokens, repositories, and DPoP proofs."""
def __init__(
self,
*,
device_repository: DeviceRepository,
jwt_service: JWTDecoder,
logger: Optional[logging.Logger] = None,
rate_limiter: Optional[RateLimiter] = None,
dpop_validator: Optional[DPoPValidator] = None,
) -> None:
self._repository = device_repository
self._jwt = jwt_service
self._log = logger or logging.getLogger("borealis.engine.auth")
self._rate_limiter = rate_limiter
self._dpop_validator = dpop_validator
def authenticate(self, request: DeviceAuthRequest, *, path: str) -> DeviceAuthContext:
"""Authenticate an access token and return the resulting context."""
claims = self._decode_claims(request.access_token)
rate_limit_key = f"fp:{claims.fingerprint.value}"
if self._rate_limiter is not None:
decision = self._rate_limiter.check(rate_limit_key, 60, 60.0)
if not decision.allowed:
raise DeviceAuthFailure(
DeviceAuthErrorCode.RATE_LIMITED,
http_status=429,
retry_after=decision.retry_after,
)
record = self._repository.fetch_by_guid(claims.guid)
if record is None:
record = self._repository.recover_missing(
claims.guid,
claims.fingerprint,
claims.token_version,
request.service_context,
)
if record is None:
raise DeviceAuthFailure(
DeviceAuthErrorCode.DEVICE_NOT_FOUND,
http_status=403,
)
self._validate_identity(record, claims)
dpop_jkt = self._validate_dpop(request, record, claims)
context = DeviceAuthContext(
identity=record.identity,
access_token=request.access_token,
claims=claims,
status=record.status,
service_context=request.service_context,
dpop_jkt=dpop_jkt,
)
if context.is_quarantined:
self._log.warning(
"device %s is quarantined; limited access for %s",
record.identity.guid,
path,
)
return context
def _decode_claims(self, token: str) -> AccessTokenClaims:
try:
raw_claims = self._jwt.decode(token)
except Exception as exc: # pragma: no cover - defensive fallback
if self._is_expired_signature(exc):
raise DeviceAuthFailure(DeviceAuthErrorCode.TOKEN_EXPIRED) from exc
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_TOKEN) from exc
try:
return AccessTokenClaims.from_mapping(raw_claims)
except Exception as exc:
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_CLAIMS) from exc
@staticmethod
def _is_expired_signature(exc: Exception) -> bool:
name = exc.__class__.__name__
return name == "ExpiredSignatureError"
def _validate_identity(
self,
record: DeviceRecord,
claims: AccessTokenClaims,
) -> None:
if record.identity.guid.value != claims.guid.value:
raise DeviceAuthFailure(
DeviceAuthErrorCode.DEVICE_GUID_MISMATCH,
http_status=403,
)
if record.identity.fingerprint.value:
if record.identity.fingerprint.value != claims.fingerprint.value:
raise DeviceAuthFailure(
DeviceAuthErrorCode.FINGERPRINT_MISMATCH,
http_status=403,
)
if record.token_version > claims.token_version:
raise DeviceAuthFailure(DeviceAuthErrorCode.TOKEN_VERSION_REVOKED)
if not record.status.allows_access:
raise DeviceAuthFailure(
DeviceAuthErrorCode.DEVICE_REVOKED,
http_status=403,
)
def _validate_dpop(
self,
request: DeviceAuthRequest,
record: DeviceRecord,
claims: AccessTokenClaims,
) -> Optional[str]:
if not request.dpop_proof:
return None
if self._dpop_validator is None:
raise DeviceAuthFailure(
DeviceAuthErrorCode.DPOP_NOT_SUPPORTED,
http_status=400,
)
try:
return self._dpop_validator.verify(
request.http_method,
request.htu,
request.dpop_proof,
request.access_token,
)
except DPoPReplayError as exc:
raise DeviceAuthFailure(
DeviceAuthErrorCode.DPOP_REPLAYED,
http_status=400,
) from exc
except DPoPVerificationError as exc:
raise DeviceAuthFailure(
DeviceAuthErrorCode.DPOP_INVALID,
http_status=400,
) from exc

View File

@@ -1,105 +0,0 @@
"""DPoP proof validation for Engine services."""
from __future__ import annotations
import hashlib
import time
from threading import Lock
from typing import Dict, Optional
import jwt
__all__ = ["DPoPValidator", "DPoPVerificationError", "DPoPReplayError"]
_DP0P_MAX_SKEW = 300.0
class DPoPVerificationError(Exception):
pass
class DPoPReplayError(DPoPVerificationError):
pass
class DPoPValidator:
def __init__(self) -> None:
self._observed_jti: Dict[str, float] = {}
self._lock = Lock()
def verify(
self,
method: str,
htu: str,
proof: str,
access_token: Optional[str] = None,
) -> str:
if not proof:
raise DPoPVerificationError("DPoP proof missing")
try:
header = jwt.get_unverified_header(proof)
except Exception as exc:
raise DPoPVerificationError("invalid DPoP header") from exc
jwk = header.get("jwk")
alg = header.get("alg")
if not jwk or not isinstance(jwk, dict):
raise DPoPVerificationError("missing jwk in DPoP header")
if alg not in ("EdDSA", "ES256", "ES384", "ES512"):
raise DPoPVerificationError(f"unsupported DPoP alg {alg}")
try:
key = jwt.PyJWK(jwk)
public_key = key.key
except Exception as exc:
raise DPoPVerificationError("invalid jwk in DPoP header") from exc
try:
claims = jwt.decode(
proof,
public_key,
algorithms=[alg],
options={"require": ["htm", "htu", "jti", "iat"]},
)
except Exception as exc:
raise DPoPVerificationError("invalid DPoP signature") from exc
htm = claims.get("htm")
proof_htu = claims.get("htu")
jti = claims.get("jti")
iat = claims.get("iat")
ath = claims.get("ath")
if not isinstance(htm, str) or htm.lower() != method.lower():
raise DPoPVerificationError("DPoP htm mismatch")
if not isinstance(proof_htu, str) or proof_htu != htu:
raise DPoPVerificationError("DPoP htu mismatch")
if not isinstance(jti, str):
raise DPoPVerificationError("DPoP jti missing")
if not isinstance(iat, (int, float)):
raise DPoPVerificationError("DPoP iat missing")
now = time.time()
if abs(now - float(iat)) > _DP0P_MAX_SKEW:
raise DPoPVerificationError("DPoP proof outside allowed skew")
if ath and access_token:
expected_ath = jwt.utils.base64url_encode(
hashlib.sha256(access_token.encode("utf-8")).digest()
).decode("ascii")
if expected_ath != ath:
raise DPoPVerificationError("DPoP ath mismatch")
with self._lock:
expiry = self._observed_jti.get(jti)
if expiry and expiry > now:
raise DPoPReplayError("DPoP proof replay detected")
self._observed_jti[jti] = now + _DP0P_MAX_SKEW
stale = [key for key, exp in self._observed_jti.items() if exp <= now]
for key in stale:
self._observed_jti.pop(key, None)
thumbprint = jwt.PyJWK(jwk).thumbprint()
return thumbprint.decode("ascii")

View File

@@ -1,124 +0,0 @@
"""JWT issuance utilities for the Engine."""
from __future__ import annotations
import hashlib
import time
from typing import Any, Dict, Optional
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
from Data.Engine.runtime import ensure_runtime_dir, runtime_path
__all__ = ["JWTService", "load_service"]
_KEY_DIR = runtime_path("auth_keys")
_KEY_FILE = _KEY_DIR / "engine-jwt-ed25519.key"
_LEGACY_KEY_FILE = runtime_path("keys") / "borealis-jwt-ed25519.key"
class JWTService:
def __init__(self, private_key: ed25519.Ed25519PrivateKey, key_id: str) -> None:
self._private_key = private_key
self._public_key = private_key.public_key()
self._key_id = key_id
@property
def key_id(self) -> str:
return self._key_id
def issue_access_token(
self,
guid: str,
ssl_key_fingerprint: str,
token_version: int,
expires_in: int = 900,
extra_claims: Optional[Dict[str, Any]] = None,
) -> str:
now = int(time.time())
payload: Dict[str, Any] = {
"sub": f"device:{guid}",
"guid": guid,
"ssl_key_fingerprint": ssl_key_fingerprint,
"token_version": int(token_version),
"iat": now,
"nbf": now,
"exp": now + int(expires_in),
}
if extra_claims:
payload.update(extra_claims)
token = jwt.encode(
payload,
self._private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
),
algorithm="EdDSA",
headers={"kid": self._key_id},
)
return token
def decode(self, token: str, *, audience: Optional[str] = None) -> Dict[str, Any]:
options = {"require": ["exp", "iat", "sub"]}
public_pem = self._public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
return jwt.decode(
token,
public_pem,
algorithms=["EdDSA"],
audience=audience,
options=options,
)
def public_jwk(self) -> Dict[str, Any]:
public_bytes = self._public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
jwk_x = jwt.utils.base64url_encode(public_bytes).decode("ascii")
return {"kty": "OKP", "crv": "Ed25519", "kid": self._key_id, "alg": "EdDSA", "use": "sig", "x": jwk_x}
def load_service() -> JWTService:
private_key = _load_or_create_private_key()
public_bytes = private_key.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
key_id = hashlib.sha256(public_bytes).hexdigest()[:16]
return JWTService(private_key, key_id)
def _load_or_create_private_key() -> ed25519.Ed25519PrivateKey:
ensure_runtime_dir("auth_keys")
if _KEY_FILE.exists():
with _KEY_FILE.open("rb") as fh:
return serialization.load_pem_private_key(fh.read(), password=None)
if _LEGACY_KEY_FILE.exists():
with _LEGACY_KEY_FILE.open("rb") as fh:
return serialization.load_pem_private_key(fh.read(), password=None)
private_key = ed25519.Ed25519PrivateKey.generate()
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
_KEY_DIR.mkdir(parents=True, exist_ok=True)
with _KEY_FILE.open("wb") as fh:
fh.write(pem)
try:
if hasattr(_KEY_FILE, "chmod"):
_KEY_FILE.chmod(0o600)
except Exception:
pass
return private_key

View File

@@ -1,211 +0,0 @@
"""Operator account management service."""
from __future__ import annotations
import logging
import time
from dataclasses import dataclass
from typing import Optional
from Data.Engine.domain import OperatorAccount
from Data.Engine.repositories.sqlite.user_repository import SQLiteUserRepository
class OperatorAccountError(Exception):
"""Base class for operator account management failures."""
class UsernameAlreadyExistsError(OperatorAccountError):
"""Raised when attempting to create an operator with a duplicate username."""
class AccountNotFoundError(OperatorAccountError):
"""Raised when the requested operator account cannot be located."""
class LastAdminError(OperatorAccountError):
"""Raised when attempting to demote or delete the last remaining admin."""
class LastUserError(OperatorAccountError):
"""Raised when attempting to delete the final operator account."""
class CannotModifySelfError(OperatorAccountError):
"""Raised when the caller attempts to delete themselves."""
class InvalidRoleError(OperatorAccountError):
"""Raised when a role value is invalid."""
class InvalidPasswordHashError(OperatorAccountError):
"""Raised when a password hash is malformed."""
@dataclass(frozen=True, slots=True)
class OperatorAccountRecord:
username: str
display_name: str
role: str
last_login: int
created_at: int
updated_at: int
mfa_enabled: bool
class OperatorAccountService:
"""High-level operations for managing operator accounts."""
def __init__(
self,
repository: SQLiteUserRepository,
*,
logger: Optional[logging.Logger] = None,
) -> None:
self._repository = repository
self._log = logger or logging.getLogger("borealis.engine.services.operator_accounts")
def list_accounts(self) -> list[OperatorAccountRecord]:
return [_to_record(account) for account in self._repository.list_accounts()]
def create_account(
self,
*,
username: str,
password_sha512: str,
role: str,
display_name: Optional[str] = None,
) -> OperatorAccountRecord:
normalized_role = self._normalize_role(role)
username = (username or "").strip()
password_sha512 = (password_sha512 or "").strip().lower()
display_name = (display_name or username or "").strip()
if not username or not password_sha512:
raise InvalidPasswordHashError("username and password are required")
if len(password_sha512) != 128:
raise InvalidPasswordHashError("password hash must be 128 hex characters")
now = int(time.time())
try:
self._repository.create_account(
username=username,
display_name=display_name or username,
password_sha512=password_sha512,
role=normalized_role,
timestamp=now,
)
except Exception as exc: # pragma: no cover - sqlite integrity errors are deterministic
import sqlite3
if isinstance(exc, sqlite3.IntegrityError):
raise UsernameAlreadyExistsError("username already exists") from exc
raise
account = self._repository.fetch_by_username(username)
if not account: # pragma: no cover - sanity guard
raise AccountNotFoundError("account creation failed")
return _to_record(account)
def delete_account(self, username: str, *, actor: Optional[str] = None) -> None:
username = (username or "").strip()
if not username:
raise AccountNotFoundError("invalid username")
if actor and actor.strip().lower() == username.lower():
raise CannotModifySelfError("cannot delete yourself")
total_accounts = self._repository.count_accounts()
if total_accounts <= 1:
raise LastUserError("cannot delete the last user")
target = self._repository.fetch_by_username(username)
if not target:
raise AccountNotFoundError("user not found")
if target.role.lower() == "admin" and self._repository.count_admins() <= 1:
raise LastAdminError("cannot delete the last admin")
if not self._repository.delete_account(username):
raise AccountNotFoundError("user not found")
def reset_password(self, username: str, password_sha512: str) -> None:
username = (username or "").strip()
password_sha512 = (password_sha512 or "").strip().lower()
if len(password_sha512) != 128:
raise InvalidPasswordHashError("invalid password hash")
now = int(time.time())
if not self._repository.update_password(username, password_sha512, timestamp=now):
raise AccountNotFoundError("user not found")
def change_role(self, username: str, role: str, *, actor: Optional[str] = None) -> OperatorAccountRecord:
username = (username or "").strip()
normalized_role = self._normalize_role(role)
account = self._repository.fetch_by_username(username)
if not account:
raise AccountNotFoundError("user not found")
if account.role.lower() == "admin" and normalized_role.lower() != "admin":
if self._repository.count_admins() <= 1:
raise LastAdminError("cannot demote the last admin")
now = int(time.time())
if not self._repository.update_role(username, normalized_role, timestamp=now):
raise AccountNotFoundError("user not found")
updated = self._repository.fetch_by_username(username)
if not updated: # pragma: no cover - guard
raise AccountNotFoundError("user not found")
record = _to_record(updated)
if actor and actor.strip().lower() == username.lower():
self._log.info("actor-role-updated", extra={"username": username, "role": record.role})
return record
def update_mfa(self, username: str, *, enabled: bool, reset_secret: bool) -> None:
username = (username or "").strip()
if not username:
raise AccountNotFoundError("invalid username")
now = int(time.time())
if not self._repository.update_mfa(username, enabled=enabled, reset_secret=reset_secret, timestamp=now):
raise AccountNotFoundError("user not found")
def fetch_account(self, username: str) -> Optional[OperatorAccountRecord]:
account = self._repository.fetch_by_username(username)
return _to_record(account) if account else None
def _normalize_role(self, role: str) -> str:
normalized = (role or "").strip().title() or "User"
if normalized not in {"User", "Admin"}:
raise InvalidRoleError("invalid role")
return normalized
def _to_record(account: OperatorAccount) -> OperatorAccountRecord:
return OperatorAccountRecord(
username=account.username,
display_name=account.display_name or account.username,
role=account.role or "User",
last_login=int(account.last_login or 0),
created_at=int(account.created_at or 0),
updated_at=int(account.updated_at or 0),
mfa_enabled=bool(account.mfa_enabled),
)
__all__ = [
"OperatorAccountService",
"OperatorAccountError",
"UsernameAlreadyExistsError",
"AccountNotFoundError",
"LastAdminError",
"LastUserError",
"CannotModifySelfError",
"InvalidRoleError",
"InvalidPasswordHashError",
"OperatorAccountRecord",
]

View File

@@ -1,236 +0,0 @@
"""Operator authentication service."""
from __future__ import annotations
import base64
import io
import logging
import os
import time
import uuid
from typing import Optional
try: # pragma: no cover - optional dependencies mirror legacy server behaviour
import pyotp # type: ignore
except Exception: # pragma: no cover - gracefully degrade when unavailable
pyotp = None # type: ignore
try: # pragma: no cover - optional dependency
import qrcode # type: ignore
except Exception: # pragma: no cover - gracefully degrade when unavailable
qrcode = None # type: ignore
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from Data.Engine.builders.operator_auth import (
OperatorLoginRequest,
OperatorMFAVerificationRequest,
)
from Data.Engine.domain import (
OperatorAccount,
OperatorLoginSuccess,
OperatorMFAChallenge,
)
from Data.Engine.repositories.sqlite.user_repository import SQLiteUserRepository
class OperatorAuthError(Exception):
"""Base class for operator authentication errors."""
class InvalidCredentialsError(OperatorAuthError):
"""Raised when username/password verification fails."""
class MFAUnavailableError(OperatorAuthError):
"""Raised when MFA functionality is requested but dependencies are missing."""
class InvalidMFACodeError(OperatorAuthError):
"""Raised when the submitted MFA code is invalid."""
class MFASessionError(OperatorAuthError):
"""Raised when the MFA session state cannot be validated."""
class OperatorAuthService:
"""Authenticate operator accounts and manage MFA challenges."""
def __init__(
self,
repository: SQLiteUserRepository,
*,
logger: Optional[logging.Logger] = None,
) -> None:
self._repository = repository
self._log = logger or logging.getLogger("borealis.engine.services.operator_auth")
def authenticate(
self, request: OperatorLoginRequest
) -> OperatorLoginSuccess | OperatorMFAChallenge:
account = self._repository.fetch_by_username(request.username)
if not account:
raise InvalidCredentialsError("invalid username or password")
if not self._password_matches(account, request.password_sha512):
raise InvalidCredentialsError("invalid username or password")
if not account.mfa_enabled:
return self._finalize_login(account)
stage = "verify" if account.mfa_secret else "setup"
return self._build_mfa_challenge(account, stage)
def verify_mfa(
self,
challenge: OperatorMFAChallenge,
request: OperatorMFAVerificationRequest,
) -> OperatorLoginSuccess:
now = int(time.time())
if challenge.pending_token != request.pending_token:
raise MFASessionError("invalid_session")
if challenge.expires_at < now:
raise MFASessionError("expired")
if challenge.stage == "setup":
secret = (challenge.secret or "").strip()
if not secret:
raise MFASessionError("mfa_not_configured")
totp = self._totp_for_secret(secret)
if not totp.verify(request.code, valid_window=1):
raise InvalidMFACodeError("invalid_code")
self._repository.store_mfa_secret(challenge.username, secret, timestamp=now)
else:
account = self._repository.fetch_by_username(challenge.username)
if not account or not account.mfa_secret:
raise MFASessionError("mfa_not_configured")
totp = self._totp_for_secret(account.mfa_secret)
if not totp.verify(request.code, valid_window=1):
raise InvalidMFACodeError("invalid_code")
account = self._repository.fetch_by_username(challenge.username)
if not account:
raise InvalidCredentialsError("invalid username or password")
return self._finalize_login(account)
def issue_token(self, username: str, role: str) -> str:
serializer = self._token_serializer()
payload = {"u": username, "r": role or "User", "ts": int(time.time())}
return serializer.dumps(payload)
def resolve_token(self, token: str, *, max_age: int = 30 * 24 * 3600) -> Optional[OperatorAccount]:
"""Return the account associated with *token* if it is valid."""
token = (token or "").strip()
if not token:
return None
serializer = self._token_serializer()
try:
payload = serializer.loads(token, max_age=max_age)
except (BadSignature, SignatureExpired):
return None
username = str(payload.get("u") or "").strip()
if not username:
return None
return self._repository.fetch_by_username(username)
def fetch_account(self, username: str) -> Optional[OperatorAccount]:
"""Return the operator account for *username* if it exists."""
username = (username or "").strip()
if not username:
return None
return self._repository.fetch_by_username(username)
def _finalize_login(self, account: OperatorAccount) -> OperatorLoginSuccess:
now = int(time.time())
self._repository.update_last_login(account.username, now)
token = self.issue_token(account.username, account.role)
return OperatorLoginSuccess(username=account.username, role=account.role, token=token)
def _password_matches(self, account: OperatorAccount, provided_hash: str) -> bool:
expected = (account.password_sha512 or "").strip().lower()
candidate = (provided_hash or "").strip().lower()
return bool(expected and candidate and expected == candidate)
def _build_mfa_challenge(
self,
account: OperatorAccount,
stage: str,
) -> OperatorMFAChallenge:
now = int(time.time())
pending_token = uuid.uuid4().hex
secret = None
otpauth_url = None
qr_image = None
if stage == "setup":
secret = self._generate_totp_secret()
otpauth_url = self._totp_provisioning_uri(secret, account.username)
qr_image = self._totp_qr_data_uri(otpauth_url) if otpauth_url else None
return OperatorMFAChallenge(
username=account.username,
role=account.role,
stage="verify" if stage == "verify" else "setup",
pending_token=pending_token,
expires_at=now + 300,
secret=secret,
otpauth_url=otpauth_url,
qr_image=qr_image,
)
def _token_serializer(self) -> URLSafeTimedSerializer:
secret = os.getenv("BOREALIS_FLASK_SECRET_KEY") or "change-me"
return URLSafeTimedSerializer(secret, salt="borealis-auth")
def _generate_totp_secret(self) -> str:
if not pyotp:
raise MFAUnavailableError("pyotp is not installed; MFA unavailable")
return pyotp.random_base32() # type: ignore[no-any-return]
def _totp_for_secret(self, secret: str):
if not pyotp:
raise MFAUnavailableError("pyotp is not installed; MFA unavailable")
normalized = secret.replace(" ", "").strip().upper()
if not normalized:
raise MFASessionError("mfa_not_configured")
return pyotp.TOTP(normalized, digits=6, interval=30)
def _totp_provisioning_uri(self, secret: str, username: str) -> Optional[str]:
try:
totp = self._totp_for_secret(secret)
except OperatorAuthError:
return None
issuer = os.getenv("BOREALIS_MFA_ISSUER", "Borealis")
try:
return totp.provisioning_uri(name=username, issuer_name=issuer)
except Exception: # pragma: no cover - defensive
return None
def _totp_qr_data_uri(self, payload: str) -> Optional[str]:
if not payload or qrcode is None:
return None
try:
img = qrcode.make(payload, box_size=6, border=4)
buf = io.BytesIO()
img.save(buf, format="PNG")
encoded = base64.b64encode(buf.getvalue()).decode("ascii")
return f"data:image/png;base64,{encoded}"
except Exception: # pragma: no cover - defensive
self._log.warning("failed to generate MFA QR code", exc_info=True)
return None
__all__ = [
"OperatorAuthService",
"OperatorAuthError",
"InvalidCredentialsError",
"MFAUnavailableError",
"InvalidMFACodeError",
"MFASessionError",
]

View File

@@ -1,190 +0,0 @@
"""Token refresh service extracted from the legacy blueprint."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional, Protocol
import hashlib
import logging
from Data.Engine.builders.device_auth import RefreshTokenRequest
from Data.Engine.domain.device_auth import DeviceGuid
from .device_auth_service import (
DeviceRecord,
DeviceRepository,
DPoPReplayError,
DPoPVerificationError,
DPoPValidator,
)
__all__ = ["RefreshTokenRecord", "TokenService", "TokenRefreshError", "TokenRefreshErrorCode"]
class JWTIssuer(Protocol):
def issue_access_token(self, guid: str, fingerprint: str, token_version: int) -> str: # pragma: no cover - protocol
...
class TokenRefreshErrorCode(str):
INVALID_REFRESH_TOKEN = "invalid_refresh_token"
REFRESH_TOKEN_REVOKED = "refresh_token_revoked"
REFRESH_TOKEN_EXPIRED = "refresh_token_expired"
DEVICE_NOT_FOUND = "device_not_found"
DEVICE_REVOKED = "device_revoked"
DPOP_REPLAYED = "dpop_replayed"
DPOP_INVALID = "dpop_invalid"
class TokenRefreshError(Exception):
def __init__(self, code: str, *, http_status: int = 400) -> None:
self.code = code
self.http_status = http_status
super().__init__(code)
def to_dict(self) -> dict[str, str]:
return {"error": self.code}
@dataclass(frozen=True, slots=True)
class RefreshTokenRecord:
record_id: str
guid: DeviceGuid
token_hash: str
dpop_jkt: Optional[str]
created_at: datetime
expires_at: Optional[datetime]
revoked_at: Optional[datetime]
@classmethod
def from_row(
cls,
*,
record_id: str,
guid: DeviceGuid,
token_hash: str,
dpop_jkt: Optional[str],
created_at: datetime,
expires_at: Optional[datetime],
revoked_at: Optional[datetime],
) -> "RefreshTokenRecord":
return cls(
record_id=record_id,
guid=guid,
token_hash=token_hash,
dpop_jkt=dpop_jkt,
created_at=created_at,
expires_at=expires_at,
revoked_at=revoked_at,
)
class RefreshTokenRepository(Protocol):
def fetch(self, guid: DeviceGuid, token_hash: str) -> Optional[RefreshTokenRecord]: # pragma: no cover - protocol
...
def clear_dpop_binding(self, record_id: str) -> None: # pragma: no cover - protocol
...
def touch(self, record_id: str, *, last_used_at: datetime, dpop_jkt: Optional[str]) -> None: # pragma: no cover - protocol
...
@dataclass(frozen=True, slots=True)
class AccessTokenResponse:
access_token: str
expires_in: int
token_type: str
class TokenService:
def __init__(
self,
*,
refresh_token_repository: RefreshTokenRepository,
device_repository: DeviceRepository,
jwt_service: JWTIssuer,
dpop_validator: Optional[DPoPValidator] = None,
logger: Optional[logging.Logger] = None,
) -> None:
self._refresh_tokens = refresh_token_repository
self._devices = device_repository
self._jwt = jwt_service
self._dpop_validator = dpop_validator
self._log = logger or logging.getLogger("borealis.engine.auth")
def refresh_access_token(
self,
request: RefreshTokenRequest,
) -> AccessTokenResponse:
record = self._refresh_tokens.fetch(
request.guid,
self._hash_token(request.refresh_token),
)
if record is None:
raise TokenRefreshError(TokenRefreshErrorCode.INVALID_REFRESH_TOKEN, http_status=401)
if record.guid.value != request.guid.value:
raise TokenRefreshError(TokenRefreshErrorCode.INVALID_REFRESH_TOKEN, http_status=401)
if record.revoked_at is not None:
raise TokenRefreshError(TokenRefreshErrorCode.REFRESH_TOKEN_REVOKED, http_status=401)
if record.expires_at is not None and record.expires_at <= self._now():
raise TokenRefreshError(TokenRefreshErrorCode.REFRESH_TOKEN_EXPIRED, http_status=401)
device = self._devices.fetch_by_guid(request.guid)
if device is None:
raise TokenRefreshError(TokenRefreshErrorCode.DEVICE_NOT_FOUND, http_status=404)
if not device.status.allows_access:
raise TokenRefreshError(TokenRefreshErrorCode.DEVICE_REVOKED, http_status=403)
dpop_jkt = record.dpop_jkt or ""
if request.dpop_proof:
if self._dpop_validator is None:
raise TokenRefreshError(TokenRefreshErrorCode.DPOP_INVALID)
try:
dpop_jkt = self._dpop_validator.verify(
request.http_method,
request.htu,
request.dpop_proof,
None,
)
except DPoPReplayError as exc:
raise TokenRefreshError(TokenRefreshErrorCode.DPOP_REPLAYED) from exc
except DPoPVerificationError as exc:
raise TokenRefreshError(TokenRefreshErrorCode.DPOP_INVALID) from exc
elif record.dpop_jkt:
self._log.warning(
"Clearing stored DPoP binding for guid=%s due to missing proof",
request.guid.value,
)
self._refresh_tokens.clear_dpop_binding(record.record_id)
access_token = self._jwt.issue_access_token(
request.guid.value,
device.identity.fingerprint.value,
max(device.token_version, 1),
)
self._refresh_tokens.touch(
record.record_id,
last_used_at=self._now(),
dpop_jkt=dpop_jkt or None,
)
return AccessTokenResponse(
access_token=access_token,
expires_in=900,
token_type="Bearer",
)
@staticmethod
def _hash_token(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()
@staticmethod
def _now() -> datetime:
return datetime.now(tz=timezone.utc)