fastapi-sso 0.13.1__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_sso/sso/base.py +67 -11
- fastapi_sso/sso/gitlab.py +49 -4
- fastapi_sso/sso/yandex.py +44 -0
- {fastapi_sso-0.13.1.dist-info → fastapi_sso-0.14.0.dist-info}/METADATA +2 -1
- {fastapi_sso-0.13.1.dist-info → fastapi_sso-0.14.0.dist-info}/RECORD +7 -6
- {fastapi_sso-0.13.1.dist-info → fastapi_sso-0.14.0.dist-info}/LICENSE.md +0 -0
- {fastapi_sso-0.13.1.dist-info → fastapi_sso-0.14.0.dist-info}/WHEEL +0 -0
fastapi_sso/sso/base.py
CHANGED
|
@@ -8,7 +8,7 @@ import os
|
|
|
8
8
|
import sys
|
|
9
9
|
import warnings
|
|
10
10
|
from types import TracebackType
|
|
11
|
-
from typing import Any, Dict, List, Optional, Type, Union
|
|
11
|
+
from typing import Any, Dict, List, Literal, Optional, Type, Union, overload
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
import pydantic
|
|
@@ -296,6 +296,7 @@ class SSOBase:
|
|
|
296
296
|
response.set_cookie("pkce_code_verifier", str(self._pkce_code_verifier))
|
|
297
297
|
return response
|
|
298
298
|
|
|
299
|
+
@overload
|
|
299
300
|
async def verify_and_process(
|
|
300
301
|
self,
|
|
301
302
|
request: Request,
|
|
@@ -303,7 +304,29 @@ class SSOBase:
|
|
|
303
304
|
params: Optional[Dict[str, Any]] = None,
|
|
304
305
|
headers: Optional[Dict[str, Any]] = None,
|
|
305
306
|
redirect_uri: Optional[str] = None,
|
|
306
|
-
|
|
307
|
+
convert_response: Literal[True] = True,
|
|
308
|
+
) -> Optional[OpenID]: ...
|
|
309
|
+
|
|
310
|
+
@overload
|
|
311
|
+
async def verify_and_process(
|
|
312
|
+
self,
|
|
313
|
+
request: Request,
|
|
314
|
+
*,
|
|
315
|
+
params: Optional[Dict[str, Any]] = None,
|
|
316
|
+
headers: Optional[Dict[str, Any]] = None,
|
|
317
|
+
redirect_uri: Optional[str] = None,
|
|
318
|
+
convert_response: Literal[False],
|
|
319
|
+
) -> Optional[Dict[str, Any]]: ...
|
|
320
|
+
|
|
321
|
+
async def verify_and_process(
|
|
322
|
+
self,
|
|
323
|
+
request: Request,
|
|
324
|
+
*,
|
|
325
|
+
params: Optional[Dict[str, Any]] = None,
|
|
326
|
+
headers: Optional[Dict[str, Any]] = None,
|
|
327
|
+
redirect_uri: Optional[str] = None,
|
|
328
|
+
convert_response: Union[Literal[True], Literal[False]] = True,
|
|
329
|
+
) -> Union[Optional[OpenID], Optional[Dict[str, Any]]]:
|
|
307
330
|
"""
|
|
308
331
|
Processes the login given a FastAPI (Starlette) Request object. This should be used for the /callback path.
|
|
309
332
|
|
|
@@ -312,12 +335,14 @@ class SSOBase:
|
|
|
312
335
|
params (Optional[Dict[str, Any]]): Additional query parameters to pass to the provider.
|
|
313
336
|
headers (Optional[Dict[str, Any]]): Additional headers to pass to the provider.
|
|
314
337
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
338
|
+
convert_response (bool): If True, userinfo response is converted to OpenID object.
|
|
315
339
|
|
|
316
340
|
Raises:
|
|
317
341
|
SSOLoginError: If the 'code' parameter is not found in the callback request.
|
|
318
342
|
|
|
319
343
|
Returns:
|
|
320
|
-
Optional[OpenID]: User information
|
|
344
|
+
Optional[OpenID]: User information as OpenID instance (if convert_response == True)
|
|
345
|
+
Optional[Dict[str, Any]]: The original JSON response from the API.
|
|
321
346
|
"""
|
|
322
347
|
headers = headers or {}
|
|
323
348
|
code = request.query_params.get("code")
|
|
@@ -338,6 +363,7 @@ class SSOBase:
|
|
|
338
363
|
additional_headers=headers,
|
|
339
364
|
redirect_uri=redirect_uri,
|
|
340
365
|
pkce_code_verifier=pkce_code_verifier,
|
|
366
|
+
convert_response=convert_response,
|
|
341
367
|
)
|
|
342
368
|
|
|
343
369
|
def __enter__(self) -> "SSOBase":
|
|
@@ -363,6 +389,7 @@ class SSOBase:
|
|
|
363
389
|
def _extra_query_params(self) -> Dict:
|
|
364
390
|
return {}
|
|
365
391
|
|
|
392
|
+
@overload
|
|
366
393
|
async def process_login(
|
|
367
394
|
self,
|
|
368
395
|
code: str,
|
|
@@ -372,7 +399,33 @@ class SSOBase:
|
|
|
372
399
|
additional_headers: Optional[Dict[str, Any]] = None,
|
|
373
400
|
redirect_uri: Optional[str] = None,
|
|
374
401
|
pkce_code_verifier: Optional[str] = None,
|
|
375
|
-
|
|
402
|
+
convert_response: Literal[True] = True,
|
|
403
|
+
) -> Optional[OpenID]: ...
|
|
404
|
+
|
|
405
|
+
@overload
|
|
406
|
+
async def process_login(
|
|
407
|
+
self,
|
|
408
|
+
code: str,
|
|
409
|
+
request: Request,
|
|
410
|
+
*,
|
|
411
|
+
params: Optional[Dict[str, Any]] = None,
|
|
412
|
+
additional_headers: Optional[Dict[str, Any]] = None,
|
|
413
|
+
redirect_uri: Optional[str] = None,
|
|
414
|
+
pkce_code_verifier: Optional[str] = None,
|
|
415
|
+
convert_response: Literal[False],
|
|
416
|
+
) -> Optional[Dict[str, Any]]: ...
|
|
417
|
+
|
|
418
|
+
async def process_login(
|
|
419
|
+
self,
|
|
420
|
+
code: str,
|
|
421
|
+
request: Request,
|
|
422
|
+
*,
|
|
423
|
+
params: Optional[Dict[str, Any]] = None,
|
|
424
|
+
additional_headers: Optional[Dict[str, Any]] = None,
|
|
425
|
+
redirect_uri: Optional[str] = None,
|
|
426
|
+
pkce_code_verifier: Optional[str] = None,
|
|
427
|
+
convert_response: Union[Literal[True], Literal[False]] = True,
|
|
428
|
+
) -> Union[Optional[OpenID], Optional[Dict[str, Any]]]:
|
|
376
429
|
"""
|
|
377
430
|
Processes login from the callback endpoint to verify the user and request user info endpoint.
|
|
378
431
|
It's a lower-level method, typically, you should use `verify_and_process` instead.
|
|
@@ -384,12 +437,14 @@ class SSOBase:
|
|
|
384
437
|
additional_headers (Optional[Dict[str, Any]]): Additional headers to be added to all requests.
|
|
385
438
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
386
439
|
pkce_code_verifier (Optional[str]): A PKCE code verifier sent to the server to verify the login request.
|
|
440
|
+
convert_response (bool): If True, userinfo response is converted to OpenID object.
|
|
387
441
|
|
|
388
442
|
Raises:
|
|
389
443
|
ReusedOauthClientWarning: If the SSO object is reused, which is not safe and caused security issues.
|
|
390
444
|
|
|
391
445
|
Returns:
|
|
392
|
-
Optional[OpenID]: User information in OpenID format if the login was successful.
|
|
446
|
+
Optional[OpenID]: User information in OpenID format if the login was successful (convert_response == True).
|
|
447
|
+
Optional[Dict[str, Any]]: Original userinfo API endpoint response.
|
|
393
448
|
"""
|
|
394
449
|
# pylint: disable=too-many-locals
|
|
395
450
|
if self._oauth_client is not None: # pragma: no cover
|
|
@@ -417,6 +472,9 @@ class SSOBase:
|
|
|
417
472
|
|
|
418
473
|
current_path = f"{url.scheme}://{url.netloc}{url.path}"
|
|
419
474
|
|
|
475
|
+
if pkce_code_verifier:
|
|
476
|
+
params.update({"code_verifier": pkce_code_verifier})
|
|
477
|
+
|
|
420
478
|
token_url, headers, body = self.oauth_client.prepare_token_request(
|
|
421
479
|
await self.token_endpoint,
|
|
422
480
|
authorization_response=current_url,
|
|
@@ -432,11 +490,8 @@ class SSOBase:
|
|
|
432
490
|
|
|
433
491
|
auth = httpx.BasicAuth(self.client_id, self.client_secret)
|
|
434
492
|
|
|
435
|
-
if pkce_code_verifier:
|
|
436
|
-
params.update({"code_verifier": pkce_code_verifier})
|
|
437
|
-
|
|
438
493
|
async with httpx.AsyncClient() as session:
|
|
439
|
-
response = await session.post(token_url, headers=headers, content=body, auth=auth
|
|
494
|
+
response = await session.post(token_url, headers=headers, content=body, auth=auth)
|
|
440
495
|
content = response.json()
|
|
441
496
|
self._refresh_token = content.get("refresh_token")
|
|
442
497
|
self._id_token = content.get("id_token")
|
|
@@ -447,5 +502,6 @@ class SSOBase:
|
|
|
447
502
|
session.headers.update(headers)
|
|
448
503
|
response = await session.get(uri)
|
|
449
504
|
content = response.json()
|
|
450
|
-
|
|
451
|
-
|
|
505
|
+
if convert_response:
|
|
506
|
+
return await self.openid_from_response(content, session)
|
|
507
|
+
return content
|
fastapi_sso/sso/gitlab.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Gitlab SSO Oauth Helper class"""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, Optional
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
|
|
6
|
+
import pydantic
|
|
4
7
|
|
|
5
8
|
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
6
9
|
|
|
@@ -14,19 +17,61 @@ class GitlabSSO(SSOBase):
|
|
|
14
17
|
provider = "gitlab"
|
|
15
18
|
scope = ["read_user", "openid", "profile"]
|
|
16
19
|
additional_headers = {"accept": "application/json"}
|
|
20
|
+
base_endpoint_url = "https://gitlab.com"
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
client_id: str,
|
|
25
|
+
client_secret: str,
|
|
26
|
+
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
|
|
27
|
+
allow_insecure_http: bool = False,
|
|
28
|
+
use_state: bool = False, # TODO: Remove use_state argument
|
|
29
|
+
scope: Optional[List[str]] = None,
|
|
30
|
+
base_endpoint_url: Optional[str] = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(
|
|
33
|
+
client_id,
|
|
34
|
+
client_secret,
|
|
35
|
+
redirect_uri,
|
|
36
|
+
allow_insecure_http,
|
|
37
|
+
use_state, # TODO: Remove use_state argument
|
|
38
|
+
scope,
|
|
39
|
+
)
|
|
40
|
+
self.base_endpoint_url = base_endpoint_url or self.base_endpoint_url
|
|
17
41
|
|
|
18
42
|
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
43
|
+
"""Override the discovery document method to return Yandex OAuth endpoints."""
|
|
44
|
+
|
|
19
45
|
return {
|
|
20
|
-
"authorization_endpoint": "
|
|
21
|
-
"token_endpoint": "
|
|
22
|
-
"userinfo_endpoint": "
|
|
46
|
+
"authorization_endpoint": urljoin(self.base_endpoint_url, "/oauth/authorize"),
|
|
47
|
+
"token_endpoint": urljoin(self.base_endpoint_url, "/oauth/token"),
|
|
48
|
+
"userinfo_endpoint": urljoin(self.base_endpoint_url, "/api/v4/user"),
|
|
23
49
|
}
|
|
24
50
|
|
|
51
|
+
def _parse_name(self, full_name: Optional[str]) -> Tuple[Union[str, None], Union[str, None]]:
|
|
52
|
+
"""Parses the full name from Gitlab into the first and last name."""
|
|
53
|
+
if not full_name or not isinstance(full_name, str):
|
|
54
|
+
return None, None
|
|
55
|
+
|
|
56
|
+
name_parts = full_name.split()
|
|
57
|
+
|
|
58
|
+
if len(name_parts) == 1:
|
|
59
|
+
return name_parts[0], None
|
|
60
|
+
|
|
61
|
+
first_name = name_parts[0]
|
|
62
|
+
last_name = " ".join(name_parts[1:])
|
|
63
|
+
return first_name, last_name
|
|
64
|
+
|
|
25
65
|
async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
|
|
66
|
+
"""Converts Gitlab user info response to OpenID object."""
|
|
67
|
+
first_name, last_name = self._parse_name(response.get("name"))
|
|
68
|
+
|
|
26
69
|
return OpenID(
|
|
27
70
|
email=response["email"],
|
|
28
71
|
provider=self.provider,
|
|
29
72
|
id=str(response["id"]),
|
|
73
|
+
first_name=first_name,
|
|
74
|
+
last_name=last_name,
|
|
30
75
|
display_name=response["username"],
|
|
31
76
|
picture=response["avatar_url"],
|
|
32
77
|
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Yandex SSO Login Helper
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import httpx # pragma: no cover
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class YandexSSO(SSOBase):
|
|
13
|
+
"""Class providing login using Yandex OAuth."""
|
|
14
|
+
|
|
15
|
+
provider = "yandex"
|
|
16
|
+
scope = ["login:email", "login:info", "login:avatar"]
|
|
17
|
+
avatar_url = "https://avatars.yandex.net/get-yapic"
|
|
18
|
+
|
|
19
|
+
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
20
|
+
"""Override the discovery document method to return Yandex OAuth endpoints."""
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
"authorization_endpoint": "https://oauth.yandex.ru/authorize",
|
|
24
|
+
"token_endpoint": "https://oauth.yandex.ru/token",
|
|
25
|
+
"userinfo_endpoint": "https://login.yandex.ru/info",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
|
|
29
|
+
"""Converts Yandex user info response to OpenID object."""
|
|
30
|
+
|
|
31
|
+
picture = None
|
|
32
|
+
|
|
33
|
+
if (avatar_id := response.get("default_avatar_id")) is not None:
|
|
34
|
+
picture = f"{self.avatar_url}/{avatar_id}/islands-200"
|
|
35
|
+
|
|
36
|
+
return OpenID(
|
|
37
|
+
email=response.get("default_email"),
|
|
38
|
+
display_name=response.get("display_name"),
|
|
39
|
+
provider=self.provider,
|
|
40
|
+
id=response.get("id"),
|
|
41
|
+
first_name=response.get("first_name"),
|
|
42
|
+
last_name=response.get("last_name"),
|
|
43
|
+
picture=picture,
|
|
44
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-sso
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -85,6 +85,7 @@ I tend to process Pull Requests faster when properly caffeinated 😉.
|
|
|
85
85
|
- Gitlab (by Alessandro Pischedda) - [Cereal84](https://github.com/Cereal84)
|
|
86
86
|
- Line (by Jimmy Yeh) - [jimmyyyeh](https://github.com/jimmyyyeh)
|
|
87
87
|
- LinkedIn (by Alessandro Pischedda) - [Cereal84](https://github.com/Cereal84)
|
|
88
|
+
- Yandex (by Akim Faskhutdinov) – [akimrx](https://github.com/akimrx)
|
|
88
89
|
|
|
89
90
|
See [Contributing](#contributing) for a guide on how to contribute your own login provider.
|
|
90
91
|
|
|
@@ -2,12 +2,12 @@ fastapi_sso/__init__.py,sha256=bQH_ECRRvpAQfPfpe5bIiGdfRoPULFgvbSjGS9OPkYQ,690
|
|
|
2
2
|
fastapi_sso/pkce.py,sha256=_J9wMCaSwPsUk32qoHzorhB9HG79YVifcQcRA4l_N7I,790
|
|
3
3
|
fastapi_sso/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
fastapi_sso/sso/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
fastapi_sso/sso/base.py,sha256=
|
|
5
|
+
fastapi_sso/sso/base.py,sha256=DDqOSjuXzP6-GrAuQeRAt5M4aKXAsxuDXztd8orBeO0,19272
|
|
6
6
|
fastapi_sso/sso/facebook.py,sha256=fyO9R7wmhuDp8gZWOp5hogMfTLbff_7ms1XcrLL9udU,1347
|
|
7
7
|
fastapi_sso/sso/fitbit.py,sha256=ofWcrGVeZk6n4b59RI9dLxPz_GzpizJiLNGLvrIphmY,1278
|
|
8
8
|
fastapi_sso/sso/generic.py,sha256=nfH_ju0zRM4HAX-YYHMulSGRb_UEr342YAcK8JtJ3CE,2645
|
|
9
9
|
fastapi_sso/sso/github.py,sha256=GA0jalPGjgBvhJH2SSyIxiizcj6PR8ccOXQYemmaKpQ,1713
|
|
10
|
-
fastapi_sso/sso/gitlab.py,sha256=
|
|
10
|
+
fastapi_sso/sso/gitlab.py,sha256=xYpzDfah8fRD0vVClG_w2_oHWoQ3JdkVh4zOWDVhoZs,2683
|
|
11
11
|
fastapi_sso/sso/google.py,sha256=iyVqh5YG5Xw-Ss4y8xEv1APDvubdI9Vt3ijvPMmStVk,1380
|
|
12
12
|
fastapi_sso/sso/kakao.py,sha256=-St6EEI0r3oFFphFd7UgqppA0z7XtZ22FmBOyhPfJ9M,884
|
|
13
13
|
fastapi_sso/sso/line.py,sha256=1u27gqq8ZNiNn3XRumyTzNTj67vyTlo_HfVBRj4_crI,1210
|
|
@@ -17,8 +17,9 @@ fastapi_sso/sso/naver.py,sha256=L9rMTESnZrKZb1Z3odxn2oR2hxjrbnPa8rG8LcypbqU,921
|
|
|
17
17
|
fastapi_sso/sso/notion.py,sha256=3OU70JpE4mle9DXyZrf4sRpwq1raWcOHjFJrRmsI-EM,1302
|
|
18
18
|
fastapi_sso/sso/spotify.py,sha256=EyCuUK1fiP-I7NSg1ssDoPJilHnIULyXB4AyJNxPiYg,1269
|
|
19
19
|
fastapi_sso/sso/twitter.py,sha256=NsRbKZlau_8gZNKgCywlggKv1jZLRP1oDXTp-TId2PU,1227
|
|
20
|
+
fastapi_sso/sso/yandex.py,sha256=cTfxnK1xKBqGZ1GHd4Y1GtqTJGt1dYV6RAE6kACvX14,1489
|
|
20
21
|
fastapi_sso/state.py,sha256=bwqsl73I5_VSf-TfCTvDfVb7L_3PwUqtP7jDjco-8Nw,298
|
|
21
|
-
fastapi_sso-0.
|
|
22
|
-
fastapi_sso-0.
|
|
23
|
-
fastapi_sso-0.
|
|
24
|
-
fastapi_sso-0.
|
|
22
|
+
fastapi_sso-0.14.0.dist-info/LICENSE.md,sha256=5NVQtYs6liDtYdWM4VObWmTTKaK0k9C9txx5pLPJSyQ,1093
|
|
23
|
+
fastapi_sso-0.14.0.dist-info/METADATA,sha256=rhu8DYZyQHwK-A7z9eTYTpW6QILwqIKq7UkCWsUr83M,4635
|
|
24
|
+
fastapi_sso-0.14.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
25
|
+
fastapi_sso-0.14.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|