byoi 0.1.0a1__tar.gz
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.
- byoi-0.1.0a1/LICENSE +21 -0
- byoi-0.1.0a1/PKG-INFO +504 -0
- byoi-0.1.0a1/README.md +457 -0
- byoi-0.1.0a1/pyproject.toml +311 -0
- byoi-0.1.0a1/src/byoi/__init__.py +233 -0
- byoi-0.1.0a1/src/byoi/__main__.py +228 -0
- byoi-0.1.0a1/src/byoi/cache.py +346 -0
- byoi-0.1.0a1/src/byoi/config.py +349 -0
- byoi-0.1.0a1/src/byoi/dependencies.py +144 -0
- byoi-0.1.0a1/src/byoi/errors.py +360 -0
- byoi-0.1.0a1/src/byoi/models.py +451 -0
- byoi-0.1.0a1/src/byoi/pkce.py +144 -0
- byoi-0.1.0a1/src/byoi/providers.py +434 -0
- byoi-0.1.0a1/src/byoi/py.typed +0 -0
- byoi-0.1.0a1/src/byoi/repositories.py +252 -0
- byoi-0.1.0a1/src/byoi/service.py +723 -0
- byoi-0.1.0a1/src/byoi/telemetry.py +352 -0
- byoi-0.1.0a1/src/byoi/tokens.py +340 -0
- byoi-0.1.0a1/src/byoi/types.py +130 -0
byoi-0.1.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 to present ndiverge and individual contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
byoi-0.1.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: byoi
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Bring-Your-Own-Identity server library for FastAPI applications.
|
|
5
|
+
Keywords: fastapi,authentication,oidc,openid-connect,oauth2,identity,jwt,pkce,sso,single-sign-on,identity-provider,security
|
|
6
|
+
Author: Wietse Sas
|
|
7
|
+
Author-email: Wietse Sas <wietse.sas@ndiverge.eu>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Framework :: FastAPI
|
|
19
|
+
Classifier: Framework :: Pydantic
|
|
20
|
+
Classifier: Framework :: Pydantic :: 2
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Dist: fastapi>=0.128.0,<1.0.0
|
|
26
|
+
Requires-Dist: httpx>=0.28.1,<1.0.0
|
|
27
|
+
Requires-Dist: pydantic>=2.12.5,<3.0.0
|
|
28
|
+
Requires-Dist: python-jose[cryptography]>=3.5.0,<4.0.0
|
|
29
|
+
Requires-Dist: redis>=7.1.0,<8.0.0 ; extra == 'all'
|
|
30
|
+
Requires-Dist: opentelemetry-api>=1.20.0,<2.0.0 ; extra == 'all'
|
|
31
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0,<2.0.0 ; extra == 'all'
|
|
32
|
+
Requires-Dist: redis>=7.1.0,<8.0.0 ; extra == 'redis'
|
|
33
|
+
Requires-Dist: opentelemetry-api>=1.20.0,<2.0.0 ; extra == 'telemetry'
|
|
34
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0,<2.0.0 ; extra == 'telemetry'
|
|
35
|
+
Maintainer: Wietse Sas
|
|
36
|
+
Maintainer-email: Wietse Sas <wietse.sas@ndiverge.eu>
|
|
37
|
+
Requires-Python: >=3.11
|
|
38
|
+
Project-URL: Homepage, https://www.ndiverge.eu/byoi
|
|
39
|
+
Project-URL: Documentation, https://www.ndiverge.eu/byoi/docs
|
|
40
|
+
Project-URL: Repository, https://github.com/ndiverge/byoi
|
|
41
|
+
Project-URL: Issues, https://github.com/ndiverge/byoi/issues
|
|
42
|
+
Project-URL: Changelog, https://www.ndiverge.eu/byoi/release-notes
|
|
43
|
+
Provides-Extra: all
|
|
44
|
+
Provides-Extra: redis
|
|
45
|
+
Provides-Extra: telemetry
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# BYOI - Bring Your Own Identity
|
|
49
|
+
|
|
50
|
+
A server library for FastAPI applications that provides OIDC (OpenID Connect) authentication with support for multiple identity providers.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **PKCE (Proof Key for Code Exchange)** - Prevent authorization code interception attacks
|
|
55
|
+
- **ID Token Validation** - Full JWT validation with JWKS fetching and caching
|
|
56
|
+
- **Identity Linking** - Users can link multiple providers (Google + Microsoft, etc.)
|
|
57
|
+
- **Nonce Validation** - Prevents replay attacks
|
|
58
|
+
- **Proper Error Handling** - Clean error responses throughout
|
|
59
|
+
- **FastAPI Dependencies** - Easy integration with dependency injection
|
|
60
|
+
- **Multiple Client Types** - Support for web, desktop, and mobile applications
|
|
61
|
+
- **Flexible Caching** - In-memory and Redis cache implementations
|
|
62
|
+
- **OpenTelemetry Support** - Optional tracing integration for observability
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install byoi
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For Redis cache support:
|
|
71
|
+
```bash
|
|
72
|
+
pip install byoi[redis]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For OpenTelemetry support:
|
|
76
|
+
```bash
|
|
77
|
+
pip install byoi[telemetry]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For all optional dependencies:
|
|
81
|
+
```bash
|
|
82
|
+
pip install byoi[all]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
### 1. Implement the Repository Protocols
|
|
88
|
+
|
|
89
|
+
BYOI requires you to implement three repository protocols for data persistence:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from datetime import datetime
|
|
93
|
+
from typing import Any
|
|
94
|
+
from byoi import (
|
|
95
|
+
UserProtocol,
|
|
96
|
+
LinkedIdentityProtocol,
|
|
97
|
+
AuthStateProtocol,
|
|
98
|
+
UserRepositoryProtocol,
|
|
99
|
+
LinkedIdentityRepositoryProtocol,
|
|
100
|
+
AuthStateRepositoryProtocol,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Example User implementation
|
|
104
|
+
class User:
|
|
105
|
+
def __init__(self, id: str, email: str | None, is_active: bool = True):
|
|
106
|
+
self.id = id
|
|
107
|
+
self.email = email
|
|
108
|
+
self.is_active = is_active
|
|
109
|
+
|
|
110
|
+
# Example UserRepository implementation
|
|
111
|
+
class UserRepository:
|
|
112
|
+
def __init__(self):
|
|
113
|
+
self._users: dict[str, User] = {}
|
|
114
|
+
|
|
115
|
+
async def get_by_id(self, user_id: str) -> User | None:
|
|
116
|
+
return self._users.get(user_id)
|
|
117
|
+
|
|
118
|
+
async def get_by_email(self, email: str) -> User | None:
|
|
119
|
+
for user in self._users.values():
|
|
120
|
+
if user.email == email:
|
|
121
|
+
return user
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
async def create(
|
|
125
|
+
self,
|
|
126
|
+
email: str | None = None,
|
|
127
|
+
is_active: bool = True,
|
|
128
|
+
**extra_data: Any,
|
|
129
|
+
) -> User:
|
|
130
|
+
import uuid
|
|
131
|
+
user = User(id=str(uuid.uuid4()), email=email, is_active=is_active)
|
|
132
|
+
self._users[user.id] = user
|
|
133
|
+
return user
|
|
134
|
+
|
|
135
|
+
async def update(self, user_id: str, **data: Any) -> User | None:
|
|
136
|
+
user = self._users.get(user_id)
|
|
137
|
+
if user:
|
|
138
|
+
for key, value in data.items():
|
|
139
|
+
setattr(user, key, value)
|
|
140
|
+
return user
|
|
141
|
+
|
|
142
|
+
# Implement LinkedIdentityRepository and AuthStateRepository similarly...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 2. Configure BYOI
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from fastapi import FastAPI
|
|
149
|
+
from byoi import BYOI, BYOIConfig, OIDCProviderConfig
|
|
150
|
+
|
|
151
|
+
# Create repositories
|
|
152
|
+
user_repo = UserRepository()
|
|
153
|
+
identity_repo = LinkedIdentityRepository()
|
|
154
|
+
auth_state_repo = AuthStateRepository()
|
|
155
|
+
|
|
156
|
+
# Configure BYOI
|
|
157
|
+
config = BYOIConfig(
|
|
158
|
+
user_repository=user_repo,
|
|
159
|
+
identity_repository=identity_repo,
|
|
160
|
+
auth_state_repository=auth_state_repo,
|
|
161
|
+
# cache is optional - defaults to InMemoryCache if not provided
|
|
162
|
+
providers=[
|
|
163
|
+
OIDCProviderConfig(
|
|
164
|
+
name="google",
|
|
165
|
+
display_name="Google",
|
|
166
|
+
issuer="https://accounts.google.com",
|
|
167
|
+
client_id="your-google-client-id",
|
|
168
|
+
client_secret="your-google-client-secret",
|
|
169
|
+
),
|
|
170
|
+
OIDCProviderConfig(
|
|
171
|
+
name="microsoft",
|
|
172
|
+
display_name="Microsoft",
|
|
173
|
+
issuer="https://login.microsoftonline.com/{tenant}/v2.0",
|
|
174
|
+
client_id="your-microsoft-client-id",
|
|
175
|
+
client_secret="your-microsoft-client-secret",
|
|
176
|
+
),
|
|
177
|
+
],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Create BYOI instance
|
|
181
|
+
byoi = BYOI(config)
|
|
182
|
+
|
|
183
|
+
# Create FastAPI app with BYOI lifespan
|
|
184
|
+
app = FastAPI(lifespan=byoi.lifespan)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 3. Create Authentication Routes
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from fastapi import FastAPI, HTTPException
|
|
191
|
+
from byoi import (
|
|
192
|
+
AuthServiceDep,
|
|
193
|
+
ProviderManagerDep,
|
|
194
|
+
AuthorizationRequest,
|
|
195
|
+
TokenExchangeRequest,
|
|
196
|
+
ClientType,
|
|
197
|
+
InvalidStateError,
|
|
198
|
+
StateExpiredError,
|
|
199
|
+
TokenExchangeError,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@app.get("/auth/providers")
|
|
203
|
+
async def list_providers(provider_manager: ProviderManagerDep):
|
|
204
|
+
"""List all configured identity providers."""
|
|
205
|
+
return provider_manager.list_providers()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.post("/auth/authorize")
|
|
209
|
+
async def create_authorization_url(
|
|
210
|
+
provider_name: str,
|
|
211
|
+
redirect_uri: str,
|
|
212
|
+
auth_service: AuthServiceDep,
|
|
213
|
+
):
|
|
214
|
+
"""Create an authorization URL for OAuth flow."""
|
|
215
|
+
request = AuthorizationRequest(
|
|
216
|
+
provider_name=provider_name,
|
|
217
|
+
redirect_uri=redirect_uri,
|
|
218
|
+
client_type=ClientType.WEB,
|
|
219
|
+
)
|
|
220
|
+
return await auth_service.create_authorization_url(request)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@app.post("/auth/callback")
|
|
224
|
+
async def handle_callback(
|
|
225
|
+
code: str,
|
|
226
|
+
state: str,
|
|
227
|
+
redirect_uri: str,
|
|
228
|
+
auth_service: AuthServiceDep,
|
|
229
|
+
):
|
|
230
|
+
"""Handle OAuth callback and authenticate user."""
|
|
231
|
+
try:
|
|
232
|
+
request = TokenExchangeRequest(
|
|
233
|
+
code=code,
|
|
234
|
+
state=state,
|
|
235
|
+
redirect_uri=redirect_uri,
|
|
236
|
+
)
|
|
237
|
+
user = await auth_service.authenticate(request)
|
|
238
|
+
# Create your session/JWT here
|
|
239
|
+
return {"user_id": user.user_id, "is_new": user.is_new_user}
|
|
240
|
+
except InvalidStateError:
|
|
241
|
+
raise HTTPException(400, "Invalid state parameter")
|
|
242
|
+
except StateExpiredError:
|
|
243
|
+
raise HTTPException(400, "Authorization request expired")
|
|
244
|
+
except TokenExchangeError as e:
|
|
245
|
+
raise HTTPException(400, f"Authentication failed: {e.message}")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Identity Linking
|
|
249
|
+
|
|
250
|
+
Allow users to link multiple identity providers to their account:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
@app.post("/auth/link")
|
|
254
|
+
async def link_identity(
|
|
255
|
+
user_id: str, # From your session/JWT
|
|
256
|
+
code: str,
|
|
257
|
+
state: str,
|
|
258
|
+
redirect_uri: str,
|
|
259
|
+
auth_service: AuthServiceDep,
|
|
260
|
+
):
|
|
261
|
+
"""Link a new identity provider to user's account."""
|
|
262
|
+
from byoi import LinkIdentityRequest, IdentityAlreadyLinkedError
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
request = LinkIdentityRequest(
|
|
266
|
+
user_id=user_id,
|
|
267
|
+
code=code,
|
|
268
|
+
state=state,
|
|
269
|
+
redirect_uri=redirect_uri,
|
|
270
|
+
)
|
|
271
|
+
return await auth_service.link_identity(request)
|
|
272
|
+
except IdentityAlreadyLinkedError:
|
|
273
|
+
raise HTTPException(400, "This identity is already linked to another account")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.get("/auth/identities/{user_id}")
|
|
277
|
+
async def get_user_identities(
|
|
278
|
+
user_id: str,
|
|
279
|
+
auth_service: AuthServiceDep,
|
|
280
|
+
):
|
|
281
|
+
"""Get all linked identities for a user."""
|
|
282
|
+
return await auth_service.get_user_identities(user_id)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.delete("/auth/identities/{user_id}/{identity_id}")
|
|
286
|
+
async def unlink_identity(
|
|
287
|
+
user_id: str,
|
|
288
|
+
identity_id: str,
|
|
289
|
+
auth_service: AuthServiceDep,
|
|
290
|
+
):
|
|
291
|
+
"""Unlink an identity from a user's account."""
|
|
292
|
+
from byoi import UnlinkIdentityRequest
|
|
293
|
+
|
|
294
|
+
request = UnlinkIdentityRequest(user_id=user_id, identity_id=identity_id)
|
|
295
|
+
return await auth_service.unlink_identity(request)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Caching
|
|
299
|
+
|
|
300
|
+
BYOI supports multiple cache implementations:
|
|
301
|
+
|
|
302
|
+
### In-Memory Cache (Default)
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from byoi import InMemoryCache
|
|
306
|
+
|
|
307
|
+
cache = InMemoryCache(cleanup_interval_seconds=300)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Redis Cache
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
from byoi import RedisCache
|
|
314
|
+
|
|
315
|
+
cache = RedisCache(
|
|
316
|
+
url="redis://localhost:6379/0",
|
|
317
|
+
key_prefix="byoi:",
|
|
318
|
+
max_connections=10, # optional
|
|
319
|
+
)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Null Cache
|
|
323
|
+
|
|
324
|
+
A no-op cache implementation that doesn't cache anything. Useful for testing or when caching is not desired.
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from byoi import NullCache
|
|
328
|
+
|
|
329
|
+
cache = NullCache()
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Custom Cache
|
|
333
|
+
|
|
334
|
+
Implement the `CacheProtocol`:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from byoi import CacheProtocol
|
|
338
|
+
|
|
339
|
+
class MyCache(CacheProtocol):
|
|
340
|
+
async def get(self, key: str) -> Any | None:
|
|
341
|
+
...
|
|
342
|
+
|
|
343
|
+
async def set(self, key: str, value: Any, ttl_seconds: int | None = None) -> None:
|
|
344
|
+
...
|
|
345
|
+
|
|
346
|
+
async def delete(self, key: str) -> bool:
|
|
347
|
+
...
|
|
348
|
+
|
|
349
|
+
async def clear(self) -> None:
|
|
350
|
+
...
|
|
351
|
+
|
|
352
|
+
async def close(self) -> None:
|
|
353
|
+
...
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Adding Custom Providers
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
from byoi import OIDCProviderConfig
|
|
360
|
+
|
|
361
|
+
# Standard OIDC provider
|
|
362
|
+
okta_config = OIDCProviderConfig(
|
|
363
|
+
name="okta",
|
|
364
|
+
display_name="Okta",
|
|
365
|
+
issuer="https://your-domain.okta.com",
|
|
366
|
+
client_id="your-client-id",
|
|
367
|
+
client_secret="your-client-secret",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Custom provider with overrides
|
|
371
|
+
custom_config = OIDCProviderConfig(
|
|
372
|
+
name="custom",
|
|
373
|
+
display_name="Custom IDP",
|
|
374
|
+
issuer="https://idp.example.com",
|
|
375
|
+
client_id="client-id",
|
|
376
|
+
client_secret="client-secret",
|
|
377
|
+
# Override discovered endpoints if needed
|
|
378
|
+
authorization_endpoint_override="https://idp.example.com/oauth/authorize",
|
|
379
|
+
token_endpoint_override="https://idp.example.com/oauth/token",
|
|
380
|
+
jwks_uri_override="https://idp.example.com/.well-known/jwks.json",
|
|
381
|
+
# Custom scopes
|
|
382
|
+
scopes=["openid", "email", "profile", "custom_scope"],
|
|
383
|
+
# Extra auth parameters
|
|
384
|
+
extra_auth_params={"prompt": "consent"},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Add providers after setup
|
|
388
|
+
byoi.add_provider(custom_config)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Error Handling
|
|
392
|
+
|
|
393
|
+
BYOI provides specific exception types for different error scenarios:
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from byoi import (
|
|
397
|
+
BYOIError, # Base exception for all BYOI errors
|
|
398
|
+
ConfigurationError, # Configuration problems
|
|
399
|
+
# Provider errors
|
|
400
|
+
ProviderError, # Base class for provider-related errors
|
|
401
|
+
ProviderNotFoundError, # Unknown provider
|
|
402
|
+
ProviderDiscoveryError, # OIDC discovery failed
|
|
403
|
+
# Authentication errors
|
|
404
|
+
AuthenticationError, # Base class for authentication errors
|
|
405
|
+
InvalidStateError, # Invalid OAuth state
|
|
406
|
+
StateExpiredError, # OAuth state expired
|
|
407
|
+
InvalidCodeError, # Invalid authorization code
|
|
408
|
+
TokenExchangeError, # Token exchange failed
|
|
409
|
+
TokenRefreshError, # Token refresh failed
|
|
410
|
+
# Token validation errors
|
|
411
|
+
TokenValidationError, # ID token validation failed
|
|
412
|
+
InvalidNonceError, # Nonce mismatch
|
|
413
|
+
InvalidIssuerError, # Issuer mismatch
|
|
414
|
+
InvalidAudienceError, # Audience mismatch
|
|
415
|
+
TokenExpiredError, # Token has expired
|
|
416
|
+
JWKSFetchError, # Failed to fetch JWKS
|
|
417
|
+
# Identity errors
|
|
418
|
+
IdentityError, # Base class for identity errors
|
|
419
|
+
IdentityAlreadyLinkedError, # Identity already linked to another account
|
|
420
|
+
IdentityNotFoundError, # Identity not found
|
|
421
|
+
CannotUnlinkLastIdentityError, # Cannot unlink the last identity
|
|
422
|
+
UserNotFoundError, # User not found
|
|
423
|
+
# Cache errors
|
|
424
|
+
CacheError, # Cache operation failed
|
|
425
|
+
)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Protocols Reference
|
|
429
|
+
|
|
430
|
+
### UserProtocol
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
class UserProtocol(Protocol):
|
|
434
|
+
@property
|
|
435
|
+
def id(self) -> str: ...
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def email(self) -> str | None: ...
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def is_active(self) -> bool: ...
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### LinkedIdentityProtocol
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
class LinkedIdentityProtocol(Protocol):
|
|
448
|
+
@property
|
|
449
|
+
def id(self) -> str: ...
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def user_id(self) -> str: ...
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def provider_name(self) -> str: ...
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def provider_subject(self) -> str: ...
|
|
459
|
+
|
|
460
|
+
@property
|
|
461
|
+
def email(self) -> str | None: ...
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def created_at(self) -> datetime: ...
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def last_used_at(self) -> datetime | None: ...
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### AuthStateProtocol
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
class AuthStateProtocol(Protocol):
|
|
474
|
+
@property
|
|
475
|
+
def state(self) -> str: ...
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def code_verifier(self) -> str: ...
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def nonce(self) -> str: ...
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def provider_name(self) -> str: ...
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def redirect_uri(self) -> str: ...
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def created_at(self) -> datetime: ...
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def expires_at(self) -> datetime: ...
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def client_type(self) -> str: ...
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def extra_data(self) -> dict[str, Any]: ...
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## License
|
|
503
|
+
|
|
504
|
+
MIT License - see LICENSE file for details.
|