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 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
- ) -> Optional[OpenID]:
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 in OpenID format if the login was successful.
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
- ) -> Optional[OpenID]:
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, params=params)
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
- return await self.openid_from_response(content, session)
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": "https://gitlab.com/oauth/authorize",
21
- "token_endpoint": "https://gitlab.com/oauth/token",
22
- "userinfo_endpoint": "https://gitlab.com/api/v4/user",
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.13.1
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=VypbWdKTC4B2cmHpNv7TI8VjbI0Sl1gMwak8cHf3Z0w,17130
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=Fdq4_f3YmxQUEEx11GahXh6ELwExRyOC9ERwNYvbIHw,1057
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.13.1.dist-info/LICENSE.md,sha256=5NVQtYs6liDtYdWM4VObWmTTKaK0k9C9txx5pLPJSyQ,1093
22
- fastapi_sso-0.13.1.dist-info/METADATA,sha256=oCN_56pDK4ELk7iPOAYHjLuB9FvgBnxbIR3gcpjouj0,4563
23
- fastapi_sso-0.13.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
24
- fastapi_sso-0.13.1.dist-info/RECORD,,
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,,