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

1"""Basic Authentication Adapter for FastBlocks. 

2 

3Provides HTTP Basic Authentication with session management for FastBlocks applications. 

4Includes secure credential validation, session middleware integration, and user model support. 

5 

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 

13 

14Requirements: 

15- starlette>=0.47.1 

16- pydantic>=2.11.7 

17 

18Usage: 

19```python 

20from acb.depends import depends 

21from acb.adapters import import_adapter 

22 

23auth = depends.get("auth") 

24 

25Auth = import_adapter("auth") 

26 

27auth_middleware = await auth.init() 

28``` 

29 

30Author: lesleslie <les@wedgwoodwebworks.com> 

31Created: 2025-01-12 

32""" 

33 

34import base64 

35import binascii 

36import typing as t 

37from contextlib import suppress 

38from uuid import UUID 

39 

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 

48 

49from ._base import AuthBase, AuthBaseSettings 

50 

51 

52class AuthSettings(AuthBaseSettings): ... 

53 

54 

55class CurrentUser: 

56 def has_role(self, _: str) -> str: ... 

57 

58 def set_role(self, _: str) -> str | bool | None: ... 

59 

60 @property 

61 def identity(self) -> UUID4 | str | int: ... 

62 

63 @property 

64 def display_name(self) -> str: ... 

65 

66 @property 

67 def email(self) -> EmailStr | None: ... 

68 

69 def is_authenticated( 

70 self, 

71 request: HtmxRequest | None = None, 

72 config: t.Any = None, 

73 ) -> bool | int | str: ... 

74 

75 

76class Auth(AuthBase): 

77 secret_key: SecretStr 

78 

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 

101 

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 

109 

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 ) 

123 

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 

130 

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 ] 

140 

141 async def login(self, request: HtmxRequest) -> bool: ... 

142 

143 async def logout(self, request: HtmxRequest) -> bool: ... 

144 

145 

146MODULE_ID = UUID("01937d86-5f3b-7c4d-9e0f-2345678901bc") 

147MODULE_STATUS = AdapterStatus.STABLE 

148 

149with suppress(Exception): 

150 depends.set(Auth)