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

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 Inject, 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 pass 

54 

55 

56class CurrentUser: 

57 def has_role(self, _: str) -> str: 

58 raise NotImplementedError 

59 

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

61 raise NotImplementedError 

62 

63 @property 

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

65 raise NotImplementedError 

66 

67 @property 

68 def display_name(self) -> str: 

69 raise NotImplementedError 

70 

71 @property 

72 def email(self) -> EmailStr | None: 

73 raise NotImplementedError 

74 

75 def is_authenticated( 

76 self, 

77 request: HtmxRequest | None = None, 

78 config: t.Any = None, 

79 ) -> bool | int | str: 

80 raise NotImplementedError 

81 

82 

83class Auth(AuthBase): 

84 secret_key: SecretStr 

85 

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 

108 

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 

116 

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 ) 

130 

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 

137 

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 ] 

147 

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

149 raise NotImplementedError 

150 

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

152 raise NotImplementedError 

153 

154 

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

156MODULE_STATUS = AdapterStatus.STABLE 

157 

158with suppress(Exception): 

159 depends.set(Auth)