Coverage for fastblocks/adapters/auth/basic.py: 0%
59 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -0700
1"""Basic Authentication Adapter for FastBlocks.
3Provides HTTP Basic Authentication with session management for FastBlocks applications.
4Includes secure credential validation, session middleware integration, and user model support.
6Features:
7- HTTP Basic Authentication with base64 credential encoding
8- Session-based authentication state management
9- Configurable secret key for session security
10- HTTPS-only cookies in production environments
11- Integration with Starlette authentication middleware
12- Custom user model support for extended user data
14Requirements:
15- starlette>=0.47.1
16- pydantic>=2.11.7
18Usage:
19```python
20from acb.depends import depends
21from acb.adapters import import_adapter
23auth = depends.get("auth")
25Auth = import_adapter("auth")
27auth_middleware = await auth.init()
28```
30Author: lesleslie <les@wedgwoodwebworks.com>
31Created: 2025-01-12
32"""
34import base64
35import binascii
36import typing as t
37from contextlib import suppress
38from uuid import UUID
40from acb.adapters import AdapterStatus
41from acb.depends import depends
42from pydantic import UUID4, EmailStr, SecretStr
43from starlette.authentication import AuthCredentials, AuthenticationError, SimpleUser
44from starlette.middleware import Middleware
45from starlette.middleware.sessions import SessionMiddleware
46from starlette.requests import Request
47from fastblocks.htmx import HtmxRequest
49from ._base import AuthBase, AuthBaseSettings
52class AuthSettings(AuthBaseSettings): ...
55class CurrentUser:
56 def has_role(self, _: str) -> str: ...
58 def set_role(self, _: str) -> str | bool | None: ...
60 @property
61 def identity(self) -> UUID4 | str | int: ...
63 @property
64 def display_name(self) -> str: ...
66 @property
67 def email(self) -> EmailStr | None: ...
69 def is_authenticated(
70 self,
71 request: HtmxRequest | None = None,
72 config: t.Any = None,
73 ) -> bool | int | str: ...
76class Auth(AuthBase):
77 secret_key: SecretStr
79 @staticmethod
80 async def authenticate(request: HtmxRequest | Request) -> bool:
81 headers = getattr(request, "headers", {})
82 if "Authorization" not in headers:
83 return False
84 auth = headers["Authorization"]
85 try:
86 scheme, credentials = auth.split()
87 if scheme.lower() != "basic":
88 return False
89 decoded = base64.b64decode(credentials).decode("ascii")
90 except (ValueError, UnicodeDecodeError, binascii.Error):
91 msg = "Invalid basic auth credentials"
92 raise AuthenticationError(msg)
93 username, _, _ = decoded.partition(":")
94 state = getattr(request, "state", None)
95 if state:
96 state.auth_credentials = (
97 AuthCredentials(["authenticated"]),
98 SimpleUser(username),
99 )
100 return True
102 def __init__(
103 self,
104 secret_key: SecretStr | None = None,
105 user_model: t.Any | None = None,
106 ) -> None:
107 if secret_key is None:
108 from acb.config import Config
110 config = Config()
111 config.init()
112 secret_key = config.get("auth.secret_key")
113 if secret_key is None:
114 secret_key = (
115 getattr(self.config.app, "secret_key", None)
116 if hasattr(self, "config")
117 else None
118 )
119 if secret_key is None:
120 raise ValueError(
121 "secret_key must be provided either directly or via config"
122 )
124 super().__init__(secret_key, user_model)
125 self.secret_key = secret_key
126 if not self.secret_key:
127 raise ValueError("secret_key must be provided via config or parameter")
128 self.name = "basic"
129 self.user_model = user_model
131 async def init(self) -> None:
132 self.middlewares = [
133 Middleware(
134 SessionMiddleware,
135 secret_key=self.secret_key.get_secret_value(),
136 session_cookie=f"{self.token_id}_admin",
137 https_only=bool(self.config.deployed),
138 ),
139 ]
141 async def login(self, request: HtmxRequest) -> bool: ...
143 async def logout(self, request: HtmxRequest) -> bool: ...
146MODULE_ID = UUID("01937d86-5f3b-7c4d-9e0f-2345678901bc")
147MODULE_STATUS = AdapterStatus.STABLE
149with suppress(Exception):
150 depends.set(Auth)