fastapi-sso 0.12.1__py3-none-any.whl → 0.13.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 +1 -0
- fastapi_sso/pkce.py +25 -0
- fastapi_sso/sso/base.py +59 -3
- fastapi_sso/sso/facebook.py +1 -1
- fastapi_sso/sso/fitbit.py +1 -1
- fastapi_sso/sso/generic.py +1 -1
- fastapi_sso/sso/github.py +1 -1
- fastapi_sso/sso/gitlab.py +1 -1
- fastapi_sso/sso/kakao.py +1 -1
- fastapi_sso/sso/line.py +1 -1
- fastapi_sso/sso/linkedin.py +5 -5
- fastapi_sso/sso/microsoft.py +1 -1
- fastapi_sso/sso/naver.py +1 -1
- fastapi_sso/sso/spotify.py +1 -1
- fastapi_sso/sso/twitter.py +35 -0
- fastapi_sso/state.py +10 -0
- {fastapi_sso-0.12.1.dist-info → fastapi_sso-0.13.0.dist-info}/METADATA +1 -1
- fastapi_sso-0.13.0.dist-info/RECORD +24 -0
- {fastapi_sso-0.12.1.dist-info → fastapi_sso-0.13.0.dist-info}/WHEEL +1 -1
- fastapi_sso-0.12.1.dist-info/RECORD +0 -21
- {fastapi_sso-0.12.1.dist-info → fastapi_sso-0.13.0.dist-info}/LICENSE.md +0 -0
fastapi_sso/__init__.py
CHANGED
fastapi_sso/pkce.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""PKCE-related helper functions"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_code_verifier(length: int = 96) -> str:
|
|
10
|
+
"""Get code verifier for PKCE challenge"""
|
|
11
|
+
length = max(43, min(length, 128))
|
|
12
|
+
bytes_length = int(length * 3 / 4)
|
|
13
|
+
return base64.urlsafe_b64encode(os.urandom(bytes_length)).decode("utf-8").replace("=", "")[:length]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_pkce_challenge_pair(verifier_length: int = 96) -> Tuple[str, str]:
|
|
17
|
+
"""Get tuple of (verifier, challenge) for PKCE challenge."""
|
|
18
|
+
code_verifier = get_code_verifier(verifier_length)
|
|
19
|
+
code_challenge = (
|
|
20
|
+
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("utf-8")).digest())
|
|
21
|
+
.decode("utf-8")
|
|
22
|
+
.replace("=", "")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return (code_verifier, code_challenge)
|
fastapi_sso/sso/base.py
CHANGED
|
@@ -17,6 +17,9 @@ from starlette.exceptions import HTTPException
|
|
|
17
17
|
from starlette.requests import Request
|
|
18
18
|
from starlette.responses import RedirectResponse
|
|
19
19
|
|
|
20
|
+
from fastapi_sso.pkce import get_pkce_challenge_pair
|
|
21
|
+
from fastapi_sso.state import generate_random_state
|
|
22
|
+
|
|
20
23
|
if sys.version_info >= (3, 8):
|
|
21
24
|
from typing import TypedDict
|
|
22
25
|
else:
|
|
@@ -63,6 +66,10 @@ class SSOBase:
|
|
|
63
66
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = NotImplemented
|
|
64
67
|
scope: List[str] = NotImplemented
|
|
65
68
|
additional_headers: Optional[Dict[str, Any]] = None
|
|
69
|
+
uses_pkce: bool = False
|
|
70
|
+
requires_state: bool = False
|
|
71
|
+
|
|
72
|
+
_pkce_challenge_length: int = 96
|
|
66
73
|
|
|
67
74
|
def __init__(
|
|
68
75
|
self,
|
|
@@ -79,6 +86,7 @@ class SSOBase:
|
|
|
79
86
|
self.redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = redirect_uri
|
|
80
87
|
self.allow_insecure_http: bool = allow_insecure_http
|
|
81
88
|
self._oauth_client: Optional[WebApplicationClient] = None
|
|
89
|
+
self._generated_state: Optional[str] = None
|
|
82
90
|
|
|
83
91
|
if self.allow_insecure_http:
|
|
84
92
|
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
|
@@ -96,6 +104,9 @@ class SSOBase:
|
|
|
96
104
|
self._refresh_token: Optional[str] = None
|
|
97
105
|
self._id_token: Optional[str] = None
|
|
98
106
|
self._state: Optional[str] = None
|
|
107
|
+
self._pkce_code_challenge: Optional[str] = None
|
|
108
|
+
self._pkce_code_verifier: Optional[str] = None
|
|
109
|
+
self._pkce_challenge_method = "S256"
|
|
99
110
|
|
|
100
111
|
@property
|
|
101
112
|
def state(self) -> Optional[str]:
|
|
@@ -236,8 +247,26 @@ class SSOBase:
|
|
|
236
247
|
redirect_uri = redirect_uri or self.redirect_uri
|
|
237
248
|
if redirect_uri is None:
|
|
238
249
|
raise ValueError("redirect_uri must be provided, either at construction or request time")
|
|
250
|
+
if self.uses_pkce and not all((self._pkce_code_verifier, self._pkce_code_challenge)):
|
|
251
|
+
warnings.warn(
|
|
252
|
+
f"{self.__class__.__name__!r} uses PKCE and no code was generated yet. "
|
|
253
|
+
"Use SSO class as a context manager to get rid of this warning and possible errors."
|
|
254
|
+
)
|
|
255
|
+
if self.requires_state and not state:
|
|
256
|
+
if self._generated_state is None:
|
|
257
|
+
warnings.warn(
|
|
258
|
+
f"{self.__class__.__name__!r} requires state in the request but none was provided nor "
|
|
259
|
+
"generated automatically. Use SSO as a context manager. The login process will most probably fail."
|
|
260
|
+
)
|
|
261
|
+
state = self._generated_state
|
|
239
262
|
request_uri = self.oauth_client.prepare_request_uri(
|
|
240
|
-
await self.authorization_endpoint,
|
|
263
|
+
await self.authorization_endpoint,
|
|
264
|
+
redirect_uri=redirect_uri,
|
|
265
|
+
state=state,
|
|
266
|
+
scope=self.scope,
|
|
267
|
+
code_challenge=self._pkce_code_challenge,
|
|
268
|
+
code_challenge_method=self._pkce_challenge_method,
|
|
269
|
+
**params,
|
|
241
270
|
)
|
|
242
271
|
return request_uri
|
|
243
272
|
|
|
@@ -259,8 +288,12 @@ class SSOBase:
|
|
|
259
288
|
Returns:
|
|
260
289
|
RedirectResponse: A Starlette response directing to the login page of the OAuth SSO provider.
|
|
261
290
|
"""
|
|
291
|
+
if self.requires_state and not state:
|
|
292
|
+
state = self._generated_state
|
|
262
293
|
login_uri = await self.get_login_url(redirect_uri=redirect_uri, params=params, state=state)
|
|
263
294
|
response = RedirectResponse(login_uri, 303)
|
|
295
|
+
if self.uses_pkce:
|
|
296
|
+
response.set_cookie("pkce_code_verifier", str(self._pkce_code_verifier))
|
|
264
297
|
return response
|
|
265
298
|
|
|
266
299
|
async def verify_and_process(
|
|
@@ -291,14 +324,31 @@ class SSOBase:
|
|
|
291
324
|
if code is None:
|
|
292
325
|
raise SSOLoginError(400, "'code' parameter was not found in callback request")
|
|
293
326
|
self._state = request.query_params.get("state")
|
|
327
|
+
pkce_code_verifier: Optional[str] = None
|
|
328
|
+
if self.uses_pkce:
|
|
329
|
+
pkce_code_verifier = request.cookies.get("pkce_code_verifier")
|
|
330
|
+
if pkce_code_verifier is None:
|
|
331
|
+
warnings.warn(
|
|
332
|
+
"PKCE code verifier was not found in the request Cookie. This will probably lead to a login error."
|
|
333
|
+
)
|
|
294
334
|
return await self.process_login(
|
|
295
|
-
code,
|
|
335
|
+
code,
|
|
336
|
+
request,
|
|
337
|
+
params=params,
|
|
338
|
+
additional_headers=headers,
|
|
339
|
+
redirect_uri=redirect_uri,
|
|
340
|
+
pkce_code_verifier=pkce_code_verifier,
|
|
296
341
|
)
|
|
297
342
|
|
|
298
343
|
def __enter__(self) -> "SSOBase":
|
|
299
344
|
self._oauth_client = None
|
|
300
345
|
self._refresh_token = None
|
|
301
346
|
self._id_token = None
|
|
347
|
+
self._state = None
|
|
348
|
+
if self.requires_state:
|
|
349
|
+
self._generated_state = generate_random_state()
|
|
350
|
+
if self.uses_pkce:
|
|
351
|
+
self._pkce_code_verifier, self._pkce_code_challenge = get_pkce_challenge_pair(self._pkce_challenge_length)
|
|
302
352
|
return self
|
|
303
353
|
|
|
304
354
|
def __exit__(
|
|
@@ -321,6 +371,7 @@ class SSOBase:
|
|
|
321
371
|
params: Optional[Dict[str, Any]] = None,
|
|
322
372
|
additional_headers: Optional[Dict[str, Any]] = None,
|
|
323
373
|
redirect_uri: Optional[str] = None,
|
|
374
|
+
pkce_code_verifier: Optional[str] = None,
|
|
324
375
|
) -> Optional[OpenID]:
|
|
325
376
|
"""
|
|
326
377
|
Processes login from the callback endpoint to verify the user and request user info endpoint.
|
|
@@ -332,6 +383,7 @@ class SSOBase:
|
|
|
332
383
|
params (Optional[Dict[str, Any]]): Additional query parameters to pass to the provider.
|
|
333
384
|
additional_headers (Optional[Dict[str, Any]]): Additional headers to be added to all requests.
|
|
334
385
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
386
|
+
pkce_code_verifier (Optional[str]): A PKCE code verifier sent to the server to verify the login request.
|
|
335
387
|
|
|
336
388
|
Raises:
|
|
337
389
|
ReusedOauthClientWarning: If the SSO object is reused, which is not safe and caused security issues.
|
|
@@ -379,8 +431,12 @@ class SSOBase:
|
|
|
379
431
|
headers.update(additional_headers)
|
|
380
432
|
|
|
381
433
|
auth = httpx.BasicAuth(self.client_id, self.client_secret)
|
|
434
|
+
|
|
435
|
+
if pkce_code_verifier:
|
|
436
|
+
params.update({"code_verifier": pkce_code_verifier})
|
|
437
|
+
|
|
382
438
|
async with httpx.AsyncClient() as session:
|
|
383
|
-
response = await session.post(token_url, headers=headers, content=body, auth=auth)
|
|
439
|
+
response = await session.post(token_url, headers=headers, content=body, auth=auth, params=params)
|
|
384
440
|
content = response.json()
|
|
385
441
|
self._refresh_token = content.get("refresh_token")
|
|
386
442
|
self._id_token = content.get("id_token")
|
fastapi_sso/sso/facebook.py
CHANGED
fastapi_sso/sso/fitbit.py
CHANGED
fastapi_sso/sso/generic.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Uni
|
|
|
8
8
|
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
import httpx
|
|
11
|
+
import httpx # pragma: no cover
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
fastapi_sso/sso/github.py
CHANGED
fastapi_sso/sso/gitlab.py
CHANGED
fastapi_sso/sso/kakao.py
CHANGED
fastapi_sso/sso/line.py
CHANGED
fastapi_sso/sso/linkedin.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Dict, Optional
|
|
|
5
5
|
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
-
import httpx
|
|
8
|
+
import httpx # pragma: no cover
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class LinkedInSSO(SSOBase):
|
|
@@ -30,8 +30,8 @@ class LinkedInSSO(SSOBase):
|
|
|
30
30
|
return OpenID(
|
|
31
31
|
email=response.get("email"),
|
|
32
32
|
provider=self.provider,
|
|
33
|
-
id=response
|
|
34
|
-
first_name=response
|
|
35
|
-
last_name=response
|
|
36
|
-
picture=response
|
|
33
|
+
id=response.get("sub"),
|
|
34
|
+
first_name=response.get("given_name"),
|
|
35
|
+
last_name=response.get("family_name"),
|
|
36
|
+
picture=response.get("picture"),
|
|
37
37
|
)
|
fastapi_sso/sso/microsoft.py
CHANGED
fastapi_sso/sso/naver.py
CHANGED
fastapi_sso/sso/spotify.py
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Twitter (X) SSO Oauth Helper class"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, 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
|
+
class TwitterSSO(SSOBase):
|
|
12
|
+
"""Class providing login via Twitter SSO"""
|
|
13
|
+
|
|
14
|
+
provider = "twitter"
|
|
15
|
+
scope = ["users.read", "tweet.read"]
|
|
16
|
+
uses_pkce = True
|
|
17
|
+
requires_state = True
|
|
18
|
+
|
|
19
|
+
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
20
|
+
return {
|
|
21
|
+
"authorization_endpoint": "https://twitter.com/i/oauth2/authorize",
|
|
22
|
+
"token_endpoint": "https://api.twitter.com/2/oauth2/token",
|
|
23
|
+
"userinfo_endpoint": "https://api.twitter.com/2/users/me",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
|
|
27
|
+
first_name, *last_name_parts = response["data"].get("name", "").split(" ")
|
|
28
|
+
last_name = " ".join(last_name_parts) if last_name_parts else None
|
|
29
|
+
return OpenID(
|
|
30
|
+
id=str(response["data"]["id"]),
|
|
31
|
+
display_name=response["data"]["username"],
|
|
32
|
+
first_name=first_name,
|
|
33
|
+
last_name=last_name,
|
|
34
|
+
provider=self.provider,
|
|
35
|
+
)
|
fastapi_sso/state.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Helper functions to generate state param"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_random_state(length: int = 64) -> str:
|
|
8
|
+
"""Generate a url-safe string to use as a state"""
|
|
9
|
+
bytes_length = int(length * 3 / 4)
|
|
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.
|
|
3
|
+
Version: 0.13.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
|
|
@@ -0,0 +1,24 @@
|
|
|
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=VypbWdKTC4B2cmHpNv7TI8VjbI0Sl1gMwak8cHf3Z0w,17130
|
|
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=cHf6fy8jP2zQYt1NUp3ah5qf0134n88FLk2sHZeWZds,1052
|
|
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=L9rMTESnZrKZb1Z3odxn2oR2hxjrbnPa8rG8LcypbqU,921
|
|
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/state.py,sha256=bwqsl73I5_VSf-TfCTvDfVb7L_3PwUqtP7jDjco-8Nw,298
|
|
21
|
+
fastapi_sso-0.13.0.dist-info/LICENSE.md,sha256=5NVQtYs6liDtYdWM4VObWmTTKaK0k9C9txx5pLPJSyQ,1093
|
|
22
|
+
fastapi_sso-0.13.0.dist-info/METADATA,sha256=XQw80jXm5vDEMzgKxrNhiN1xek3iZXbp4O7ZXQClTEI,4549
|
|
23
|
+
fastapi_sso-0.13.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
24
|
+
fastapi_sso-0.13.0.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
fastapi_sso/__init__.py,sha256=aqYfVqDcq3sk8BuPNbcVnSED6ouoHi-1is9oe_Lgl-M,654
|
|
2
|
-
fastapi_sso/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
fastapi_sso/sso/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
fastapi_sso/sso/base.py,sha256=K2UnC8ay20lFeCn5WUx1z-DYzpqFYqDC8RZjpVdamTI,14607
|
|
5
|
-
fastapi_sso/sso/facebook.py,sha256=F1ox1auZ5PlJW2xccAN0oF8HpvTA5tF6cdPdXar_xF0,1327
|
|
6
|
-
fastapi_sso/sso/fitbit.py,sha256=OcCL1Go5jZ6XAarucORErX7Nuk9g9qFCQPamdcEbcFE,1258
|
|
7
|
-
fastapi_sso/sso/generic.py,sha256=pLVK7WuxiQS8XdblkdC5h6he6BcybTdMCrXvfdJlvA8,2625
|
|
8
|
-
fastapi_sso/sso/github.py,sha256=DM4gDK0QTqf8Iftn0La3V7g7bBZ6KuXUB2QyVUELXro,1693
|
|
9
|
-
fastapi_sso/sso/gitlab.py,sha256=DzvbTkW0dalDDJVGbbQkr8G5Q3GuMF991-aM_L15v64,1032
|
|
10
|
-
fastapi_sso/sso/google.py,sha256=iyVqh5YG5Xw-Ss4y8xEv1APDvubdI9Vt3ijvPMmStVk,1380
|
|
11
|
-
fastapi_sso/sso/kakao.py,sha256=WvlPMWdYSDM_XHv8wtbvzIzm3xUzxmaEulcf-UaeQSE,864
|
|
12
|
-
fastapi_sso/sso/line.py,sha256=s0nsC11wXvCvddwCWSiv108cvCFUPHkF2FEpdOAVMj4,1190
|
|
13
|
-
fastapi_sso/sso/linkedin.py,sha256=QLehBYsbGUdV4QWluS3ERuxvcf2w1k5kyNhZ4zW8OYM,1236
|
|
14
|
-
fastapi_sso/sso/microsoft.py,sha256=tVxHHGlXTuFCwiLh8pcM7I9aP9ncea5U0ZPp-JEB6hU,1918
|
|
15
|
-
fastapi_sso/sso/naver.py,sha256=mwv7ondZHzaUOXleMiBNkKEHnUywctVUjw0wo83l-II,901
|
|
16
|
-
fastapi_sso/sso/notion.py,sha256=3OU70JpE4mle9DXyZrf4sRpwq1raWcOHjFJrRmsI-EM,1302
|
|
17
|
-
fastapi_sso/sso/spotify.py,sha256=GhB-Fg3C4In_kzkEUCiPHi00oa1a2Z-QuhX3MCve8m4,1249
|
|
18
|
-
fastapi_sso-0.12.1.dist-info/LICENSE.md,sha256=5NVQtYs6liDtYdWM4VObWmTTKaK0k9C9txx5pLPJSyQ,1093
|
|
19
|
-
fastapi_sso-0.12.1.dist-info/METADATA,sha256=MVJfKbWsFkIdawyMkCf9rUqVagOqj1HBQx4yCh7wbkg,4549
|
|
20
|
-
fastapi_sso-0.12.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
21
|
-
fastapi_sso-0.12.1.dist-info/RECORD,,
|
|
File without changes
|