fastapi-sso 0.14.2__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/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
- """FastAPI plugin to enable SSO to most common providers
1
+ """FastAPI plugin to enable SSO to most common providers.
2
+
2
3
  (such as Facebook login, Google login and login via Microsoft Office 365 account)
3
4
  """
4
5
 
@@ -17,3 +18,23 @@ from .sso.naver import NaverSSO
17
18
  from .sso.notion import NotionSSO
18
19
  from .sso.spotify import SpotifySSO
19
20
  from .sso.twitter import TwitterSSO
21
+
22
+ __all__ = [
23
+ "OpenID",
24
+ "SSOBase",
25
+ "SSOLoginError",
26
+ "FacebookSSO",
27
+ "FitbitSSO",
28
+ "create_provider",
29
+ "GithubSSO",
30
+ "GitlabSSO",
31
+ "GoogleSSO",
32
+ "KakaoSSO",
33
+ "LineSSO",
34
+ "LinkedInSSO",
35
+ "MicrosoftSSO",
36
+ "NaverSSO",
37
+ "NotionSSO",
38
+ "SpotifySSO",
39
+ "TwitterSSO",
40
+ ]
fastapi_sso/pkce.py CHANGED
@@ -1,4 +1,4 @@
1
- """PKCE-related helper functions"""
1
+ """PKCE-related helper functions."""
2
2
 
3
3
  import base64
4
4
  import hashlib
@@ -7,7 +7,7 @@ from typing import Tuple
7
7
 
8
8
 
9
9
  def get_code_verifier(length: int = 96) -> str:
10
- """Get code verifier for PKCE challenge"""
10
+ """Get code verifier for PKCE challenge."""
11
11
  length = max(43, min(length, 128))
12
12
  bytes_length = int(length * 3 / 4)
13
13
  return base64.urlsafe_b64encode(os.urandom(bytes_length)).decode("utf-8").replace("=", "")[:length]
fastapi_sso/sso/base.py CHANGED
@@ -1,14 +1,13 @@
1
- """SSO login base dependency
2
- """
3
-
4
- # pylint: disable=too-few-public-methods
1
+ """SSO login base dependency."""
5
2
 
3
+ import asyncio
6
4
  import json
5
+ import logging
7
6
  import os
8
7
  import sys
9
8
  import warnings
10
9
  from types import TracebackType
11
- from typing import Any, Dict, List, Literal, Optional, Type, Union, overload
10
+ from typing import Any, ClassVar, Dict, List, Literal, Optional, Type, TypedDict, TypeVar, Union, overload
12
11
 
13
12
  import httpx
14
13
  import pydantic
@@ -20,31 +19,44 @@ from starlette.responses import RedirectResponse
20
19
  from fastapi_sso.pkce import get_pkce_challenge_pair
21
20
  from fastapi_sso.state import generate_random_state
22
21
 
23
- if sys.version_info >= (3, 8):
24
- from typing import TypedDict
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
25
26
  else:
26
- from typing_extensions import TypedDict # pragma: no cover
27
+ from collections.abc import Callable
28
+ from typing import ParamSpec
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ T = TypeVar("T")
33
+ P = ParamSpec("P")
34
+
35
+
36
+ class DiscoveryDocument(TypedDict):
37
+ """Discovery document."""
27
38
 
28
- DiscoveryDocument = TypedDict(
29
- "DiscoveryDocument", {"authorization_endpoint": str, "token_endpoint": str, "userinfo_endpoint": str}
30
- )
39
+ authorization_endpoint: str
40
+ token_endpoint: str
41
+ userinfo_endpoint: str
31
42
 
32
43
 
33
44
  class UnsetStateWarning(UserWarning):
34
- """Warning about unset state parameter"""
45
+ """Warning about unset state parameter."""
35
46
 
36
47
 
37
48
  class ReusedOauthClientWarning(UserWarning):
38
- """Warning about reused oauth client instance"""
49
+ """Warning about reused oauth client instance."""
39
50
 
40
51
 
41
52
  class SSOLoginError(HTTPException):
42
- """Raised when any login-related error ocurrs
43
- (such as when user is not verified or if there was an attempt for fake login)
53
+ """Raised when any login-related error ocurrs.
54
+
55
+ Such as when user is not verified or if there was an attempt for fake login.
44
56
  """
45
57
 
46
58
 
47
- class OpenID(pydantic.BaseModel): # pylint: disable=no-member
59
+ class OpenID(pydantic.BaseModel):
48
60
  """Class (schema) to represent information got from sso provider in a common form."""
49
61
 
50
62
  id: Optional[str] = None
@@ -56,16 +68,35 @@ class OpenID(pydantic.BaseModel): # pylint: disable=no-member
56
68
  provider: Optional[str] = None
57
69
 
58
70
 
59
- # pylint: disable=too-many-instance-attributes
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
+
60
91
  class SSOBase:
61
- """Base class (mixin) for all SSO providers"""
92
+ """Base class for all SSO providers."""
62
93
 
63
94
  provider: str = NotImplemented
64
95
  client_id: str = NotImplemented
65
96
  client_secret: str = NotImplemented
66
97
  redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = NotImplemented
67
- scope: List[str] = NotImplemented
68
- additional_headers: Optional[Dict[str, Any]] = None
98
+ scope: ClassVar[List[str]] = []
99
+ additional_headers: ClassVar[Optional[Dict[str, Any]]] = None
69
100
  uses_pkce: bool = False
70
101
  requires_state: bool = False
71
102
 
@@ -80,15 +111,18 @@ class SSOBase:
80
111
  use_state: bool = False,
81
112
  scope: Optional[List[str]] = None,
82
113
  ):
83
- # pylint: disable=too-many-arguments
114
+ """Base class (mixin) for all SSO providers."""
84
115
  self.client_id: str = client_id
85
116
  self.client_secret: str = client_secret
86
117
  self.redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = redirect_uri
87
118
  self.allow_insecure_http: bool = allow_insecure_http
119
+ self._login_lock = asyncio.Lock()
120
+ self._in_stack = False
88
121
  self._oauth_client: Optional[WebApplicationClient] = None
89
122
  self._generated_state: Optional[str] = None
90
123
 
91
124
  if self.allow_insecure_http:
125
+ logger.debug("Initializing %s with allow_insecure_http=True", self.__class__.__name__)
92
126
  os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
93
127
 
94
128
  # TODO: Remove use_state argument and attribute
@@ -100,7 +134,7 @@ class SSOBase:
100
134
  ),
101
135
  DeprecationWarning,
102
136
  )
103
- self.scope = scope or self.scope
137
+ self._scope = scope or self.scope
104
138
  self._refresh_token: Optional[str] = None
105
139
  self._id_token: Optional[str] = None
106
140
  self._state: Optional[str] = None
@@ -110,8 +144,7 @@ class SSOBase:
110
144
 
111
145
  @property
112
146
  def state(self) -> Optional[str]:
113
- """
114
- Retrieves the state as it was returned from the server.
147
+ """Retrieves the state as it was returned from the server.
115
148
 
116
149
  Warning:
117
150
  This will emit a warning if the state is unset, implying either that
@@ -130,9 +163,9 @@ class SSOBase:
130
163
  return self._state
131
164
 
132
165
  @property
166
+ @requires_async_context
133
167
  def oauth_client(self) -> WebApplicationClient:
134
- """
135
- Retrieves the OAuth Client to aid in generating requests and parsing responses.
168
+ """Retrieves the OAuth Client to aid in generating requests and parsing responses.
136
169
 
137
170
  Raises:
138
171
  NotImplementedError: If the provider is not supported or `client_id` is not set.
@@ -147,9 +180,9 @@ class SSOBase:
147
180
  return self._oauth_client
148
181
 
149
182
  @property
183
+ @requires_async_context
150
184
  def access_token(self) -> Optional[str]:
151
- """
152
- Retrieves the access token from token endpoint.
185
+ """Retrieves the access token from token endpoint.
153
186
 
154
187
  Returns:
155
188
  Optional[str]: The access token if available.
@@ -157,9 +190,9 @@ class SSOBase:
157
190
  return self.oauth_client.access_token
158
191
 
159
192
  @property
193
+ @requires_async_context
160
194
  def refresh_token(self) -> Optional[str]:
161
- """
162
- Retrieves the refresh token if returned from provider.
195
+ """Retrieves the refresh token if returned from provider.
163
196
 
164
197
  Returns:
165
198
  Optional[str]: The refresh token if available.
@@ -167,9 +200,9 @@ class SSOBase:
167
200
  return self._refresh_token or self.oauth_client.refresh_token
168
201
 
169
202
  @property
203
+ @requires_async_context
170
204
  def id_token(self) -> Optional[str]:
171
- """
172
- Retrieves the id token if returned from provider.
205
+ """Retrieves the id token if returned from provider.
173
206
 
174
207
  Returns:
175
208
  Optional[str]: The id token if available.
@@ -177,8 +210,7 @@ class SSOBase:
177
210
  return self._id_token
178
211
 
179
212
  async def openid_from_response(self, response: dict, session: Optional[httpx.AsyncClient] = None) -> OpenID:
180
- """
181
- Converts a response from the provider's user info endpoint to an OpenID object.
213
+ """Converts a response from the provider's user info endpoint to an OpenID object.
182
214
 
183
215
  Args:
184
216
  response (dict): The response from the user info endpoint.
@@ -193,8 +225,7 @@ class SSOBase:
193
225
  raise NotImplementedError(f"Provider {self.provider} not supported")
194
226
 
195
227
  async def get_discovery_document(self) -> DiscoveryDocument:
196
- """
197
- Retrieves the discovery document containing useful URLs.
228
+ """Retrieves the discovery document containing useful URLs.
198
229
 
199
230
  Raises:
200
231
  NotImplementedError: If the provider is not supported.
@@ -206,19 +237,19 @@ class SSOBase:
206
237
 
207
238
  @property
208
239
  async def authorization_endpoint(self) -> Optional[str]:
209
- """Return `authorization_endpoint` from discovery document"""
240
+ """Return `authorization_endpoint` from discovery document."""
210
241
  discovery = await self.get_discovery_document()
211
242
  return discovery.get("authorization_endpoint")
212
243
 
213
244
  @property
214
245
  async def token_endpoint(self) -> Optional[str]:
215
- """Return `token_endpoint` from discovery document"""
246
+ """Return `token_endpoint` from discovery document."""
216
247
  discovery = await self.get_discovery_document()
217
248
  return discovery.get("token_endpoint")
218
249
 
219
250
  @property
220
251
  async def userinfo_endpoint(self) -> Optional[str]:
221
- """Return `userinfo_endpoint` from discovery document"""
252
+ """Return `userinfo_endpoint` from discovery document."""
222
253
  discovery = await self.get_discovery_document()
223
254
  return discovery.get("userinfo_endpoint")
224
255
 
@@ -229,8 +260,7 @@ class SSOBase:
229
260
  params: Optional[Dict[str, Any]] = None,
230
261
  state: Optional[str] = None,
231
262
  ) -> str:
232
- """
233
- Generates and returns the prepared login URL.
263
+ """Generates and returns the prepared login URL.
234
264
 
235
265
  Args:
236
266
  redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
@@ -263,7 +293,7 @@ class SSOBase:
263
293
  await self.authorization_endpoint,
264
294
  redirect_uri=redirect_uri,
265
295
  state=state,
266
- scope=self.scope,
296
+ scope=self._scope,
267
297
  code_challenge=self._pkce_code_challenge,
268
298
  code_challenge_method=self._pkce_challenge_method,
269
299
  **params,
@@ -277,8 +307,7 @@ class SSOBase:
277
307
  params: Optional[Dict[str, Any]] = None,
278
308
  state: Optional[str] = None,
279
309
  ) -> RedirectResponse:
280
- """
281
- Constructs and returns a redirect response to the login page of OAuth SSO provider.
310
+ """Constructs and returns a redirect response to the login page of OAuth SSO provider.
282
311
 
283
312
  Args:
284
313
  redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
@@ -318,6 +347,7 @@ class SSOBase:
318
347
  convert_response: Literal[False],
319
348
  ) -> Optional[Dict[str, Any]]: ...
320
349
 
350
+ @requires_async_context
321
351
  async def verify_and_process(
322
352
  self,
323
353
  request: Request,
@@ -327,8 +357,7 @@ class SSOBase:
327
357
  redirect_uri: Optional[str] = None,
328
358
  convert_response: Union[Literal[True], Literal[False]] = True,
329
359
  ) -> Union[Optional[OpenID], Optional[Dict[str, Any]]]:
330
- """
331
- Processes the login given a FastAPI (Starlette) Request object. This should be used for the /callback path.
360
+ """Processes the login given a FastAPI (Starlette) Request object. This should be used for the /callback path.
332
361
 
333
362
  Args:
334
363
  request (Request): FastAPI or Starlette request object.
@@ -347,6 +376,12 @@ class SSOBase:
347
376
  headers = headers or {}
348
377
  code = request.query_params.get("code")
349
378
  if code is None:
379
+ logger.debug(
380
+ "Callback request:\n\tURI: %s\n\tHeaders: %s\n\tQuery params: %s",
381
+ request.url,
382
+ request.headers,
383
+ request.query_params,
384
+ )
350
385
  raise SSOLoginError(400, "'code' parameter was not found in callback request")
351
386
  self._state = request.query_params.get("state")
352
387
  pkce_code_verifier: Optional[str] = None
@@ -367,6 +402,12 @@ class SSOBase:
367
402
  )
368
403
 
369
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
+ )
370
411
  self._oauth_client = None
371
412
  self._refresh_token = None
372
413
  self._id_token = None
@@ -377,6 +418,28 @@ class SSOBase:
377
418
  self._pkce_code_verifier, self._pkce_code_challenge = get_pkce_challenge_pair(self._pkce_challenge_length)
378
419
  return self
379
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
+
380
443
  def __exit__(
381
444
  self,
382
445
  _exc_type: Optional[Type[BaseException]],
@@ -415,6 +478,7 @@ class SSOBase:
415
478
  convert_response: Literal[False],
416
479
  ) -> Optional[Dict[str, Any]]: ...
417
480
 
481
+ @requires_async_context
418
482
  async def process_login(
419
483
  self,
420
484
  code: str,
@@ -426,8 +490,7 @@ class SSOBase:
426
490
  pkce_code_verifier: Optional[str] = None,
427
491
  convert_response: Union[Literal[True], Literal[False]] = True,
428
492
  ) -> Union[Optional[OpenID], Optional[Dict[str, Any]]]:
429
- """
430
- Processes login from the callback endpoint to verify the user and request user info endpoint.
493
+ """Processes login from the callback endpoint to verify the user and request user info endpoint.
431
494
  It's a lower-level method, typically, you should use `verify_and_process` instead.
432
495
 
433
496
  Args:
@@ -446,7 +509,6 @@ class SSOBase:
446
509
  Optional[OpenID]: User information in OpenID format if the login was successful (convert_response == True).
447
510
  Optional[Dict[str, Any]]: Original userinfo API endpoint response.
448
511
  """
449
- # pylint: disable=too-many-locals
450
512
  if self._oauth_client is not None: # pragma: no cover
451
513
  self._oauth_client = None
452
514
  self._refresh_token = None
@@ -1,7 +1,6 @@
1
- """Facebook SSO Login Helper
2
- """
1
+ """Facebook SSO Login Helper."""
3
2
 
4
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
5
4
 
6
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
7
6
 
@@ -10,14 +9,14 @@ if TYPE_CHECKING:
10
9
 
11
10
 
12
11
  class FacebookSSO(SSOBase):
13
- """Class providing login via Facebook OAuth"""
12
+ """Class providing login via Facebook OAuth."""
14
13
 
15
14
  provider = "facebook"
16
- base_url = "https://graph.facebook.com/v9.0"
17
- scope = ["email"]
15
+ base_url = "https://graph.facebook.com/v19.0"
16
+ scope: ClassVar = ["email"]
18
17
 
19
18
  async def get_discovery_document(self) -> DiscoveryDocument:
20
- """Get document containing handy urls"""
19
+ """Get document containing handy urls."""
21
20
  return {
22
21
  "authorization_endpoint": "https://www.facebook.com/v9.0/dialog/oauth",
23
22
  "token_endpoint": f"{self.base_url}/oauth/access_token",
@@ -25,9 +24,10 @@ class FacebookSSO(SSOBase):
25
24
  }
26
25
 
27
26
  async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
28
- """Return OpenID from user information provided by Facebook"""
27
+ """Return OpenID from user information provided by Facebook."""
28
+
29
29
  return OpenID(
30
- email=response.get("email", ""),
30
+ email=response.get("email"),
31
31
  first_name=response.get("first_name"),
32
32
  last_name=response.get("last_name"),
33
33
  display_name=response.get("name"),
fastapi_sso/sso/fitbit.py CHANGED
@@ -1,7 +1,6 @@
1
- """Fitbit OAuth Login Helper
2
- """
1
+ """Fitbit OAuth Login Helper."""
3
2
 
4
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
5
4
 
6
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase, SSOLoginError
7
6
 
@@ -10,13 +9,13 @@ if TYPE_CHECKING:
10
9
 
11
10
 
12
11
  class FitbitSSO(SSOBase):
13
- """Class providing login via Fitbit OAuth"""
12
+ """Class providing login via Fitbit OAuth."""
14
13
 
15
14
  provider = "fitbit"
16
- scope = ["profile"]
15
+ scope: ClassVar = ["profile"]
17
16
 
18
17
  async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
19
- """Return OpenID from user information provided by Google"""
18
+ """Return OpenID from user information provided by Google."""
20
19
  info = response.get("user")
21
20
  if not info:
22
21
  raise SSOLoginError(401, "Failed to process login via Fitbit")
@@ -29,7 +28,7 @@ class FitbitSSO(SSOBase):
29
28
  )
30
29
 
31
30
  async def get_discovery_document(self) -> DiscoveryDocument:
32
- """Get document containing handy urls"""
31
+ """Get document containing handy urls."""
33
32
  return {
34
33
  "authorization_endpoint": "https://www.fitbit.com/oauth2/authorize?response_type=code",
35
34
  "token_endpoint": "https://api.fitbit.com/oauth2/token",
@@ -1,5 +1,5 @@
1
1
  """A generic OAuth client that can be used to quickly create support for any OAuth provider
2
- with close to no code
2
+ with close to no code.
3
3
  """
4
4
 
5
5
  import logging
@@ -24,7 +24,6 @@ def create_provider(
24
24
  Returns a class.
25
25
 
26
26
  Args:
27
-
28
27
  name: Name of the provider
29
28
  default_scope: default list of scopes (can be overriden in constructor)
30
29
  discovery_document: a dictionary containing discovery document or a callable returning it
@@ -53,13 +52,13 @@ def create_provider(
53
52
  """
54
53
 
55
54
  class GenericSSOProvider(SSOBase):
56
- """SSO Provider Template"""
55
+ """SSO Provider Template."""
57
56
 
58
57
  provider = name
59
58
  scope = default_scope or ["openid"]
60
59
 
61
60
  async def get_discovery_document(self) -> DiscoveryDocument:
62
- """Get document containing handy urls"""
61
+ """Get document containing handy urls."""
63
62
  if callable(discovery_document):
64
63
  return discovery_document(self)
65
64
  return discovery_document
fastapi_sso/sso/github.py CHANGED
@@ -1,6 +1,6 @@
1
- """Github SSO Oauth Helper class"""
1
+ """Github SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
4
4
 
5
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
6
6
 
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class GithubSSO(SSOBase):
12
- """Class providing login via Github SSO"""
12
+ """Class providing login via Github SSO."""
13
13
 
14
14
  provider = "github"
15
- scope = ["user:email"]
16
- additional_headers = {"accept": "application/json"}
15
+ scope: ClassVar = ["user:email"]
16
+ additional_headers: ClassVar = {"accept": "application/json"}
17
17
  emails_endpoint = "https://api.github.com/user/emails"
18
18
 
19
19
  async def get_discovery_document(self) -> DiscoveryDocument:
@@ -25,7 +25,8 @@ class GithubSSO(SSOBase):
25
25
 
26
26
  async def _get_primary_email(self, session: Optional["httpx.AsyncClient"] = None) -> Optional[str]:
27
27
  """Attempt to get primary email from Github for a current user.
28
- The session received must be authenticated."""
28
+ The session received must be authenticated.
29
+ """
29
30
  if not session:
30
31
  return None
31
32
  response = await session.get(self.emails_endpoint)
fastapi_sso/sso/gitlab.py CHANGED
@@ -1,6 +1,6 @@
1
- """Gitlab SSO Oauth Helper class"""
1
+ """Gitlab SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, List, Optional, Tuple, Union
3
+ from typing import TYPE_CHECKING, ClassVar, List, Optional, Tuple, Union
4
4
  from urllib.parse import urljoin
5
5
 
6
6
  import pydantic
@@ -12,11 +12,11 @@ if TYPE_CHECKING:
12
12
 
13
13
 
14
14
  class GitlabSSO(SSOBase):
15
- """Class providing login via Gitlab SSO"""
15
+ """Class providing login via Gitlab SSO."""
16
16
 
17
17
  provider = "gitlab"
18
- scope = ["read_user", "openid", "profile"]
19
- additional_headers = {"accept": "application/json"}
18
+ scope: ClassVar = ["read_user", "openid", "profile"]
19
+ additional_headers: ClassVar = {"accept": "application/json"}
20
20
  base_endpoint_url = "https://gitlab.com"
21
21
 
22
22
  def __init__(
@@ -41,7 +41,6 @@ class GitlabSSO(SSOBase):
41
41
 
42
42
  async def get_discovery_document(self) -> DiscoveryDocument:
43
43
  """Override the discovery document method to return Yandex OAuth endpoints."""
44
-
45
44
  return {
46
45
  "authorization_endpoint": urljoin(self.base_endpoint_url, "/oauth/authorize"),
47
46
  "token_endpoint": urljoin(self.base_endpoint_url, "/oauth/token"),
fastapi_sso/sso/google.py CHANGED
@@ -1,7 +1,6 @@
1
- """Google SSO Login Helper
2
- """
1
+ """Google SSO Login Helper."""
3
2
 
4
- from typing import Optional
3
+ from typing import ClassVar, Optional
5
4
 
6
5
  import httpx
7
6
 
@@ -9,17 +8,17 @@ from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase, SSOLoginErr
9
8
 
10
9
 
11
10
  class GoogleSSO(SSOBase):
12
- """Class providing login via Google OAuth"""
11
+ """Class providing login via Google OAuth."""
13
12
 
14
13
  discovery_url = "https://accounts.google.com/.well-known/openid-configuration"
15
14
  provider = "google"
16
- scope = ["openid", "email", "profile"]
15
+ scope: ClassVar = ["openid", "email", "profile"]
17
16
 
18
17
  async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
19
- """Return OpenID from user information provided by Google"""
18
+ """Return OpenID from user information provided by Google."""
20
19
  if response.get("email_verified"):
21
20
  return OpenID(
22
- email=response.get("email", ""),
21
+ email=response.get("email"),
23
22
  provider=self.provider,
24
23
  id=response.get("sub"),
25
24
  first_name=response.get("given_name"),
@@ -30,7 +29,7 @@ class GoogleSSO(SSOBase):
30
29
  raise SSOLoginError(401, f"User {response.get('email')} is not verified with Google")
31
30
 
32
31
  async def get_discovery_document(self) -> DiscoveryDocument:
33
- """Get document containing handy urls"""
32
+ """Get document containing handy urls."""
34
33
  async with httpx.AsyncClient() as session:
35
34
  response = await session.get(self.discovery_url)
36
35
  content = response.json()
fastapi_sso/sso/kakao.py CHANGED
@@ -1,6 +1,6 @@
1
- """Kakao SSO Oauth Helper class"""
1
+ """Kakao SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
4
4
 
5
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
6
6
 
@@ -9,10 +9,10 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class KakaoSSO(SSOBase):
12
- """Class providing login using Kakao OAuth"""
12
+ """Class providing login using Kakao OAuth."""
13
13
 
14
14
  provider = "kakao"
15
- scope = ["openid"]
15
+ scop: ClassVar = ["openid"]
16
16
  version = "v2"
17
17
 
18
18
  async def get_discovery_document(self) -> DiscoveryDocument:
fastapi_sso/sso/line.py CHANGED
@@ -1,7 +1,6 @@
1
- """Line SSO Login Helper
2
- """
1
+ """Line SSO Login Helper."""
3
2
 
4
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
5
4
 
6
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
7
6
 
@@ -10,14 +9,14 @@ if TYPE_CHECKING:
10
9
 
11
10
 
12
11
  class LineSSO(SSOBase):
13
- """Class providing login via Line OAuth"""
12
+ """Class providing login via Line OAuth."""
14
13
 
15
14
  provider = "line"
16
15
  base_url = "https://api.line.me/oauth2/v2.1"
17
- scope = ["email", "profile", "openid"]
16
+ scope: ClassVar = ["email", "profile", "openid"]
18
17
 
19
18
  async def get_discovery_document(self) -> DiscoveryDocument:
20
- """Get document containing handy urls"""
19
+ """Get document containing handy urls."""
21
20
  return {
22
21
  "authorization_endpoint": "https://access.line.me/oauth2/v2.1/authorize",
23
22
  "token_endpoint": f"{self.base_url}/token",
@@ -25,7 +24,7 @@ class LineSSO(SSOBase):
25
24
  }
26
25
 
27
26
  async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
28
- """Return OpenID from user information provided by Line"""
27
+ """Return OpenID from user information provided by Line."""
29
28
  return OpenID(
30
29
  email=response.get("email"),
31
30
  first_name=None,
@@ -1,6 +1,6 @@
1
- """LinkedIn SSO Oauth Helper class"""
1
+ """LinkedIn SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, Dict, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Dict, Optional
4
4
 
5
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
6
6
 
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class LinkedInSSO(SSOBase):
12
- """Class providing login via LinkedIn SSO"""
12
+ """Class providing login via LinkedIn SSO."""
13
13
 
14
14
  provider = "linkedin"
15
- scope = ["openid", "profile", "email"]
16
- additional_headers = {"accept": "application/json"}
15
+ scope: ClassVar = ["openid", "profile", "email"]
16
+ additional_headers: ClassVar = {"accept": "application/json"}
17
17
 
18
18
  @property
19
19
  def _extra_query_params(self) -> Dict:
@@ -1,6 +1,6 @@
1
- """Microsoft SSO Oauth Helper class"""
1
+ """Microsoft SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, List, Optional, Union
3
+ from typing import TYPE_CHECKING, ClassVar, List, Optional, Union
4
4
 
5
5
  import pydantic
6
6
 
@@ -11,10 +11,10 @@ if TYPE_CHECKING:
11
11
 
12
12
 
13
13
  class MicrosoftSSO(SSOBase):
14
- """Class providing login using Microsoft OAuth"""
14
+ """Class providing login using Microsoft OAuth."""
15
15
 
16
16
  provider = "microsoft"
17
- scope = ["openid", "User.Read", "email"]
17
+ scope: ClassVar = ["openid", "User.Read", "email"]
18
18
  version = "v1.0"
19
19
  tenant: str = "common"
20
20
 
fastapi_sso/sso/naver.py CHANGED
@@ -1,6 +1,6 @@
1
- """Naver SSO Oauth Helper class"""
1
+ """Naver SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, List, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, List, Optional
4
4
 
5
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
6
6
 
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class NaverSSO(SSOBase):
12
- """Class providing login using Naver OAuth"""
12
+ """Class providing login using Naver OAuth."""
13
13
 
14
14
  provider = "naver"
15
- scope: List[str] = []
16
- additional_headers = {"accept": "application/json"}
15
+ scope: ClassVar[List[str]] = []
16
+ additional_headers: ClassVar = {"accept": "application/json"}
17
17
 
18
18
  async def get_discovery_document(self) -> DiscoveryDocument:
19
19
  return {
fastapi_sso/sso/notion.py CHANGED
@@ -1,6 +1,6 @@
1
- """Notion SSO Oauth Helper class"""
1
+ """Notion SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
4
4
 
5
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase, SSOLoginError
6
6
 
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class NotionSSO(SSOBase):
12
- """Class providing login using Notion OAuth"""
12
+ """Class providing login using Notion OAuth."""
13
13
 
14
14
  provider = "notion"
15
- scope = ["openid"]
16
- additional_headers = {"Notion-Version": "2022-06-28"}
15
+ scope: ClassVar = ["openid"]
16
+ additional_headers: ClassVar = {"Notion-Version": "2022-06-28"}
17
17
 
18
18
  async def get_discovery_document(self) -> DiscoveryDocument:
19
19
  return {
@@ -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,7 +1,6 @@
1
- """Spotify SSO Login Helper
2
- """
1
+ """Spotify SSO Login Helper."""
3
2
 
4
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
5
4
 
6
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
7
6
 
@@ -10,13 +9,13 @@ if TYPE_CHECKING:
10
9
 
11
10
 
12
11
  class SpotifySSO(SSOBase):
13
- """Class providing login via Spotify OAuth"""
12
+ """Class providing login via Spotify OAuth."""
14
13
 
15
14
  provider = "spotify"
16
- scope = ["user-read-private", "user-read-email"]
15
+ scope: ClassVar = ["user-read-private", "user-read-email"]
17
16
 
18
17
  async def get_discovery_document(self) -> DiscoveryDocument:
19
- """Get document containing handy urls"""
18
+ """Get document containing handy urls."""
20
19
  return {
21
20
  "authorization_endpoint": "https://accounts.spotify.com/authorize",
22
21
  "token_endpoint": "https://accounts.spotify.com/api/token",
@@ -24,13 +23,10 @@ class SpotifySSO(SSOBase):
24
23
  }
25
24
 
26
25
  async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
27
- """Return OpenID from user information provided by Spotify"""
28
- if response.get("images", []):
29
- picture = response["images"][0]["url"]
30
- else:
31
- picture = None
26
+ """Return OpenID from user information provided by Spotify."""
27
+ picture = response["images"][0]["url"] if response.get("images", []) else None
32
28
  return OpenID(
33
- email=response.get("email", ""),
29
+ email=response.get("email"),
34
30
  display_name=response.get("display_name"),
35
31
  provider=self.provider,
36
32
  id=response.get("id"),
@@ -1,6 +1,6 @@
1
- """Twitter (X) SSO Oauth Helper class"""
1
+ """Twitter (X) SSO Oauth Helper class."""
2
2
 
3
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
4
4
 
5
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
6
6
 
@@ -9,10 +9,10 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class TwitterSSO(SSOBase):
12
- """Class providing login via Twitter SSO"""
12
+ """Class providing login via Twitter SSO."""
13
13
 
14
14
  provider = "twitter"
15
- scope = ["users.read", "tweet.read"]
15
+ scope: ClassVar = ["users.read", "tweet.read"]
16
16
  uses_pkce = True
17
17
  requires_state = True
18
18
 
fastapi_sso/sso/yandex.py CHANGED
@@ -1,7 +1,6 @@
1
- """Yandex SSO Login Helper
2
- """
1
+ """Yandex SSO Login Helper."""
3
2
 
4
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING, ClassVar, Optional
5
4
 
6
5
  from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
7
6
 
@@ -13,12 +12,11 @@ class YandexSSO(SSOBase):
13
12
  """Class providing login using Yandex OAuth."""
14
13
 
15
14
  provider = "yandex"
16
- scope = ["login:email", "login:info", "login:avatar"]
15
+ scope: ClassVar = ["login:email", "login:info", "login:avatar"]
17
16
  avatar_url = "https://avatars.yandex.net/get-yapic"
18
17
 
19
18
  async def get_discovery_document(self) -> DiscoveryDocument:
20
19
  """Override the discovery document method to return Yandex OAuth endpoints."""
21
-
22
20
  return {
23
21
  "authorization_endpoint": "https://oauth.yandex.ru/authorize",
24
22
  "token_endpoint": "https://oauth.yandex.ru/token",
@@ -27,7 +25,6 @@ class YandexSSO(SSOBase):
27
25
 
28
26
  async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
29
27
  """Converts Yandex user info response to OpenID object."""
30
-
31
28
  picture = None
32
29
 
33
30
  if (avatar_id := response.get("default_avatar_id")) is not None:
fastapi_sso/state.py CHANGED
@@ -1,10 +1,10 @@
1
- """Helper functions to generate state param"""
1
+ """Helper functions to generate state param."""
2
2
 
3
3
  import base64
4
4
  import os
5
5
 
6
6
 
7
7
  def generate_random_state(length: int = 64) -> str:
8
- """Generate a url-safe string to use as a state"""
8
+ """Generate a url-safe string to use as a state."""
9
9
  bytes_length = int(length * 3 / 4)
10
10
  return base64.urlsafe_b64encode(os.urandom(bytes_length)).decode("utf-8")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-sso
3
- Version: 0.14.2
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
@@ -28,7 +30,7 @@ Description-Content-Type: text/markdown
28
30
  ![Supported Python Versions](https://img.shields.io/pypi/pyversions/fastapi-sso)
29
31
  [![Test coverage](https://codecov.io/gh/tomasvotava/fastapi-sso/graph/badge.svg?token=SIFCTVSSOS)](https://codecov.io/gh/tomasvotava/fastapi-sso)
30
32
  ![Tests Workflow Status](https://img.shields.io/github/actions/workflow/status/tomasvotava/fastapi-sso/test.yml?label=tests)
31
- ![Pylint Workflow Status](https://img.shields.io/github/actions/workflow/status/tomasvotava/fastapi-sso/lint.yml?label=pylint)
33
+ ![Lint Workflow Status](https://img.shields.io/github/actions/workflow/status/tomasvotava/fastapi-sso/lint.yml?label=ruff)
32
34
  ![Mypy Workflow Status](https://img.shields.io/github/actions/workflow/status/tomasvotava/fastapi-sso/lint.yml?label=mypy)
33
35
  ![Black Workflow Status](https://img.shields.io/github/actions/workflow/status/tomasvotava/fastapi-sso/lint.yml?label=black)
34
36
  ![CodeQL Workflow Status](https://img.shields.io/github/actions/workflow/status/tomasvotava/fastapi-sso/codeql-analysis.yml?label=CodeQL)
@@ -46,14 +48,68 @@ backend very easily.
46
48
 
47
49
  **Source Code**: [https://github.com/tomasvotava/fastapi-sso](https://github.com/tomasvotava/fastapi-sso/)
48
50
 
49
- ## Security warning
51
+ ## Demo site
50
52
 
51
- Please note that versions preceding `0.7.0` had a security vulnerability.
52
- The SSO instance could share state between requests, which could lead to security issues.
53
- **Please update to `0.7.0` or newer**.
53
+ An awesome demo site was created and is maintained by even awesomer
54
+ [Chris Karvouniaris (@chrisK824)](https://github.com/chrisK824). Chris has also posted multiple
55
+ Medium articles about FastAPI and FastAPI SSO.
54
56
 
55
- Also, the preferred way of using the SSO instances is to use `with` statement, which will ensure the state is cleared.
56
- See example below.
57
+ Be sure to see his tutorials, follow him and show him some appreciation!
58
+
59
+ Please see his [announcement](https://github.com/tomasvotava/fastapi-sso/discussions/150) with all the links.
60
+
61
+ Quick links for the eager ones:
62
+
63
+ - [Demo site](https://fastapi-sso-example.vercel.app/)
64
+ - [Medium articles](https://medium.com/@christos.karvouniaris247)
65
+
66
+ ## Security Notice
67
+
68
+ ### Version `0.16.0` Update: Race Condition Bug Fix & Context Manager Change
69
+
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).
57
113
 
58
114
  ## Support this project
59
115
 
@@ -86,6 +142,7 @@ I tend to process Pull Requests faster when properly caffeinated 😉.
86
142
  - Line (by Jimmy Yeh) - [jimmyyyeh](https://github.com/jimmyyyeh)
87
143
  - LinkedIn (by Alessandro Pischedda) - [Cereal84](https://github.com/Cereal84)
88
144
  - Yandex (by Akim Faskhutdinov) – [akimrx](https://github.com/akimrx)
145
+ - Seznam (by Tomas Koutek) - [TomasKoutek](https://github.com/TomasKoutek)
89
146
 
90
147
  See [Contributing](#contributing) for a guide on how to contribute your own login provider.
91
148
 
@@ -0,0 +1,26 @@
1
+ fastapi_sso/__init__.py,sha256=dGVA7UO2jDN2RAk5UlI73k1p8zEXMjUEaP7Kwq3_Gzc,1006
2
+ fastapi_sso/pkce.py,sha256=QDqCH5f5EDmIG2MHfcAYbZJaURbl6w5IiuS6SHq89qA,792
3
+ fastapi_sso/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ fastapi_sso/sso/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ fastapi_sso/sso/base.py,sha256=PuxN9rEWLZ5c1WN751lFrGArVcOGipcpX43gPDbsbn8,21637
6
+ fastapi_sso/sso/facebook.py,sha256=JaGCT2v56iRRQlnoZ5OSsB19677gyMU7U5dZTjVE_mc,1368
7
+ fastapi_sso/sso/fitbit.py,sha256=CIqyyMDzZ0sYbZOtDNEotsoqq0Rvr5x4oP8_yWxfU7M,1301
8
+ fastapi_sso/sso/generic.py,sha256=VBzKVc2ckpuC4XqzlXQ56eg-xK8roV-uiUc2xsVseOI,2647
9
+ fastapi_sso/sso/github.py,sha256=Ol0W6vjmErY8M9XlMCH9nbKsnZfXeOClDNx3oUX52_c,1754
10
+ fastapi_sso/sso/gitlab.py,sha256=FeFTm0Ypt5_wVjE76lXgNy28a_ixvdr6TOtv5CYfxqs,2714
11
+ fastapi_sso/sso/google.py,sha256=LIBV-nEoH2CTJCpINVu7jEfPqRlPQCAPSWK4MKBgsM0,1399
12
+ fastapi_sso/sso/kakao.py,sha256=qcsNUXbVTdyb_0V2CHTqplG82ftbnJ1uMB_GenEaiG0,905
13
+ fastapi_sso/sso/line.py,sha256=F6XmXO5RBllGV5U0mhujEiJ4GsZKwzoabLfi-l54lwM,1233
14
+ fastapi_sso/sso/linkedin.py,sha256=w6zSufyUNZguxnyfn62RPXrOGp48J9jKJL4Nx4NWit0,1304
15
+ fastapi_sso/sso/microsoft.py,sha256=1RFIUeGZCVIBCr7kCvoOrt_FKcVRCrFO6boxnFpAioc,1960
16
+ fastapi_sso/sso/naver.py,sha256=IFFGPnUR1eOsS4JpubNLqOHDaQaqekPwDIZmm0NKRao,1149
17
+ fastapi_sso/sso/notion.py,sha256=YWGBUhN-CFTEpjIgkkEUS5JmGawyhJE57XB4Q9nFOD4,1334
18
+ fastapi_sso/sso/seznam.py,sha256=HGI01QPBYslv_b6Lq3oTL2UWPnNLPu45y3rgTdDqxpU,1383
19
+ fastapi_sso/sso/spotify.py,sha256=FvX2N91Bi3wgKRwdU1sWo-zA0s3wYJCiCYA05ebXweE,1244
20
+ fastapi_sso/sso/twitter.py,sha256=1kMjFdh-OT1b5bJvY3tWfl-BRBv2hVZ6L_liLAvNML8,1249
21
+ fastapi_sso/sso/yandex.py,sha256=8jKkh-na62lwsaBW7Pvj6VC6WlR0RMg8KrSNlr2Hj8o,1507
22
+ fastapi_sso/state.py,sha256=9RKMrFGjeN4Ab-3var81QV-gpcBlnNy152WYbxTUGVY,300
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,25 +0,0 @@
1
- fastapi_sso/__init__.py,sha256=bQH_ECRRvpAQfPfpe5bIiGdfRoPULFgvbSjGS9OPkYQ,690
2
- fastapi_sso/pkce.py,sha256=_J9wMCaSwPsUk32qoHzorhB9HG79YVifcQcRA4l_N7I,790
3
- fastapi_sso/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- fastapi_sso/sso/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- fastapi_sso/sso/base.py,sha256=DDqOSjuXzP6-GrAuQeRAt5M4aKXAsxuDXztd8orBeO0,19272
6
- fastapi_sso/sso/facebook.py,sha256=fyO9R7wmhuDp8gZWOp5hogMfTLbff_7ms1XcrLL9udU,1347
7
- fastapi_sso/sso/fitbit.py,sha256=ofWcrGVeZk6n4b59RI9dLxPz_GzpizJiLNGLvrIphmY,1278
8
- fastapi_sso/sso/generic.py,sha256=nfH_ju0zRM4HAX-YYHMulSGRb_UEr342YAcK8JtJ3CE,2645
9
- fastapi_sso/sso/github.py,sha256=GA0jalPGjgBvhJH2SSyIxiizcj6PR8ccOXQYemmaKpQ,1713
10
- fastapi_sso/sso/gitlab.py,sha256=xYpzDfah8fRD0vVClG_w2_oHWoQ3JdkVh4zOWDVhoZs,2683
11
- fastapi_sso/sso/google.py,sha256=iyVqh5YG5Xw-Ss4y8xEv1APDvubdI9Vt3ijvPMmStVk,1380
12
- fastapi_sso/sso/kakao.py,sha256=-St6EEI0r3oFFphFd7UgqppA0z7XtZ22FmBOyhPfJ9M,884
13
- fastapi_sso/sso/line.py,sha256=1u27gqq8ZNiNn3XRumyTzNTj67vyTlo_HfVBRj4_crI,1210
14
- fastapi_sso/sso/linkedin.py,sha256=huoWB0qz1lToq0arPTwr13ZlapQUzy8EBklqsqF84-E,1272
15
- fastapi_sso/sso/microsoft.py,sha256=WX5WQRNt0kYdQO7XeJXS6dE_k-pO1y3Ty4_IuZPc0gI,1938
16
- fastapi_sso/sso/naver.py,sha256=W7_gEqql8cHg8drs96Hd8uijklvh77wXa0TSgvAJlr4,1117
17
- fastapi_sso/sso/notion.py,sha256=3OU70JpE4mle9DXyZrf4sRpwq1raWcOHjFJrRmsI-EM,1302
18
- fastapi_sso/sso/spotify.py,sha256=EyCuUK1fiP-I7NSg1ssDoPJilHnIULyXB4AyJNxPiYg,1269
19
- fastapi_sso/sso/twitter.py,sha256=NsRbKZlau_8gZNKgCywlggKv1jZLRP1oDXTp-TId2PU,1227
20
- fastapi_sso/sso/yandex.py,sha256=cTfxnK1xKBqGZ1GHd4Y1GtqTJGt1dYV6RAE6kACvX14,1489
21
- fastapi_sso/state.py,sha256=bwqsl73I5_VSf-TfCTvDfVb7L_3PwUqtP7jDjco-8Nw,298
22
- fastapi_sso-0.14.2.dist-info/LICENSE.md,sha256=5NVQtYs6liDtYdWM4VObWmTTKaK0k9C9txx5pLPJSyQ,1093
23
- fastapi_sso-0.14.2.dist-info/METADATA,sha256=QzTskwHY4cPLk_fbYRQijisgfC8xq5Vh6ghsW_2SXpk,4635
24
- fastapi_sso-0.14.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
25
- fastapi_sso-0.14.2.dist-info/RECORD,,