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.
Files changed (28) hide show
  1. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/PKG-INFO +17 -5
  2. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/README.md +10 -0
  3. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/pkce.py +1 -2
  4. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/base.py +65 -33
  5. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/bitbucket.py +2 -2
  6. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/discord.py +2 -2
  7. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/generic.py +4 -4
  8. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/gitlab.py +3 -3
  9. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/linkedin.py +6 -2
  10. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/microsoft.py +2 -2
  11. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/naver.py +2 -2
  12. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/pyproject.toml +11 -10
  13. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/LICENSE.md +0 -0
  14. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/__init__.py +6 -6
  15. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/py.typed +0 -0
  16. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/__init__.py +0 -0
  17. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/facebook.py +0 -0
  18. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/fitbit.py +0 -0
  19. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/github.py +0 -0
  20. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/google.py +0 -0
  21. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/kakao.py +0 -0
  22. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/line.py +0 -0
  23. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/notion.py +0 -0
  24. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/seznam.py +0 -0
  25. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/spotify.py +0 -0
  26. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/twitter.py +0 -0
  27. {fastapi_sso-0.17.0 → fastapi_sso-0.19.0}/fastapi_sso/sso/yandex.py +0 -0
  28. {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
1
+ Metadata-Version: 2.4
2
2
  Name: fastapi-sso
3
- Version: 0.17.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.8,<4.0
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) -> Tuple[str, str]:
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, Dict, List, Literal, Optional, Type, TypedDict, TypeVar, Union, overload
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[List[str]] = []
99
- additional_headers: ClassVar[Optional[Dict[str, Any]]] = None
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[List[str]] = None,
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[Dict[str, Any]] = None,
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[Dict[str, Any]]): Additional query parameters to add to the login request.
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[Dict[str, Any]] = None,
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[Dict[str, Any]]): Additional query parameters to add to the login request.
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[Dict[str, Any]] = None,
334
- headers: Optional[Dict[str, Any]] = None,
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[Dict[str, Any]] = None,
345
- headers: Optional[Dict[str, Any]] = None,
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[Dict[str, Any]]: ...
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[Dict[str, Any]] = None,
356
- headers: Optional[Dict[str, Any]] = None,
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[Dict[str, Any]]]:
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[Dict[str, Any]]): Additional query parameters to pass to the provider.
365
- headers (Optional[Dict[str, Any]]): Additional headers to pass to the provider.
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[Dict[str, Any]]: The original JSON response from the API.
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[Type[BaseException]],
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[Type[BaseException]],
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) -> Dict:
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[Dict[str, Any]] = None,
462
- additional_headers: Optional[Dict[str, Any]] = None,
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[Dict[str, Any]] = None,
475
- additional_headers: Optional[Dict[str, Any]] = None,
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[Dict[str, Any]]: ...
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[Dict[str, Any]] = None,
488
- additional_headers: Optional[Dict[str, Any]] = None,
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[Dict[str, Any]]]:
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[Dict[str, Any]]): Additional query parameters to pass to the provider.
500
- additional_headers (Optional[Dict[str, Any]]): Additional headers to be added to all requests.
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[Dict[str, Any]]: Original userinfo API endpoint response.
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, List, Optional, Union
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[List[str]] = None,
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, List, Optional, Union
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[List[str]] = None,
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, Dict, List, Optional, Type, Union
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[List[str]] = None,
19
+ default_scope: Optional[list[str]] = None,
20
20
  discovery_document: Union[DiscoveryDocument, Callable[[SSOBase], DiscoveryDocument]],
21
- response_convertor: Optional[Callable[[Dict[str, Any], Optional["httpx.AsyncClient"]], OpenID]] = None
22
- ) -> Type[SSOBase]:
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, List, Optional, Tuple, Union
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[List[str]] = None,
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]) -> Tuple[Union[str, None], Union[str, None]]:
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, Dict, Optional
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) -> Dict:
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, List, Optional, Union
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[List[str]] = None,
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, List, Optional
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[List[str]] = []
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.17.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 = "py38"
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 = "^5"
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.27" }
98
+ mkdocstrings = { extras = ["python"], version = ">=0.23,<0.31" }
99
99
  mypy = "^1"
100
- poethepoet = ">=0.21.1,<0.30.0"
101
- pre-commit = "^3"
100
+ poethepoet = ">=0.21.1,<0.38.0"
101
+ pre-commit = ">=3,<5"
102
102
  pytest = ">=7,<9"
103
- pytest-asyncio = "^0.24"
104
- pytest-cov = ">=4,<6"
103
+ pytest-asyncio = ">=0.24,<1.3"
104
+ pytest-cov = ">=4,<8"
105
105
  uvicorn = ">=0.23.1"
106
- ruff = ">=0.4.2,<0.8.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.8,<4.0"
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
- "OpenID",
26
- "SSOBase",
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
- "BitbucketSSO",
43
- "DiscordSSO",
43
+ "create_provider",
44
44
  ]