fastapi-sso 0.17.0__tar.gz → 0.19.0__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.
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/PKG-INFO +17 -5
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/README.md +10 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/pkce.py +1 -2
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/base.py +65 -33
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/bitbucket.py +2 -2
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/discord.py +2 -2
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/generic.py +4 -4
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/gitlab.py +3 -3
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/linkedin.py +6 -2
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/microsoft.py +2 -2
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/naver.py +2 -2
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/pyproject.toml +11 -10
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/LICENSE.md +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/__init__.py +6 -6
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/py.typed +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/__init__.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/facebook.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/fitbit.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/github.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/google.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/kakao.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/line.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/notion.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/seznam.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/spotify.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/twitter.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/yandex.py +0 -0
- {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/state.py +0 -0
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-sso
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.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
|
-
Home-page: https://tomasvotava.github.io/fastapi-sso/
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE.md
|
|
7
7
|
Keywords: fastapi,sso,oauth,google,facebook,spotify,linkedin
|
|
8
8
|
Author: Tomas Votava
|
|
9
9
|
Author-email: info@tomasvotava.eu
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9,<4.0
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Requires-Dist: fastapi (>=0.80)
|
|
20
20
|
Requires-Dist: httpx (>=0.23.0)
|
|
21
21
|
Requires-Dist: oauthlib (>=3.1.0)
|
|
22
22
|
Requires-Dist: pydantic[email] (>=1.8.0)
|
|
23
|
+
Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
|
|
23
24
|
Requires-Dist: typing-extensions (>=4.12.2,<5.0.0) ; python_version < "3.10"
|
|
24
25
|
Project-URL: Documentation, https://tomasvotava.github.io/fastapi-sso/
|
|
26
|
+
Project-URL: Homepage, https://tomasvotava.github.io/fastapi-sso/
|
|
25
27
|
Project-URL: Repository, https://github.com/tomasvotava/fastapi-sso
|
|
26
28
|
Description-Content-Type: text/markdown
|
|
27
29
|
|
|
@@ -73,6 +75,16 @@ by [@parikls](https://github.com/parikls).
|
|
|
73
75
|
This issue was reported in [#186](https://github.com/tomasvotava/fastapi-sso/issues/186) and has been resolved
|
|
74
76
|
in version `0.16.0`.
|
|
75
77
|
|
|
78
|
+
### Version `0.19.0` Update: OAuth `state` Validation Fix
|
|
79
|
+
|
|
80
|
+
A critical OAuth login CSRF vulnerability caused by missing `state` validation was
|
|
81
|
+
reported by [@davidbors-snyk](https://github.com/davidbors-snyk) (Snyk Security Labs)
|
|
82
|
+
in [#266](https://github.com/tomasvotava/fastapi-sso/issues/266) and has been resolved
|
|
83
|
+
in version `0.19.0`.
|
|
84
|
+
|
|
85
|
+
Starting with `fastapi-sso==1.0.0`, OAuth `state` will be backed by a pluggable server-side store
|
|
86
|
+
(in-memory by default, with support for external stores such as `Redis`).
|
|
87
|
+
|
|
76
88
|
**Details of the Fix:**
|
|
77
89
|
|
|
78
90
|
The bug was mitigated by introducing an async lock mechanism that ensures only one user can attempt the login
|
|
@@ -46,6 +46,16 @@ by [@parikls](https://github.com/parikls).
|
|
|
46
46
|
This issue was reported in [#186](https://github.com/tomasvotava/fastapi-sso/issues/186) and has been resolved
|
|
47
47
|
in version `0.16.0`.
|
|
48
48
|
|
|
49
|
+
### Version `0.19.0` Update: OAuth `state` Validation Fix
|
|
50
|
+
|
|
51
|
+
A critical OAuth login CSRF vulnerability caused by missing `state` validation was
|
|
52
|
+
reported by [@davidbors-snyk](https://github.com/davidbors-snyk) (Snyk Security Labs)
|
|
53
|
+
in [#266](https://github.com/tomasvotava/fastapi-sso/issues/266) and has been resolved
|
|
54
|
+
in version `0.19.0`.
|
|
55
|
+
|
|
56
|
+
Starting with `fastapi-sso==1.0.0`, OAuth `state` will be backed by a pluggable server-side store
|
|
57
|
+
(in-memory by default, with support for external stores such as `Redis`).
|
|
58
|
+
|
|
49
59
|
**Details of the Fix:**
|
|
50
60
|
|
|
51
61
|
The bug was mitigated by introducing an async lock mechanism that ensures only one user can attempt the login
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import base64
|
|
4
4
|
import hashlib
|
|
5
5
|
import os
|
|
6
|
-
from typing import Tuple
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def get_code_verifier(length: int = 96) -> str:
|
|
@@ -13,7 +12,7 @@ def get_code_verifier(length: int = 96) -> str:
|
|
|
13
12
|
return base64.urlsafe_b64encode(os.urandom(bytes_length)).decode("utf-8").replace("=", "")[:length]
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
def get_pkce_challenge_pair(verifier_length: int = 96) ->
|
|
15
|
+
def get_pkce_challenge_pair(verifier_length: int = 96) -> tuple[str, str]:
|
|
17
16
|
"""Get tuple of (verifier, challenge) for PKCE challenge."""
|
|
18
17
|
code_verifier = get_code_verifier(verifier_length)
|
|
19
18
|
code_challenge = (
|
|
@@ -7,9 +7,10 @@ import os
|
|
|
7
7
|
import sys
|
|
8
8
|
import warnings
|
|
9
9
|
from types import TracebackType
|
|
10
|
-
from typing import Any, ClassVar,
|
|
10
|
+
from typing import Any, ClassVar, Literal, Optional, TypedDict, TypeVar, Union, overload
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
|
+
import jwt
|
|
13
14
|
import pydantic
|
|
14
15
|
from oauthlib.oauth2 import WebApplicationClient
|
|
15
16
|
from starlette.exceptions import HTTPException
|
|
@@ -33,6 +34,10 @@ T = TypeVar("T")
|
|
|
33
34
|
P = ParamSpec("P")
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
def _decode_id_token(id_token: str, verify: bool = False) -> dict:
|
|
38
|
+
return jwt.decode(id_token, options={"verify_signature": verify})
|
|
39
|
+
|
|
40
|
+
|
|
36
41
|
class DiscoveryDocument(TypedDict):
|
|
37
42
|
"""Discovery document."""
|
|
38
43
|
|
|
@@ -95,10 +100,11 @@ class SSOBase:
|
|
|
95
100
|
client_id: str = NotImplemented
|
|
96
101
|
client_secret: str = NotImplemented
|
|
97
102
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = NotImplemented
|
|
98
|
-
scope: ClassVar[
|
|
99
|
-
additional_headers: ClassVar[Optional[
|
|
103
|
+
scope: ClassVar[list[str]] = []
|
|
104
|
+
additional_headers: ClassVar[Optional[dict[str, Any]]] = None
|
|
100
105
|
uses_pkce: bool = False
|
|
101
106
|
requires_state: bool = False
|
|
107
|
+
use_id_token_for_user_info: ClassVar[bool] = False
|
|
102
108
|
|
|
103
109
|
_pkce_challenge_length: int = 96
|
|
104
110
|
|
|
@@ -109,7 +115,7 @@ class SSOBase:
|
|
|
109
115
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
|
|
110
116
|
allow_insecure_http: bool = False,
|
|
111
117
|
use_state: bool = False,
|
|
112
|
-
scope: Optional[
|
|
118
|
+
scope: Optional[list[str]] = None,
|
|
113
119
|
):
|
|
114
120
|
"""Base class (mixin) for all SSO providers."""
|
|
115
121
|
self.client_id: str = client_id
|
|
@@ -224,6 +230,18 @@ class SSOBase:
|
|
|
224
230
|
"""
|
|
225
231
|
raise NotImplementedError(f"Provider {self.provider} not supported")
|
|
226
232
|
|
|
233
|
+
async def openid_from_token(self, id_token: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID:
|
|
234
|
+
"""Converts an ID token from the provider's token endpoint to an OpenID object.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
id_token (dict): The id token data retrieved from the token endpoint.
|
|
238
|
+
session: (Optional[httpx.AsyncClient]): The HTTPX AsyncClient session.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
OpenID: The user information in a standardized format.
|
|
242
|
+
"""
|
|
243
|
+
raise NotImplementedError(f"Provider {self.provider} not supported")
|
|
244
|
+
|
|
227
245
|
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
228
246
|
"""Retrieves the discovery document containing useful URLs.
|
|
229
247
|
|
|
@@ -257,14 +275,14 @@ class SSOBase:
|
|
|
257
275
|
self,
|
|
258
276
|
*,
|
|
259
277
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
|
|
260
|
-
params: Optional[
|
|
278
|
+
params: Optional[dict[str, Any]] = None,
|
|
261
279
|
state: Optional[str] = None,
|
|
262
280
|
) -> str:
|
|
263
281
|
"""Generates and returns the prepared login URL.
|
|
264
282
|
|
|
265
283
|
Args:
|
|
266
284
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
267
|
-
params (Optional[
|
|
285
|
+
params (Optional[dict[str, Any]]): Additional query parameters to add to the login request.
|
|
268
286
|
state (Optional[str]): The state parameter for the OAuth 2.0 authorization request.
|
|
269
287
|
|
|
270
288
|
Raises:
|
|
@@ -304,14 +322,14 @@ class SSOBase:
|
|
|
304
322
|
self,
|
|
305
323
|
*,
|
|
306
324
|
redirect_uri: Optional[str] = None,
|
|
307
|
-
params: Optional[
|
|
325
|
+
params: Optional[dict[str, Any]] = None,
|
|
308
326
|
state: Optional[str] = None,
|
|
309
327
|
) -> RedirectResponse:
|
|
310
328
|
"""Constructs and returns a redirect response to the login page of OAuth SSO provider.
|
|
311
329
|
|
|
312
330
|
Args:
|
|
313
331
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
314
|
-
params (Optional[
|
|
332
|
+
params (Optional[dict[str, Any]]): Additional query parameters to add to the login request.
|
|
315
333
|
state (Optional[str]): The state parameter for the OAuth 2.0 authorization request.
|
|
316
334
|
|
|
317
335
|
Returns:
|
|
@@ -323,6 +341,8 @@ class SSOBase:
|
|
|
323
341
|
response = RedirectResponse(login_uri, 303)
|
|
324
342
|
if self.uses_pkce:
|
|
325
343
|
response.set_cookie("pkce_code_verifier", str(self._pkce_code_verifier))
|
|
344
|
+
if state is not None:
|
|
345
|
+
response.set_cookie("sso_state", state)
|
|
326
346
|
return response
|
|
327
347
|
|
|
328
348
|
@overload
|
|
@@ -330,8 +350,8 @@ class SSOBase:
|
|
|
330
350
|
self,
|
|
331
351
|
request: Request,
|
|
332
352
|
*,
|
|
333
|
-
params: Optional[
|
|
334
|
-
headers: Optional[
|
|
353
|
+
params: Optional[dict[str, Any]] = None,
|
|
354
|
+
headers: Optional[dict[str, Any]] = None,
|
|
335
355
|
redirect_uri: Optional[str] = None,
|
|
336
356
|
convert_response: Literal[True] = True,
|
|
337
357
|
) -> Optional[OpenID]: ...
|
|
@@ -341,28 +361,28 @@ class SSOBase:
|
|
|
341
361
|
self,
|
|
342
362
|
request: Request,
|
|
343
363
|
*,
|
|
344
|
-
params: Optional[
|
|
345
|
-
headers: Optional[
|
|
364
|
+
params: Optional[dict[str, Any]] = None,
|
|
365
|
+
headers: Optional[dict[str, Any]] = None,
|
|
346
366
|
redirect_uri: Optional[str] = None,
|
|
347
367
|
convert_response: Literal[False],
|
|
348
|
-
) -> Optional[
|
|
368
|
+
) -> Optional[dict[str, Any]]: ...
|
|
349
369
|
|
|
350
370
|
@requires_async_context
|
|
351
371
|
async def verify_and_process(
|
|
352
372
|
self,
|
|
353
373
|
request: Request,
|
|
354
374
|
*,
|
|
355
|
-
params: Optional[
|
|
356
|
-
headers: Optional[
|
|
375
|
+
params: Optional[dict[str, Any]] = None,
|
|
376
|
+
headers: Optional[dict[str, Any]] = None,
|
|
357
377
|
redirect_uri: Optional[str] = None,
|
|
358
378
|
convert_response: Union[Literal[True], Literal[False]] = True,
|
|
359
|
-
) -> Union[Optional[OpenID], Optional[
|
|
379
|
+
) -> Union[Optional[OpenID], Optional[dict[str, Any]]]:
|
|
360
380
|
"""Processes the login given a FastAPI (Starlette) Request object. This should be used for the /callback path.
|
|
361
381
|
|
|
362
382
|
Args:
|
|
363
383
|
request (Request): FastAPI or Starlette request object.
|
|
364
|
-
params (Optional[
|
|
365
|
-
headers (Optional[
|
|
384
|
+
params (Optional[dict[str, Any]]): Additional query parameters to pass to the provider.
|
|
385
|
+
headers (Optional[dict[str, Any]]): Additional headers to pass to the provider.
|
|
366
386
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
367
387
|
convert_response (bool): If True, userinfo response is converted to OpenID object.
|
|
368
388
|
|
|
@@ -371,7 +391,7 @@ class SSOBase:
|
|
|
371
391
|
|
|
372
392
|
Returns:
|
|
373
393
|
Optional[OpenID]: User information as OpenID instance (if convert_response == True)
|
|
374
|
-
Optional[
|
|
394
|
+
Optional[dict[str, Any]]: The original JSON response from the API.
|
|
375
395
|
"""
|
|
376
396
|
headers = headers or {}
|
|
377
397
|
code = request.query_params.get("code")
|
|
@@ -384,6 +404,14 @@ class SSOBase:
|
|
|
384
404
|
)
|
|
385
405
|
raise SSOLoginError(400, "'code' parameter was not found in callback request")
|
|
386
406
|
self._state = request.query_params.get("state")
|
|
407
|
+
if self._state is None and self.requires_state:
|
|
408
|
+
raise SSOLoginError(400, "'state' parameter was not found in callback request")
|
|
409
|
+
if self._state is not None:
|
|
410
|
+
sso_state = request.cookies.get("sso_state")
|
|
411
|
+
if sso_state is None and self.requires_state:
|
|
412
|
+
raise SSOLoginError(401, "State cookie not found")
|
|
413
|
+
if sso_state is not None and sso_state != self._state:
|
|
414
|
+
raise SSOLoginError(401, "Invalid state")
|
|
387
415
|
pkce_code_verifier: Optional[str] = None
|
|
388
416
|
if self.uses_pkce:
|
|
389
417
|
pkce_code_verifier = request.cookies.get("pkce_code_verifier")
|
|
@@ -433,7 +461,7 @@ class SSOBase:
|
|
|
433
461
|
|
|
434
462
|
async def __aexit__(
|
|
435
463
|
self,
|
|
436
|
-
_exc_type: Optional[
|
|
464
|
+
_exc_type: Optional[type[BaseException]],
|
|
437
465
|
_exc_val: Optional[BaseException],
|
|
438
466
|
_exc_tb: Optional[TracebackType],
|
|
439
467
|
) -> None:
|
|
@@ -442,14 +470,14 @@ class SSOBase:
|
|
|
442
470
|
|
|
443
471
|
def __exit__(
|
|
444
472
|
self,
|
|
445
|
-
_exc_type: Optional[
|
|
473
|
+
_exc_type: Optional[type[BaseException]],
|
|
446
474
|
_exc_val: Optional[BaseException],
|
|
447
475
|
_exc_tb: Optional[TracebackType],
|
|
448
476
|
) -> None:
|
|
449
477
|
return None
|
|
450
478
|
|
|
451
479
|
@property
|
|
452
|
-
def _extra_query_params(self) ->
|
|
480
|
+
def _extra_query_params(self) -> dict:
|
|
453
481
|
return {}
|
|
454
482
|
|
|
455
483
|
@overload
|
|
@@ -458,8 +486,8 @@ class SSOBase:
|
|
|
458
486
|
code: str,
|
|
459
487
|
request: Request,
|
|
460
488
|
*,
|
|
461
|
-
params: Optional[
|
|
462
|
-
additional_headers: Optional[
|
|
489
|
+
params: Optional[dict[str, Any]] = None,
|
|
490
|
+
additional_headers: Optional[dict[str, Any]] = None,
|
|
463
491
|
redirect_uri: Optional[str] = None,
|
|
464
492
|
pkce_code_verifier: Optional[str] = None,
|
|
465
493
|
convert_response: Literal[True] = True,
|
|
@@ -471,12 +499,12 @@ class SSOBase:
|
|
|
471
499
|
code: str,
|
|
472
500
|
request: Request,
|
|
473
501
|
*,
|
|
474
|
-
params: Optional[
|
|
475
|
-
additional_headers: Optional[
|
|
502
|
+
params: Optional[dict[str, Any]] = None,
|
|
503
|
+
additional_headers: Optional[dict[str, Any]] = None,
|
|
476
504
|
redirect_uri: Optional[str] = None,
|
|
477
505
|
pkce_code_verifier: Optional[str] = None,
|
|
478
506
|
convert_response: Literal[False],
|
|
479
|
-
) -> Optional[
|
|
507
|
+
) -> Optional[dict[str, Any]]: ...
|
|
480
508
|
|
|
481
509
|
@requires_async_context
|
|
482
510
|
async def process_login(
|
|
@@ -484,20 +512,20 @@ class SSOBase:
|
|
|
484
512
|
code: str,
|
|
485
513
|
request: Request,
|
|
486
514
|
*,
|
|
487
|
-
params: Optional[
|
|
488
|
-
additional_headers: Optional[
|
|
515
|
+
params: Optional[dict[str, Any]] = None,
|
|
516
|
+
additional_headers: Optional[dict[str, Any]] = None,
|
|
489
517
|
redirect_uri: Optional[str] = None,
|
|
490
518
|
pkce_code_verifier: Optional[str] = None,
|
|
491
519
|
convert_response: Union[Literal[True], Literal[False]] = True,
|
|
492
|
-
) -> Union[Optional[OpenID], Optional[
|
|
520
|
+
) -> Union[Optional[OpenID], Optional[dict[str, Any]]]:
|
|
493
521
|
"""Processes login from the callback endpoint to verify the user and request user info endpoint.
|
|
494
522
|
It's a lower-level method, typically, you should use `verify_and_process` instead.
|
|
495
523
|
|
|
496
524
|
Args:
|
|
497
525
|
code (str): The authorization code.
|
|
498
526
|
request (Request): FastAPI or Starlette request object.
|
|
499
|
-
params (Optional[
|
|
500
|
-
additional_headers (Optional[
|
|
527
|
+
params (Optional[dict[str, Any]]): Additional query parameters to pass to the provider.
|
|
528
|
+
additional_headers (Optional[dict[str, Any]]): Additional headers to be added to all requests.
|
|
501
529
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
502
530
|
pkce_code_verifier (Optional[str]): A PKCE code verifier sent to the server to verify the login request.
|
|
503
531
|
convert_response (bool): If True, userinfo response is converted to OpenID object.
|
|
@@ -507,7 +535,7 @@ class SSOBase:
|
|
|
507
535
|
|
|
508
536
|
Returns:
|
|
509
537
|
Optional[OpenID]: User information in OpenID format if the login was successful (convert_response == True).
|
|
510
|
-
Optional[
|
|
538
|
+
Optional[dict[str, Any]]: Original userinfo API endpoint response.
|
|
511
539
|
"""
|
|
512
540
|
if self._oauth_client is not None: # pragma: no cover
|
|
513
541
|
self._oauth_client = None
|
|
@@ -565,5 +593,9 @@ class SSOBase:
|
|
|
565
593
|
response = await session.get(uri)
|
|
566
594
|
content = response.json()
|
|
567
595
|
if convert_response:
|
|
596
|
+
if self.use_id_token_for_user_info:
|
|
597
|
+
if not self._id_token:
|
|
598
|
+
raise SSOLoginError(401, f"Provider {self.provider!r} did not return id token.")
|
|
599
|
+
return await self.openid_from_token(_decode_id_token(self._id_token), session)
|
|
568
600
|
return await self.openid_from_response(content, session)
|
|
569
601
|
return content
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""BitBucket SSO Oauth Helper class"""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar,
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Optional, Union
|
|
4
4
|
|
|
5
5
|
import pydantic
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ class BitbucketSSO(SSOBase):
|
|
|
23
23
|
client_secret: str,
|
|
24
24
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
|
|
25
25
|
allow_insecure_http: bool = False,
|
|
26
|
-
scope: Optional[
|
|
26
|
+
scope: Optional[list[str]] = None,
|
|
27
27
|
):
|
|
28
28
|
super().__init__(
|
|
29
29
|
client_id=client_id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Discord SSO Oauth Helper class"""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar,
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Optional, Union
|
|
4
4
|
|
|
5
5
|
import pydantic
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ class DiscordSSO(SSOBase):
|
|
|
22
22
|
client_secret: str,
|
|
23
23
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
|
|
24
24
|
allow_insecure_http: bool = False,
|
|
25
|
-
scope: Optional[
|
|
25
|
+
scope: Optional[list[str]] = None,
|
|
26
26
|
):
|
|
27
27
|
super().__init__(
|
|
28
28
|
client_id=client_id,
|
|
@@ -3,7 +3,7 @@ with close to no code.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
|
7
7
|
|
|
8
8
|
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
9
9
|
|
|
@@ -16,10 +16,10 @@ logger = logging.getLogger(__name__)
|
|
|
16
16
|
def create_provider(
|
|
17
17
|
*,
|
|
18
18
|
name: str = "generic",
|
|
19
|
-
default_scope: Optional[
|
|
19
|
+
default_scope: Optional[list[str]] = None,
|
|
20
20
|
discovery_document: Union[DiscoveryDocument, Callable[[SSOBase], DiscoveryDocument]],
|
|
21
|
-
response_convertor: Optional[Callable[[
|
|
22
|
-
) ->
|
|
21
|
+
response_convertor: Optional[Callable[[dict[str, Any], Optional["httpx.AsyncClient"]], OpenID]] = None
|
|
22
|
+
) -> type[SSOBase]:
|
|
23
23
|
"""A factory to create a generic OAuth client usable with almost any OAuth provider.
|
|
24
24
|
Returns a class.
|
|
25
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Gitlab SSO Oauth Helper class."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar,
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Optional, Union
|
|
4
4
|
from urllib.parse import urljoin
|
|
5
5
|
|
|
6
6
|
import pydantic
|
|
@@ -26,7 +26,7 @@ class GitlabSSO(SSOBase):
|
|
|
26
26
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
|
|
27
27
|
allow_insecure_http: bool = False,
|
|
28
28
|
use_state: bool = False, # TODO: Remove use_state argument
|
|
29
|
-
scope: Optional[
|
|
29
|
+
scope: Optional[list[str]] = None,
|
|
30
30
|
base_endpoint_url: Optional[str] = None,
|
|
31
31
|
) -> None:
|
|
32
32
|
super().__init__(
|
|
@@ -47,7 +47,7 @@ class GitlabSSO(SSOBase):
|
|
|
47
47
|
"userinfo_endpoint": urljoin(self.base_endpoint_url, "/api/v4/user"),
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
def _parse_name(self, full_name: Optional[str]) ->
|
|
50
|
+
def _parse_name(self, full_name: Optional[str]) -> tuple[Union[str, None], Union[str, None]]:
|
|
51
51
|
"""Parses the full name from Gitlab into the first and last name."""
|
|
52
52
|
if not full_name or not isinstance(full_name, str):
|
|
53
53
|
return None, None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""LinkedIn SSO Oauth Helper class."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar,
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Optional
|
|
4
4
|
|
|
5
5
|
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
6
6
|
|
|
@@ -14,9 +14,10 @@ class LinkedInSSO(SSOBase):
|
|
|
14
14
|
provider = "linkedin"
|
|
15
15
|
scope: ClassVar = ["openid", "profile", "email"]
|
|
16
16
|
additional_headers: ClassVar = {"accept": "application/json"}
|
|
17
|
+
use_id_token_for_user_info: ClassVar = True
|
|
17
18
|
|
|
18
19
|
@property
|
|
19
|
-
def _extra_query_params(self) ->
|
|
20
|
+
def _extra_query_params(self) -> dict:
|
|
20
21
|
return {"client_secret": self.client_secret}
|
|
21
22
|
|
|
22
23
|
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
@@ -26,6 +27,9 @@ class LinkedInSSO(SSOBase):
|
|
|
26
27
|
"userinfo_endpoint": "https://api.linkedin.com/v2/userinfo",
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
async def openid_from_token(self, id_token: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
|
|
31
|
+
return await self.openid_from_response(id_token, session)
|
|
32
|
+
|
|
29
33
|
async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
|
|
30
34
|
return OpenID(
|
|
31
35
|
email=response.get("email"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Microsoft SSO Oauth Helper class."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar,
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Optional, Union
|
|
4
4
|
|
|
5
5
|
import pydantic
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ class MicrosoftSSO(SSOBase):
|
|
|
25
25
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
|
|
26
26
|
allow_insecure_http: bool = False,
|
|
27
27
|
use_state: bool = False, # TODO: Remove use_state argument
|
|
28
|
-
scope: Optional[
|
|
28
|
+
scope: Optional[list[str]] = None,
|
|
29
29
|
tenant: Optional[str] = None,
|
|
30
30
|
):
|
|
31
31
|
super().__init__(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Naver SSO Oauth Helper class."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar,
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Optional
|
|
4
4
|
|
|
5
5
|
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ class NaverSSO(SSOBase):
|
|
|
12
12
|
"""Class providing login using Naver OAuth."""
|
|
13
13
|
|
|
14
14
|
provider = "naver"
|
|
15
|
-
scope: ClassVar[
|
|
15
|
+
scope: ClassVar[list[str]] = []
|
|
16
16
|
additional_headers: ClassVar = {"accept": "application/json"}
|
|
17
17
|
|
|
18
18
|
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "fastapi-sso"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.0"
|
|
4
4
|
description = "FastAPI plugin to enable SSO to most common providers (such as Facebook login, Google login and login via Microsoft Office 365 Account)"
|
|
5
5
|
authors = ["Tomas Votava <info@tomasvotava.eu>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -36,7 +36,7 @@ addopts = [
|
|
|
36
36
|
line-length = 120
|
|
37
37
|
|
|
38
38
|
[tool.ruff]
|
|
39
|
-
target-version = "
|
|
39
|
+
target-version = "py39"
|
|
40
40
|
line-length = 120
|
|
41
41
|
|
|
42
42
|
[tool.ruff.lint]
|
|
@@ -92,26 +92,27 @@ docs = "mkdocs build --clean"
|
|
|
92
92
|
|
|
93
93
|
[tool.poetry.group.dev.dependencies]
|
|
94
94
|
black = ">=23.7.0"
|
|
95
|
-
isort = "
|
|
95
|
+
isort = ">=5,<7"
|
|
96
96
|
markdown-include = "^0.8.1"
|
|
97
97
|
mkdocs-material = { extras = ["imaging"], version = "^9.3.2" }
|
|
98
|
-
mkdocstrings = { extras = ["python"], version = ">=0.23,<0.
|
|
98
|
+
mkdocstrings = { extras = ["python"], version = ">=0.23,<0.31" }
|
|
99
99
|
mypy = "^1"
|
|
100
|
-
poethepoet = ">=0.21.1,<0.
|
|
101
|
-
pre-commit = "
|
|
100
|
+
poethepoet = ">=0.21.1,<0.38.0"
|
|
101
|
+
pre-commit = ">=3,<5"
|
|
102
102
|
pytest = ">=7,<9"
|
|
103
|
-
pytest-asyncio = "
|
|
104
|
-
pytest-cov = ">=4,<
|
|
103
|
+
pytest-asyncio = ">=0.24,<1.3"
|
|
104
|
+
pytest-cov = ">=4,<8"
|
|
105
105
|
uvicorn = ">=0.23.1"
|
|
106
|
-
ruff = ">=0.4.2,<0.
|
|
106
|
+
ruff = ">=0.4.2,<0.15.0"
|
|
107
107
|
|
|
108
108
|
[tool.poetry.dependencies]
|
|
109
109
|
fastapi = ">=0.80"
|
|
110
110
|
httpx = ">=0.23.0"
|
|
111
111
|
oauthlib = ">=3.1.0"
|
|
112
112
|
pydantic = { extras = ["email"], version = ">=1.8.0" }
|
|
113
|
-
python = ">=3.
|
|
113
|
+
python = ">=3.9,<4.0"
|
|
114
114
|
typing-extensions = { version = "^4.12.2", python = "<3.10" }
|
|
115
|
+
pyjwt = "^2.10.1"
|
|
115
116
|
|
|
116
117
|
[build-system]
|
|
117
118
|
requires = ["poetry-core>=1.0.0"]
|
|
File without changes
|
|
@@ -22,12 +22,10 @@ from .sso.spotify import SpotifySSO
|
|
|
22
22
|
from .sso.twitter import TwitterSSO
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"SSOLoginError",
|
|
25
|
+
"BitbucketSSO",
|
|
26
|
+
"DiscordSSO",
|
|
28
27
|
"FacebookSSO",
|
|
29
28
|
"FitbitSSO",
|
|
30
|
-
"create_provider",
|
|
31
29
|
"GithubSSO",
|
|
32
30
|
"GitlabSSO",
|
|
33
31
|
"GoogleSSO",
|
|
@@ -37,8 +35,10 @@ __all__ = [
|
|
|
37
35
|
"MicrosoftSSO",
|
|
38
36
|
"NaverSSO",
|
|
39
37
|
"NotionSSO",
|
|
38
|
+
"OpenID",
|
|
39
|
+
"SSOBase",
|
|
40
|
+
"SSOLoginError",
|
|
40
41
|
"SpotifySSO",
|
|
41
42
|
"TwitterSSO",
|
|
42
|
-
"
|
|
43
|
-
"DiscordSSO",
|
|
43
|
+
"create_provider",
|
|
44
44
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|