Coverage for fastblocks/adapters/auth/basic.py: 0%
70 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -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 Inject, 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):
53 pass
56class CurrentUser:
57 def has_role(self, _: str) -> str:
58 raise NotImplementedError
60 def set_role(self, _: str) -> str | bool | None:
61 raise NotImplementedError
63 @property
64 def identity(self) -> UUID4 | str | int:
65 raise NotImplementedError
67 @property
68 def display_name(self) -> str:
69 raise NotImplementedError
71 @property
72 def email(self) -> EmailStr | None:
73 raise NotImplementedError
75 def is_authenticated(
76 self,
77 request: HtmxRequest | None = None,
78 config: t.Any = None,
79 ) -> bool | int | str:
80 raise NotImplementedError
83class Auth(AuthBase):
84 secret_key: SecretStr
86 @staticmethod
87 async def authenticate(request: HtmxRequest | Request) -> bool:
88 headers = getattr(request, "headers", {})
89 if "Authorization" not in headers:
90 return False
91 auth = headers["Authorization"]
92 try:
93 scheme, credentials = auth.split()
94 if scheme.lower() != "basic":
95 return False
96 decoded = base64.b64decode(credentials).decode("ascii")
97 except (ValueError, UnicodeDecodeError, binascii.Error):
98 msg = "Invalid basic auth credentials"
99 raise AuthenticationError(msg)
100 username, _, _ = decoded.partition(":")
101 state = getattr(request, "state", None)
102 if state:
103 state.auth_credentials = (
104 AuthCredentials(["authenticated"]),
105 SimpleUser(username),
106 )
107 return True
109 def __init__(
110 self,
111 secret_key: SecretStr | None = None,
112 user_model: t.Any | None = None,
113 ) -> None:
114 if secret_key is None:
115 from acb.config import Config
117 config = Config()
118 config.init()
119 secret_key = config.get("auth.secret_key")
120 if secret_key is None:
121 secret_key = (
122 getattr(self.config.app, "secret_key", None)
123 if hasattr(self, "config")
124 else None
125 )
126 if secret_key is None:
127 raise ValueError(
128 "secret_key must be provided either directly or via config"
129 )
131 super().__init__(secret_key, user_model)
132 self.secret_key = secret_key
133 if not self.secret_key:
134 raise ValueError("secret_key must be provided via config or parameter")
135 self.name = "basic"
136 self.user_model = user_model
138 async def init(self) -> None:
139 self.middlewares = [
140 Middleware(
141 SessionMiddleware,
142 secret_key=self.secret_key.get_secret_value(),
143 session_cookie=f"{self.token_id}_admin",
144 https_only=bool(self.config.deployed),
145 ),
146 ]
148 async def login(self, request: HtmxRequest) -> bool:
149 raise NotImplementedError
151 async def logout(self, request: HtmxRequest) -> bool:
152 raise NotImplementedError
155MODULE_ID = UUID("01937d86-5f3b-7c4d-9e0f-2345678901bc")
156MODULE_STATUS = AdapterStatus.STABLE
158with suppress(Exception):
159 depends.set(Auth)