belgie 0.1.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- belgie/__init__.py +97 -0
- belgie/alchemy.py +24 -0
- belgie/auth/__init__.py +65 -0
- belgie/auth/core/__init__.py +0 -0
- belgie/auth/core/auth.py +368 -0
- belgie/auth/core/client.py +204 -0
- belgie/auth/core/exceptions.py +26 -0
- belgie/auth/core/hooks.py +87 -0
- belgie/auth/core/settings.py +100 -0
- belgie/auth/providers/__init__.py +10 -0
- belgie/auth/providers/google.py +284 -0
- belgie/auth/providers/protocols.py +43 -0
- belgie/auth/py.typed +0 -0
- belgie/auth/session/__init__.py +3 -0
- belgie/auth/session/manager.py +168 -0
- belgie/auth/utils/__init__.py +9 -0
- belgie/auth/utils/crypto.py +10 -0
- belgie/auth/utils/scopes.py +49 -0
- belgie/mcp.py +12 -0
- belgie/oauth.py +12 -0
- belgie/proto.py +22 -0
- belgie-0.1.0a4.dist-info/METADATA +231 -0
- belgie-0.1.0a4.dist-info/RECORD +24 -0
- belgie-0.1.0a4.dist-info/WHEEL +4 -0
belgie/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Belgie - Modern authentication and analytics for FastAPI."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from belgie.auth import (
|
|
7
|
+
Auth,
|
|
8
|
+
AuthClient,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
AuthorizationError,
|
|
11
|
+
AuthSettings,
|
|
12
|
+
BelgieError,
|
|
13
|
+
ConfigurationError,
|
|
14
|
+
CookieSettings,
|
|
15
|
+
DBConnection,
|
|
16
|
+
GoogleOAuthProvider,
|
|
17
|
+
GoogleProviderSettings,
|
|
18
|
+
GoogleUserInfo,
|
|
19
|
+
HookContext,
|
|
20
|
+
HookEvent,
|
|
21
|
+
HookRunner,
|
|
22
|
+
Hooks,
|
|
23
|
+
InvalidStateError,
|
|
24
|
+
OAuthError,
|
|
25
|
+
OAuthProviderProtocol,
|
|
26
|
+
Providers,
|
|
27
|
+
SessionExpiredError,
|
|
28
|
+
SessionManager,
|
|
29
|
+
SessionSettings,
|
|
30
|
+
URLSettings,
|
|
31
|
+
generate_session_id,
|
|
32
|
+
generate_state_token,
|
|
33
|
+
parse_scopes,
|
|
34
|
+
validate_scopes,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from belgie_alchemy import AlchemyAdapter as AlchemyAdapter
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
_ALCHEMY_IMPORT_ERROR = "AlchemyAdapter requires the 'alchemy' extra. Install with: uv add belgie[alchemy]"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str) -> object:
|
|
46
|
+
if name != "AlchemyAdapter":
|
|
47
|
+
msg = f"module 'belgie' has no attribute {name!r}"
|
|
48
|
+
raise AttributeError(msg)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
module = import_module("belgie_alchemy")
|
|
52
|
+
except ModuleNotFoundError as exc:
|
|
53
|
+
raise ImportError(_ALCHEMY_IMPORT_ERROR) from exc
|
|
54
|
+
|
|
55
|
+
return module.AlchemyAdapter
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = [ # noqa: RUF022
|
|
59
|
+
# Version
|
|
60
|
+
"__version__",
|
|
61
|
+
# Core
|
|
62
|
+
"Auth",
|
|
63
|
+
"AuthClient",
|
|
64
|
+
"AuthSettings",
|
|
65
|
+
"Hooks",
|
|
66
|
+
"HookContext",
|
|
67
|
+
"HookEvent",
|
|
68
|
+
"HookRunner",
|
|
69
|
+
# Adapters
|
|
70
|
+
"AlchemyAdapter",
|
|
71
|
+
"DBConnection",
|
|
72
|
+
# Session
|
|
73
|
+
"SessionManager",
|
|
74
|
+
# Providers
|
|
75
|
+
"GoogleOAuthProvider",
|
|
76
|
+
"GoogleProviderSettings",
|
|
77
|
+
"GoogleUserInfo",
|
|
78
|
+
"OAuthProviderProtocol",
|
|
79
|
+
"Providers",
|
|
80
|
+
# Settings
|
|
81
|
+
"SessionSettings",
|
|
82
|
+
"CookieSettings",
|
|
83
|
+
"URLSettings",
|
|
84
|
+
# Exceptions
|
|
85
|
+
"BelgieError",
|
|
86
|
+
"AuthenticationError",
|
|
87
|
+
"AuthorizationError",
|
|
88
|
+
"SessionExpiredError",
|
|
89
|
+
"InvalidStateError",
|
|
90
|
+
"OAuthError",
|
|
91
|
+
"ConfigurationError",
|
|
92
|
+
# Utils
|
|
93
|
+
"generate_session_id",
|
|
94
|
+
"generate_state_token",
|
|
95
|
+
"parse_scopes",
|
|
96
|
+
"validate_scopes",
|
|
97
|
+
]
|
belgie/alchemy.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Alchemy re-exports for belgie consumers."""
|
|
2
|
+
|
|
3
|
+
_ALCHEMY_IMPORT_ERROR = "belgie.alchemy requires the 'alchemy' extra. Install with: uv add belgie[alchemy]"
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from belgie_alchemy import ( # type: ignore[import-not-found]
|
|
7
|
+
AlchemyAdapter,
|
|
8
|
+
Base,
|
|
9
|
+
DatabaseSettings,
|
|
10
|
+
DateTimeUTC,
|
|
11
|
+
PrimaryKeyMixin,
|
|
12
|
+
TimestampMixin,
|
|
13
|
+
)
|
|
14
|
+
except ModuleNotFoundError as exc:
|
|
15
|
+
raise ImportError(_ALCHEMY_IMPORT_ERROR) from exc
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AlchemyAdapter",
|
|
19
|
+
"Base",
|
|
20
|
+
"DatabaseSettings",
|
|
21
|
+
"DateTimeUTC",
|
|
22
|
+
"PrimaryKeyMixin",
|
|
23
|
+
"TimestampMixin",
|
|
24
|
+
]
|
belgie/auth/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Belgie Auth - Authentication components."""
|
|
2
|
+
|
|
3
|
+
from belgie_proto import DBConnection
|
|
4
|
+
|
|
5
|
+
from belgie.auth.core.auth import Auth
|
|
6
|
+
from belgie.auth.core.client import AuthClient
|
|
7
|
+
from belgie.auth.core.exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
AuthorizationError,
|
|
10
|
+
BelgieError,
|
|
11
|
+
ConfigurationError,
|
|
12
|
+
InvalidStateError,
|
|
13
|
+
OAuthError,
|
|
14
|
+
SessionExpiredError,
|
|
15
|
+
)
|
|
16
|
+
from belgie.auth.core.hooks import HookContext, HookEvent, HookRunner, Hooks
|
|
17
|
+
from belgie.auth.core.settings import (
|
|
18
|
+
AuthSettings,
|
|
19
|
+
CookieSettings,
|
|
20
|
+
SessionSettings,
|
|
21
|
+
URLSettings,
|
|
22
|
+
)
|
|
23
|
+
from belgie.auth.providers import OAuthProviderProtocol, Providers
|
|
24
|
+
from belgie.auth.providers.google import GoogleOAuthProvider, GoogleProviderSettings, GoogleUserInfo
|
|
25
|
+
from belgie.auth.session.manager import SessionManager
|
|
26
|
+
from belgie.auth.utils.crypto import generate_session_id, generate_state_token
|
|
27
|
+
from belgie.auth.utils.scopes import parse_scopes, validate_scopes
|
|
28
|
+
|
|
29
|
+
__all__ = [ # noqa: RUF022
|
|
30
|
+
# Core
|
|
31
|
+
"Auth",
|
|
32
|
+
"AuthClient",
|
|
33
|
+
"AuthSettings",
|
|
34
|
+
"Hooks",
|
|
35
|
+
"HookContext",
|
|
36
|
+
"HookEvent",
|
|
37
|
+
"HookRunner",
|
|
38
|
+
# Adapters
|
|
39
|
+
"DBConnection",
|
|
40
|
+
# Session
|
|
41
|
+
"SessionManager",
|
|
42
|
+
# Providers
|
|
43
|
+
"GoogleOAuthProvider",
|
|
44
|
+
"GoogleProviderSettings",
|
|
45
|
+
"GoogleUserInfo",
|
|
46
|
+
"OAuthProviderProtocol",
|
|
47
|
+
"Providers",
|
|
48
|
+
# Settings
|
|
49
|
+
"SessionSettings",
|
|
50
|
+
"CookieSettings",
|
|
51
|
+
"URLSettings",
|
|
52
|
+
# Exceptions
|
|
53
|
+
"BelgieError",
|
|
54
|
+
"AuthenticationError",
|
|
55
|
+
"AuthorizationError",
|
|
56
|
+
"SessionExpiredError",
|
|
57
|
+
"InvalidStateError",
|
|
58
|
+
"OAuthError",
|
|
59
|
+
"ConfigurationError",
|
|
60
|
+
# Utils
|
|
61
|
+
"generate_session_id",
|
|
62
|
+
"generate_state_token",
|
|
63
|
+
"parse_scopes",
|
|
64
|
+
"validate_scopes",
|
|
65
|
+
]
|
|
File without changes
|
belgie/auth/core/auth.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator, Callable # noqa: TC003
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import Protocol, cast
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from belgie_proto import (
|
|
9
|
+
AccountProtocol,
|
|
10
|
+
AdapterProtocol,
|
|
11
|
+
DBConnection,
|
|
12
|
+
OAuthStateProtocol,
|
|
13
|
+
SessionProtocol,
|
|
14
|
+
UserProtocol,
|
|
15
|
+
)
|
|
16
|
+
from fastapi import APIRouter, Depends, Request, status
|
|
17
|
+
from fastapi.responses import RedirectResponse
|
|
18
|
+
from fastapi.security import SecurityScopes # noqa: TC002
|
|
19
|
+
|
|
20
|
+
from belgie.auth.core.client import AuthClient
|
|
21
|
+
from belgie.auth.core.hooks import HookRunner, Hooks
|
|
22
|
+
from belgie.auth.core.settings import AuthSettings # noqa: TC001
|
|
23
|
+
from belgie.auth.providers.protocols import OAuthProviderProtocol, Providers # noqa: TC001
|
|
24
|
+
from belgie.auth.session.manager import SessionManager
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DBDependencyProvider(Protocol):
|
|
28
|
+
"""Protocol for database dependency providers.
|
|
29
|
+
|
|
30
|
+
This allows Auth to work with any dependency injection system
|
|
31
|
+
without coupling to a specific implementation.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def dependency(self) -> Callable[[], DBConnection | AsyncGenerator[DBConnection, None]]:
|
|
36
|
+
"""FastAPI dependency that provides database connections."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _AuthCallable:
|
|
41
|
+
"""Descriptor that makes Auth instances callable with instance-specific dependencies.
|
|
42
|
+
|
|
43
|
+
This allows Depends(auth) to work seamlessly - each Auth instance gets its own
|
|
44
|
+
callable that has the Auth instance's database dependency baked into the signature.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __get__(self, obj: Auth | None, objtype: type | None = None) -> object:
|
|
48
|
+
"""Return instance-specific callable when accessed through an instance."""
|
|
49
|
+
if obj is None:
|
|
50
|
+
# Accessed through class, return descriptor itself
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
# Return a callable with this instance's db.dependency
|
|
54
|
+
if obj.db is None:
|
|
55
|
+
msg = "Auth.db must be configured with a dependency"
|
|
56
|
+
raise RuntimeError(msg)
|
|
57
|
+
dependency = obj.db.dependency
|
|
58
|
+
|
|
59
|
+
def __call__( # noqa: N807
|
|
60
|
+
db: DBConnection = Depends(dependency), # noqa: B008
|
|
61
|
+
) -> AuthClient:
|
|
62
|
+
return AuthClient(
|
|
63
|
+
db=db,
|
|
64
|
+
adapter=obj.adapter,
|
|
65
|
+
session_manager=obj.session_manager,
|
|
66
|
+
cookie_name=obj.settings.cookie.name,
|
|
67
|
+
hook_runner=obj.hook_runner,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return __call__
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Auth[UserT: UserProtocol, AccountT: AccountProtocol, SessionT: SessionProtocol, OAuthStateT: OAuthStateProtocol]:
|
|
74
|
+
"""Main authentication orchestrator for Belgie.
|
|
75
|
+
|
|
76
|
+
The Auth class provides a complete OAuth 2.0 authentication solution with session management,
|
|
77
|
+
user creation, and FastAPI integration. It automatically loads OAuth providers from environment
|
|
78
|
+
variables and creates router endpoints for authentication.
|
|
79
|
+
|
|
80
|
+
Type Parameters:
|
|
81
|
+
UserT: User model type implementing UserProtocol
|
|
82
|
+
AccountT: Account model type implementing AccountProtocol
|
|
83
|
+
SessionT: Session model type implementing SessionProtocol
|
|
84
|
+
OAuthStateT: OAuth state model type implementing OAuthStateProtocol
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
settings: Authentication configuration settings
|
|
88
|
+
adapter: Database adapter for persistence operations
|
|
89
|
+
session_manager: Session manager instance for session operations
|
|
90
|
+
providers: Dictionary of registered OAuth providers keyed by provider_id
|
|
91
|
+
router: FastAPI router with authentication endpoints (cached property)
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> from belgie import Auth, AuthSettings, AlchemyAdapter
|
|
95
|
+
>>> from belgie.auth.providers.google import GoogleOAuthProvider, GoogleProviderSettings
|
|
96
|
+
>>> from myapp.models import User, Account, Session, OAuthState
|
|
97
|
+
>>>
|
|
98
|
+
>>> settings = AuthSettings(
|
|
99
|
+
... secret="your-secret-key",
|
|
100
|
+
... base_url="http://localhost:8000",
|
|
101
|
+
... )
|
|
102
|
+
>>>
|
|
103
|
+
>>> adapter = AlchemyAdapter(
|
|
104
|
+
... user=User,
|
|
105
|
+
... account=Account,
|
|
106
|
+
... session=Session,
|
|
107
|
+
... oauth_state=OAuthState,
|
|
108
|
+
... )
|
|
109
|
+
>>> db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
|
|
110
|
+
>>>
|
|
111
|
+
>>> # Explicitly pass provider settings
|
|
112
|
+
>>> providers: Providers = {
|
|
113
|
+
... "google": GoogleProviderSettings(
|
|
114
|
+
... client_id="your-client-id",
|
|
115
|
+
... client_secret="your-client-secret",
|
|
116
|
+
... redirect_uri="http://localhost:8000/auth/provider/google/callback",
|
|
117
|
+
... ),
|
|
118
|
+
... }
|
|
119
|
+
>>> auth = Auth(settings=settings, adapter=adapter, providers=providers, db=db)
|
|
120
|
+
>>> app.include_router(auth.router)
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# Use descriptor to make each instance callable with its own dependency
|
|
124
|
+
__call__: Callable[..., AuthClient] = cast("Callable[..., AuthClient]", _AuthCallable())
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
settings: AuthSettings,
|
|
129
|
+
adapter: AdapterProtocol[UserT, AccountT, SessionT, OAuthStateT],
|
|
130
|
+
db: DBDependencyProvider,
|
|
131
|
+
providers: Providers | None = None,
|
|
132
|
+
hooks: Hooks | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Initialize the Auth instance.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
settings: Authentication configuration including session, cookie, and URL settings
|
|
138
|
+
adapter: Database adapter for user, account, session, and OAuth state persistence
|
|
139
|
+
db: Database dependency provider
|
|
140
|
+
providers: Dictionary of provider settings. Each setting is callable and returns its provider.
|
|
141
|
+
If None, no providers are registered.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
"""
|
|
145
|
+
self.settings = settings
|
|
146
|
+
self.adapter = adapter
|
|
147
|
+
self.db = db
|
|
148
|
+
|
|
149
|
+
self.session_manager = SessionManager(
|
|
150
|
+
adapter=adapter,
|
|
151
|
+
max_age=settings.session.max_age,
|
|
152
|
+
update_age=settings.session.update_age,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self.hook_runner = HookRunner(hooks or Hooks())
|
|
156
|
+
|
|
157
|
+
# Instantiate providers by calling the settings
|
|
158
|
+
self.providers: dict[str, OAuthProviderProtocol] = (
|
|
159
|
+
{provider_id: provider_settings() for provider_id, provider_settings in providers.items()} # ty: ignore[call-non-callable]
|
|
160
|
+
if providers
|
|
161
|
+
else {}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@cached_property
|
|
165
|
+
def router(self) -> APIRouter:
|
|
166
|
+
"""FastAPI router with all provider routes (cached).
|
|
167
|
+
|
|
168
|
+
Creates a router with the following structure:
|
|
169
|
+
- /auth/provider/{provider_id}/signin - Provider signin endpoints
|
|
170
|
+
- /auth/provider/{provider_id}/callback - Provider callback endpoints
|
|
171
|
+
- /auth/signout - Global signout endpoint
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
APIRouter with all authentication endpoints
|
|
175
|
+
"""
|
|
176
|
+
main_router = APIRouter(prefix="/auth", tags=["auth"])
|
|
177
|
+
provider_router = APIRouter(prefix="/provider")
|
|
178
|
+
|
|
179
|
+
if self.db is None:
|
|
180
|
+
msg = "Auth.db must be configured with a dependency"
|
|
181
|
+
raise RuntimeError(msg)
|
|
182
|
+
dependency = self.db.dependency
|
|
183
|
+
|
|
184
|
+
# Include all registered provider routers
|
|
185
|
+
for provider in self.providers.values():
|
|
186
|
+
# Provider's router has prefix /{provider_id}
|
|
187
|
+
# Combined with provider_router prefix: /auth/provider/{provider_id}/...
|
|
188
|
+
provider_specific_router = provider.get_router(
|
|
189
|
+
self.adapter,
|
|
190
|
+
self.settings.cookie,
|
|
191
|
+
session_max_age=self.settings.session.max_age,
|
|
192
|
+
signin_redirect=self.settings.urls.signin_redirect,
|
|
193
|
+
signout_redirect=self.settings.urls.signout_redirect,
|
|
194
|
+
hook_runner=self.hook_runner,
|
|
195
|
+
db_dependency=dependency,
|
|
196
|
+
)
|
|
197
|
+
provider_router.include_router(provider_specific_router)
|
|
198
|
+
|
|
199
|
+
# Add signout endpoint to main router (not provider-specific)
|
|
200
|
+
async def _get_db(db: DBConnection = Depends(dependency)) -> DBConnection: # noqa: B008
|
|
201
|
+
return db
|
|
202
|
+
|
|
203
|
+
@main_router.post("/signout")
|
|
204
|
+
async def signout(
|
|
205
|
+
request: Request,
|
|
206
|
+
db: DBConnection = Depends(_get_db), # noqa: B008, FAST002
|
|
207
|
+
) -> RedirectResponse:
|
|
208
|
+
session_id_str = request.cookies.get(self.settings.cookie.name)
|
|
209
|
+
|
|
210
|
+
if session_id_str:
|
|
211
|
+
try:
|
|
212
|
+
session_id = UUID(session_id_str)
|
|
213
|
+
await self.sign_out(db, session_id)
|
|
214
|
+
except ValueError:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
response = RedirectResponse(
|
|
218
|
+
url=self.settings.urls.signout_redirect,
|
|
219
|
+
status_code=status.HTTP_302_FOUND,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
response.delete_cookie(
|
|
223
|
+
key=self.settings.cookie.name,
|
|
224
|
+
domain=self.settings.cookie.domain,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return response
|
|
228
|
+
|
|
229
|
+
# Include provider router in main router
|
|
230
|
+
main_router.include_router(provider_router)
|
|
231
|
+
return main_router
|
|
232
|
+
|
|
233
|
+
async def get_user_from_session(
|
|
234
|
+
self,
|
|
235
|
+
db: DBConnection,
|
|
236
|
+
session_id: UUID,
|
|
237
|
+
) -> UserT | None:
|
|
238
|
+
"""Retrieve user from a session ID.
|
|
239
|
+
|
|
240
|
+
This method maintains backward compatibility by delegating to AuthClient internally.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
db: Database connection
|
|
244
|
+
session_id: UUID of the session
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
User object if session is valid and user exists, None otherwise
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> user = await auth.get_user_from_session(db, session_id)
|
|
251
|
+
>>> if user:
|
|
252
|
+
... print(f"Found user: {user.email}")
|
|
253
|
+
"""
|
|
254
|
+
client = self.__call__(db)
|
|
255
|
+
return await client.get_user_from_session(session_id)
|
|
256
|
+
|
|
257
|
+
async def sign_out(
|
|
258
|
+
self,
|
|
259
|
+
db: DBConnection,
|
|
260
|
+
session_id: UUID,
|
|
261
|
+
) -> bool:
|
|
262
|
+
"""Sign out a user by deleting their session.
|
|
263
|
+
|
|
264
|
+
This method maintains backward compatibility by delegating to AuthClient internally.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
db: Database connection
|
|
268
|
+
session_id: UUID of the session to delete
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if session was deleted, False if session didn't exist
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
>>> success = await auth.sign_out(db, session_id)
|
|
275
|
+
>>> if success:
|
|
276
|
+
... print("User signed out successfully")
|
|
277
|
+
"""
|
|
278
|
+
client = self.__call__(db)
|
|
279
|
+
return await client.sign_out(session_id)
|
|
280
|
+
|
|
281
|
+
async def _get_session_from_cookie(
|
|
282
|
+
self,
|
|
283
|
+
request: Request,
|
|
284
|
+
db: DBConnection,
|
|
285
|
+
) -> SessionT | None:
|
|
286
|
+
"""Extract and validate session from request cookies.
|
|
287
|
+
|
|
288
|
+
This method delegates to AuthClient for consistency.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
request: FastAPI Request object
|
|
292
|
+
db: Database connection
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Session if valid, None otherwise
|
|
296
|
+
"""
|
|
297
|
+
client = self.__call__(db)
|
|
298
|
+
return await client._get_session_from_cookie(request) # noqa: SLF001
|
|
299
|
+
|
|
300
|
+
async def user(
|
|
301
|
+
self,
|
|
302
|
+
security_scopes: SecurityScopes,
|
|
303
|
+
request: Request,
|
|
304
|
+
db: DBConnection,
|
|
305
|
+
) -> UserT:
|
|
306
|
+
"""FastAPI dependency for retrieving the authenticated user.
|
|
307
|
+
|
|
308
|
+
Extracts the session from cookies, validates it, and returns the authenticated user.
|
|
309
|
+
Optionally validates user-level scopes if specified.
|
|
310
|
+
|
|
311
|
+
This method maintains backward compatibility by delegating to AuthClient internally.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
security_scopes: FastAPI SecurityScopes for scope validation
|
|
315
|
+
request: FastAPI Request object containing cookies
|
|
316
|
+
db: Database connection
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Authenticated user object
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
HTTPException: 401 if not authenticated or session invalid
|
|
323
|
+
HTTPException: 403 if required scopes are not granted
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
>>> from fastapi import Depends, Security
|
|
327
|
+
>>>
|
|
328
|
+
>>> @app.get("/protected")
|
|
329
|
+
>>> async def protected_route(user: User = Depends(auth.user)):
|
|
330
|
+
... return {"email": user.email}
|
|
331
|
+
>>>
|
|
332
|
+
>>> @app.get("/resource")
|
|
333
|
+
>>> async def resource_route(user: User = Security(auth.user, scopes=[Scope.READ])):
|
|
334
|
+
... return {"data": "..."}
|
|
335
|
+
"""
|
|
336
|
+
client = self.__call__(db)
|
|
337
|
+
return await client.get_user(security_scopes, request)
|
|
338
|
+
|
|
339
|
+
async def session(
|
|
340
|
+
self,
|
|
341
|
+
request: Request,
|
|
342
|
+
db: DBConnection,
|
|
343
|
+
) -> SessionT:
|
|
344
|
+
"""FastAPI dependency for retrieving the current session.
|
|
345
|
+
|
|
346
|
+
Extracts and validates the session from cookies.
|
|
347
|
+
|
|
348
|
+
This method maintains backward compatibility by delegating to AuthClient internally.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
request: FastAPI Request object containing cookies
|
|
352
|
+
db: Database connection
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Active session object
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
HTTPException: 401 if not authenticated or session invalid/expired
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
>>> from fastapi import Depends
|
|
362
|
+
>>>
|
|
363
|
+
>>> @app.get("/session-info")
|
|
364
|
+
>>> async def session_info(session: Session = Depends(auth.session)):
|
|
365
|
+
... return {"session_id": str(session.id), "expires_at": session.expires_at.isoformat()}
|
|
366
|
+
"""
|
|
367
|
+
client = self.__call__(db)
|
|
368
|
+
return await client.get_session(request)
|