fastapi-sso 0.15.0__py3-none-any.whl → 0.16.0__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.
- fastapi_sso/sso/base.py +70 -1
- fastapi_sso/sso/seznam.py +39 -0
- {fastapi_sso-0.15.0.dist-info → fastapi_sso-0.16.0.dist-info}/METADATA +49 -7
- {fastapi_sso-0.15.0.dist-info → fastapi_sso-0.16.0.dist-info}/RECORD +6 -5
- {fastapi_sso-0.15.0.dist-info → fastapi_sso-0.16.0.dist-info}/WHEEL +1 -1
- {fastapi_sso-0.15.0.dist-info → fastapi_sso-0.16.0.dist-info}/LICENSE.md +0 -0
fastapi_sso/sso/base.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""SSO login base dependency."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
7
|
+
import sys
|
|
6
8
|
import warnings
|
|
7
9
|
from types import TracebackType
|
|
8
|
-
from typing import Any, ClassVar, Dict, List, Literal, Optional, Type, TypedDict, Union, overload
|
|
10
|
+
from typing import Any, ClassVar, Dict, List, Literal, Optional, Type, TypedDict, TypeVar, Union, overload
|
|
9
11
|
|
|
10
12
|
import httpx
|
|
11
13
|
import pydantic
|
|
@@ -17,8 +19,19 @@ from starlette.responses import RedirectResponse
|
|
|
17
19
|
from fastapi_sso.pkce import get_pkce_challenge_pair
|
|
18
20
|
from fastapi_sso.state import generate_random_state
|
|
19
21
|
|
|
22
|
+
if sys.version_info < (3, 10):
|
|
23
|
+
from typing import Callable # pragma: no cover
|
|
24
|
+
|
|
25
|
+
from typing_extensions import ParamSpec # pragma: no cover
|
|
26
|
+
else:
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
from typing import ParamSpec
|
|
29
|
+
|
|
20
30
|
logger = logging.getLogger(__name__)
|
|
21
31
|
|
|
32
|
+
T = TypeVar("T")
|
|
33
|
+
P = ParamSpec("P")
|
|
34
|
+
|
|
22
35
|
|
|
23
36
|
class DiscoveryDocument(TypedDict):
|
|
24
37
|
"""Discovery document."""
|
|
@@ -55,6 +68,26 @@ class OpenID(pydantic.BaseModel):
|
|
|
55
68
|
provider: Optional[str] = None
|
|
56
69
|
|
|
57
70
|
|
|
71
|
+
class SecurityWarning(UserWarning):
|
|
72
|
+
"""Raised when insecure usage is detected"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def requires_async_context(func: Callable[P, T]) -> Callable[P, T]:
|
|
76
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
77
|
+
if not args or not isinstance(args[0], SSOBase):
|
|
78
|
+
return func(*args, **kwargs)
|
|
79
|
+
if not args[0]._in_stack:
|
|
80
|
+
warnings.warn(
|
|
81
|
+
"Please make sure you are using SSO provider in an async context (using 'async with provider:'). "
|
|
82
|
+
"See https://github.com/tomasvotava/fastapi-sso/issues/186 for more information.",
|
|
83
|
+
category=SecurityWarning,
|
|
84
|
+
stacklevel=1,
|
|
85
|
+
)
|
|
86
|
+
return func(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
return wrapper
|
|
89
|
+
|
|
90
|
+
|
|
58
91
|
class SSOBase:
|
|
59
92
|
"""Base class for all SSO providers."""
|
|
60
93
|
|
|
@@ -83,6 +116,8 @@ class SSOBase:
|
|
|
83
116
|
self.client_secret: str = client_secret
|
|
84
117
|
self.redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = redirect_uri
|
|
85
118
|
self.allow_insecure_http: bool = allow_insecure_http
|
|
119
|
+
self._login_lock = asyncio.Lock()
|
|
120
|
+
self._in_stack = False
|
|
86
121
|
self._oauth_client: Optional[WebApplicationClient] = None
|
|
87
122
|
self._generated_state: Optional[str] = None
|
|
88
123
|
|
|
@@ -128,6 +163,7 @@ class SSOBase:
|
|
|
128
163
|
return self._state
|
|
129
164
|
|
|
130
165
|
@property
|
|
166
|
+
@requires_async_context
|
|
131
167
|
def oauth_client(self) -> WebApplicationClient:
|
|
132
168
|
"""Retrieves the OAuth Client to aid in generating requests and parsing responses.
|
|
133
169
|
|
|
@@ -144,6 +180,7 @@ class SSOBase:
|
|
|
144
180
|
return self._oauth_client
|
|
145
181
|
|
|
146
182
|
@property
|
|
183
|
+
@requires_async_context
|
|
147
184
|
def access_token(self) -> Optional[str]:
|
|
148
185
|
"""Retrieves the access token from token endpoint.
|
|
149
186
|
|
|
@@ -153,6 +190,7 @@ class SSOBase:
|
|
|
153
190
|
return self.oauth_client.access_token
|
|
154
191
|
|
|
155
192
|
@property
|
|
193
|
+
@requires_async_context
|
|
156
194
|
def refresh_token(self) -> Optional[str]:
|
|
157
195
|
"""Retrieves the refresh token if returned from provider.
|
|
158
196
|
|
|
@@ -162,6 +200,7 @@ class SSOBase:
|
|
|
162
200
|
return self._refresh_token or self.oauth_client.refresh_token
|
|
163
201
|
|
|
164
202
|
@property
|
|
203
|
+
@requires_async_context
|
|
165
204
|
def id_token(self) -> Optional[str]:
|
|
166
205
|
"""Retrieves the id token if returned from provider.
|
|
167
206
|
|
|
@@ -308,6 +347,7 @@ class SSOBase:
|
|
|
308
347
|
convert_response: Literal[False],
|
|
309
348
|
) -> Optional[Dict[str, Any]]: ...
|
|
310
349
|
|
|
350
|
+
@requires_async_context
|
|
311
351
|
async def verify_and_process(
|
|
312
352
|
self,
|
|
313
353
|
request: Request,
|
|
@@ -362,6 +402,12 @@ class SSOBase:
|
|
|
362
402
|
)
|
|
363
403
|
|
|
364
404
|
def __enter__(self) -> "SSOBase":
|
|
405
|
+
warnings.warn(
|
|
406
|
+
"SSO Providers are supposed to be used in async context, please change 'with provider' to "
|
|
407
|
+
"'async with provider'. See https://github.com/tomasvotava/fastapi-sso/issues/186 for more information.",
|
|
408
|
+
DeprecationWarning,
|
|
409
|
+
stacklevel=1,
|
|
410
|
+
)
|
|
365
411
|
self._oauth_client = None
|
|
366
412
|
self._refresh_token = None
|
|
367
413
|
self._id_token = None
|
|
@@ -372,6 +418,28 @@ class SSOBase:
|
|
|
372
418
|
self._pkce_code_verifier, self._pkce_code_challenge = get_pkce_challenge_pair(self._pkce_challenge_length)
|
|
373
419
|
return self
|
|
374
420
|
|
|
421
|
+
async def __aenter__(self) -> "SSOBase":
|
|
422
|
+
await self._login_lock.acquire()
|
|
423
|
+
self._in_stack = True
|
|
424
|
+
self._oauth_client = None
|
|
425
|
+
self._refresh_token = None
|
|
426
|
+
self._id_token = None
|
|
427
|
+
self._state = None
|
|
428
|
+
if self.requires_state:
|
|
429
|
+
self._generated_state = generate_random_state()
|
|
430
|
+
if self.uses_pkce:
|
|
431
|
+
self._pkce_code_verifier, self._pkce_code_challenge = get_pkce_challenge_pair(self._pkce_challenge_length)
|
|
432
|
+
return self
|
|
433
|
+
|
|
434
|
+
async def __aexit__(
|
|
435
|
+
self,
|
|
436
|
+
_exc_type: Optional[Type[BaseException]],
|
|
437
|
+
_exc_val: Optional[BaseException],
|
|
438
|
+
_exc_tb: Optional[TracebackType],
|
|
439
|
+
) -> None:
|
|
440
|
+
self._in_stack = False
|
|
441
|
+
self._login_lock.release()
|
|
442
|
+
|
|
375
443
|
def __exit__(
|
|
376
444
|
self,
|
|
377
445
|
_exc_type: Optional[Type[BaseException]],
|
|
@@ -410,6 +478,7 @@ class SSOBase:
|
|
|
410
478
|
convert_response: Literal[False],
|
|
411
479
|
) -> Optional[Dict[str, Any]]: ...
|
|
412
480
|
|
|
481
|
+
@requires_async_context
|
|
413
482
|
async def process_login(
|
|
414
483
|
self,
|
|
415
484
|
code: str,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Seznam SSO Login Helper."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import httpx # pragma: no cover
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# https://vyvojari.seznam.cz/oauth/doc
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SeznamSSO(SSOBase):
|
|
15
|
+
"""Class providing login via Seznam OAuth."""
|
|
16
|
+
|
|
17
|
+
provider = "seznam"
|
|
18
|
+
base_url = "https://login.szn.cz/api/v1"
|
|
19
|
+
scope: ClassVar = ["identity", "avatar"] # + ["contact-phone", "adulthood", "birthday", "gender"]
|
|
20
|
+
|
|
21
|
+
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
22
|
+
"""Get document containing handy urls."""
|
|
23
|
+
return {
|
|
24
|
+
"authorization_endpoint": f"{self.base_url}/oauth/auth",
|
|
25
|
+
"token_endpoint": f"{self.base_url}/oauth/token",
|
|
26
|
+
"userinfo_endpoint": f"{self.base_url}/user",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
|
|
30
|
+
"""Return OpenID from user information provided by Seznam."""
|
|
31
|
+
return OpenID(
|
|
32
|
+
email=response.get("email"),
|
|
33
|
+
first_name=response.get("firstname"),
|
|
34
|
+
last_name=response.get("lastname"),
|
|
35
|
+
display_name=response.get("accountDisplayName"),
|
|
36
|
+
provider=self.provider,
|
|
37
|
+
id=response.get("oauth_user_id"),
|
|
38
|
+
picture=response.get("avatar_url"),
|
|
39
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-sso
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: FastAPI plugin to enable SSO to most common providers (such as Facebook login, Google login and login via Microsoft Office 365 Account)
|
|
5
5
|
Home-page: https://tomasvotava.github.io/fastapi-sso/
|
|
6
6
|
License: MIT
|
|
@@ -15,10 +15,12 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
19
|
Requires-Dist: fastapi (>=0.80)
|
|
19
20
|
Requires-Dist: httpx (>=0.23.0)
|
|
20
21
|
Requires-Dist: oauthlib (>=3.1.0)
|
|
21
22
|
Requires-Dist: pydantic[email] (>=1.8.0)
|
|
23
|
+
Requires-Dist: typing-extensions (>=4.12.2,<5.0.0) ; python_version < "3.10"
|
|
22
24
|
Project-URL: Documentation, https://tomasvotava.github.io/fastapi-sso/
|
|
23
25
|
Project-URL: Repository, https://github.com/tomasvotava/fastapi-sso
|
|
24
26
|
Description-Content-Type: text/markdown
|
|
@@ -61,14 +63,53 @@ Quick links for the eager ones:
|
|
|
61
63
|
- [Demo site](https://fastapi-sso-example.vercel.app/)
|
|
62
64
|
- [Medium articles](https://medium.com/@christos.karvouniaris247)
|
|
63
65
|
|
|
64
|
-
## Security
|
|
66
|
+
## Security Notice
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
The SSO instance could share state between requests, which could lead to security issues.
|
|
68
|
-
**Please update to `0.7.0` or newer**.
|
|
68
|
+
### Version `0.16.0` Update: Race Condition Bug Fix & Context Manager Change
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
A race condition bug in the login flow that could, in rare cases, allow one user
|
|
71
|
+
to assume the identity of another due to concurrent login requests was recently discovered
|
|
72
|
+
by [@parikls](https://github.com/parikls).
|
|
73
|
+
This issue was reported in [#186](https://github.com/tomasvotava/fastapi-sso/issues/186) and has been resolved
|
|
74
|
+
in version `0.16.0`.
|
|
75
|
+
|
|
76
|
+
**Details of the Fix:**
|
|
77
|
+
|
|
78
|
+
The bug was mitigated by introducing an async lock mechanism that ensures only one user can attempt the login
|
|
79
|
+
process at any given time. This prevents race conditions that could lead to unintended user identity crossover.
|
|
80
|
+
|
|
81
|
+
**Important Change:**
|
|
82
|
+
|
|
83
|
+
To fully support this fix, **users must now use the SSO instance within an `async with`
|
|
84
|
+
context manager**. This adjustment is necessary for proper handling of asynchronous operations.
|
|
85
|
+
|
|
86
|
+
The synchronous `with` context manager is now deprecated and will produce a warning.
|
|
87
|
+
It will be removed in future versions to ensure best practices for async handling.
|
|
88
|
+
|
|
89
|
+
**Impact:**
|
|
90
|
+
|
|
91
|
+
This bug could potentially affect deployments with high concurrency or scenarios where multiple users initiate
|
|
92
|
+
login requests simultaneously. To prevent potential issues and deprecation warnings, **update to
|
|
93
|
+
version `0.16.0` or later and modify your code to use the async with context**.
|
|
94
|
+
|
|
95
|
+
Code Example Update:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# Before (deprecated)
|
|
99
|
+
with sso:
|
|
100
|
+
openid = await sso.verify_and_process(request)
|
|
101
|
+
|
|
102
|
+
# After (recommended)
|
|
103
|
+
async with sso:
|
|
104
|
+
openid = await sso.verify_and_process(request)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Thanks to both [@parikls](https://github.com/parikls) and the community for helping me identify and improve the
|
|
108
|
+
security of `fastapi-sso`. If you encounter any issues or potential vulnerabilities, please report them
|
|
109
|
+
immediately so they can be addressed.
|
|
110
|
+
|
|
111
|
+
For more details, refer to Issue [#186](https://github.com/tomasvotava/fastapi-sso/issues/186)
|
|
112
|
+
and PR [#189](https://github.com/tomasvotava/fastapi-sso/pull/189).
|
|
72
113
|
|
|
73
114
|
## Support this project
|
|
74
115
|
|
|
@@ -101,6 +142,7 @@ I tend to process Pull Requests faster when properly caffeinated 😉.
|
|
|
101
142
|
- Line (by Jimmy Yeh) - [jimmyyyeh](https://github.com/jimmyyyeh)
|
|
102
143
|
- LinkedIn (by Alessandro Pischedda) - [Cereal84](https://github.com/Cereal84)
|
|
103
144
|
- Yandex (by Akim Faskhutdinov) – [akimrx](https://github.com/akimrx)
|
|
145
|
+
- Seznam (by Tomas Koutek) - [TomasKoutek](https://github.com/TomasKoutek)
|
|
104
146
|
|
|
105
147
|
See [Contributing](#contributing) for a guide on how to contribute your own login provider.
|
|
106
148
|
|
|
@@ -2,7 +2,7 @@ fastapi_sso/__init__.py,sha256=dGVA7UO2jDN2RAk5UlI73k1p8zEXMjUEaP7Kwq3_Gzc,1006
|
|
|
2
2
|
fastapi_sso/pkce.py,sha256=QDqCH5f5EDmIG2MHfcAYbZJaURbl6w5IiuS6SHq89qA,792
|
|
3
3
|
fastapi_sso/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
fastapi_sso/sso/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
fastapi_sso/sso/base.py,sha256=
|
|
5
|
+
fastapi_sso/sso/base.py,sha256=PuxN9rEWLZ5c1WN751lFrGArVcOGipcpX43gPDbsbn8,21637
|
|
6
6
|
fastapi_sso/sso/facebook.py,sha256=JaGCT2v56iRRQlnoZ5OSsB19677gyMU7U5dZTjVE_mc,1368
|
|
7
7
|
fastapi_sso/sso/fitbit.py,sha256=CIqyyMDzZ0sYbZOtDNEotsoqq0Rvr5x4oP8_yWxfU7M,1301
|
|
8
8
|
fastapi_sso/sso/generic.py,sha256=VBzKVc2ckpuC4XqzlXQ56eg-xK8roV-uiUc2xsVseOI,2647
|
|
@@ -15,11 +15,12 @@ fastapi_sso/sso/linkedin.py,sha256=w6zSufyUNZguxnyfn62RPXrOGp48J9jKJL4Nx4NWit0,1
|
|
|
15
15
|
fastapi_sso/sso/microsoft.py,sha256=1RFIUeGZCVIBCr7kCvoOrt_FKcVRCrFO6boxnFpAioc,1960
|
|
16
16
|
fastapi_sso/sso/naver.py,sha256=IFFGPnUR1eOsS4JpubNLqOHDaQaqekPwDIZmm0NKRao,1149
|
|
17
17
|
fastapi_sso/sso/notion.py,sha256=YWGBUhN-CFTEpjIgkkEUS5JmGawyhJE57XB4Q9nFOD4,1334
|
|
18
|
+
fastapi_sso/sso/seznam.py,sha256=HGI01QPBYslv_b6Lq3oTL2UWPnNLPu45y3rgTdDqxpU,1383
|
|
18
19
|
fastapi_sso/sso/spotify.py,sha256=FvX2N91Bi3wgKRwdU1sWo-zA0s3wYJCiCYA05ebXweE,1244
|
|
19
20
|
fastapi_sso/sso/twitter.py,sha256=1kMjFdh-OT1b5bJvY3tWfl-BRBv2hVZ6L_liLAvNML8,1249
|
|
20
21
|
fastapi_sso/sso/yandex.py,sha256=8jKkh-na62lwsaBW7Pvj6VC6WlR0RMg8KrSNlr2Hj8o,1507
|
|
21
22
|
fastapi_sso/state.py,sha256=9RKMrFGjeN4Ab-3var81QV-gpcBlnNy152WYbxTUGVY,300
|
|
22
|
-
fastapi_sso-0.
|
|
23
|
-
fastapi_sso-0.
|
|
24
|
-
fastapi_sso-0.
|
|
25
|
-
fastapi_sso-0.
|
|
23
|
+
fastapi_sso-0.16.0.dist-info/LICENSE.md,sha256=5NVQtYs6liDtYdWM4VObWmTTKaK0k9C9txx5pLPJSyQ,1093
|
|
24
|
+
fastapi_sso-0.16.0.dist-info/METADATA,sha256=VCFezbjou97uivlibCnawLjRwRZ6H_iCiRVNCSW0fl8,7038
|
|
25
|
+
fastapi_sso-0.16.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
26
|
+
fastapi_sso-0.16.0.dist-info/RECORD,,
|
|
File without changes
|