fastapi-sso 0.14.2__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/PKG-INFO +17 -2
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/README.md +16 -1
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/__init__.py +22 -1
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/pkce.py +2 -2
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/base.py +44 -51
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/facebook.py +9 -9
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/fitbit.py +6 -7
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/generic.py +3 -4
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/github.py +7 -6
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/gitlab.py +5 -6
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/google.py +7 -8
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/kakao.py +4 -4
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/line.py +6 -7
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/linkedin.py +5 -5
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/microsoft.py +4 -4
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/naver.py +5 -5
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/notion.py +5 -5
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/spotify.py +8 -12
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/twitter.py +4 -4
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/yandex.py +3 -6
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/state.py +2 -2
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/pyproject.toml +43 -7
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/LICENSE.md +0 -0
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/py.typed +0 -0
- {fastapi_sso-0.14.2 → fastapi_sso-0.15.0}/fastapi_sso/sso/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-sso
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.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
|
|
@@ -28,7 +28,7 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|

|
|
29
29
|
[](https://codecov.io/gh/tomasvotava/fastapi-sso)
|
|
30
30
|

|
|
31
|
-

|
|
32
32
|

|
|
33
33
|

|
|
34
34
|

|
|
@@ -46,6 +46,21 @@ backend very easily.
|
|
|
46
46
|
|
|
47
47
|
**Source Code**: [https://github.com/tomasvotava/fastapi-sso](https://github.com/tomasvotava/fastapi-sso/)
|
|
48
48
|
|
|
49
|
+
## Demo site
|
|
50
|
+
|
|
51
|
+
An awesome demo site was created and is maintained by even awesomer
|
|
52
|
+
[Chris Karvouniaris (@chrisK824)](https://github.com/chrisK824). Chris has also posted multiple
|
|
53
|
+
Medium articles about FastAPI and FastAPI SSO.
|
|
54
|
+
|
|
55
|
+
Be sure to see his tutorials, follow him and show him some appreciation!
|
|
56
|
+
|
|
57
|
+
Please see his [announcement](https://github.com/tomasvotava/fastapi-sso/discussions/150) with all the links.
|
|
58
|
+
|
|
59
|
+
Quick links for the eager ones:
|
|
60
|
+
|
|
61
|
+
- [Demo site](https://fastapi-sso-example.vercel.app/)
|
|
62
|
+
- [Medium articles](https://medium.com/@christos.karvouniaris247)
|
|
63
|
+
|
|
49
64
|
## Security warning
|
|
50
65
|
|
|
51
66
|
Please note that versions preceding `0.7.0` had a security vulnerability.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|

|
|
4
4
|
[](https://codecov.io/gh/tomasvotava/fastapi-sso)
|
|
5
5
|

|
|
6
|
-

|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
@@ -21,6 +21,21 @@ backend very easily.
|
|
|
21
21
|
|
|
22
22
|
**Source Code**: [https://github.com/tomasvotava/fastapi-sso](https://github.com/tomasvotava/fastapi-sso/)
|
|
23
23
|
|
|
24
|
+
## Demo site
|
|
25
|
+
|
|
26
|
+
An awesome demo site was created and is maintained by even awesomer
|
|
27
|
+
[Chris Karvouniaris (@chrisK824)](https://github.com/chrisK824). Chris has also posted multiple
|
|
28
|
+
Medium articles about FastAPI and FastAPI SSO.
|
|
29
|
+
|
|
30
|
+
Be sure to see his tutorials, follow him and show him some appreciation!
|
|
31
|
+
|
|
32
|
+
Please see his [announcement](https://github.com/tomasvotava/fastapi-sso/discussions/150) with all the links.
|
|
33
|
+
|
|
34
|
+
Quick links for the eager ones:
|
|
35
|
+
|
|
36
|
+
- [Demo site](https://fastapi-sso-example.vercel.app/)
|
|
37
|
+
- [Medium articles](https://medium.com/@christos.karvouniaris247)
|
|
38
|
+
|
|
24
39
|
## Security warning
|
|
25
40
|
|
|
26
41
|
Please note that versions preceding `0.7.0` had a security vulnerability.
|
|
@@ -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
|
+
]
|
|
@@ -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]
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
"""SSO login base dependency
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
# pylint: disable=too-few-public-methods
|
|
1
|
+
"""SSO login base dependency."""
|
|
5
2
|
|
|
6
3
|
import json
|
|
4
|
+
import logging
|
|
7
5
|
import os
|
|
8
|
-
import sys
|
|
9
6
|
import warnings
|
|
10
7
|
from types import TracebackType
|
|
11
|
-
from typing import Any, Dict, List, Literal, Optional, Type, Union, overload
|
|
8
|
+
from typing import Any, ClassVar, Dict, List, Literal, Optional, Type, TypedDict, Union, overload
|
|
12
9
|
|
|
13
10
|
import httpx
|
|
14
11
|
import pydantic
|
|
@@ -20,31 +17,33 @@ from starlette.responses import RedirectResponse
|
|
|
20
17
|
from fastapi_sso.pkce import get_pkce_challenge_pair
|
|
21
18
|
from fastapi_sso.state import generate_random_state
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
else:
|
|
26
|
-
from typing_extensions import TypedDict # pragma: no cover
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
27
22
|
|
|
28
|
-
DiscoveryDocument
|
|
29
|
-
"
|
|
30
|
-
|
|
23
|
+
class DiscoveryDocument(TypedDict):
|
|
24
|
+
"""Discovery document."""
|
|
25
|
+
|
|
26
|
+
authorization_endpoint: str
|
|
27
|
+
token_endpoint: str
|
|
28
|
+
userinfo_endpoint: str
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
class UnsetStateWarning(UserWarning):
|
|
34
|
-
"""Warning about unset state parameter"""
|
|
32
|
+
"""Warning about unset state parameter."""
|
|
35
33
|
|
|
36
34
|
|
|
37
35
|
class ReusedOauthClientWarning(UserWarning):
|
|
38
|
-
"""Warning about reused oauth client instance"""
|
|
36
|
+
"""Warning about reused oauth client instance."""
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
class SSOLoginError(HTTPException):
|
|
42
|
-
"""Raised when any login-related error ocurrs
|
|
43
|
-
|
|
40
|
+
"""Raised when any login-related error ocurrs.
|
|
41
|
+
|
|
42
|
+
Such as when user is not verified or if there was an attempt for fake login.
|
|
44
43
|
"""
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
class OpenID(pydantic.BaseModel):
|
|
46
|
+
class OpenID(pydantic.BaseModel):
|
|
48
47
|
"""Class (schema) to represent information got from sso provider in a common form."""
|
|
49
48
|
|
|
50
49
|
id: Optional[str] = None
|
|
@@ -56,16 +55,15 @@ class OpenID(pydantic.BaseModel): # pylint: disable=no-member
|
|
|
56
55
|
provider: Optional[str] = None
|
|
57
56
|
|
|
58
57
|
|
|
59
|
-
# pylint: disable=too-many-instance-attributes
|
|
60
58
|
class SSOBase:
|
|
61
|
-
"""Base class
|
|
59
|
+
"""Base class for all SSO providers."""
|
|
62
60
|
|
|
63
61
|
provider: str = NotImplemented
|
|
64
62
|
client_id: str = NotImplemented
|
|
65
63
|
client_secret: str = NotImplemented
|
|
66
64
|
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = NotImplemented
|
|
67
|
-
scope: List[str] =
|
|
68
|
-
additional_headers: Optional[Dict[str, Any]] = None
|
|
65
|
+
scope: ClassVar[List[str]] = []
|
|
66
|
+
additional_headers: ClassVar[Optional[Dict[str, Any]]] = None
|
|
69
67
|
uses_pkce: bool = False
|
|
70
68
|
requires_state: bool = False
|
|
71
69
|
|
|
@@ -80,7 +78,7 @@ class SSOBase:
|
|
|
80
78
|
use_state: bool = False,
|
|
81
79
|
scope: Optional[List[str]] = None,
|
|
82
80
|
):
|
|
83
|
-
|
|
81
|
+
"""Base class (mixin) for all SSO providers."""
|
|
84
82
|
self.client_id: str = client_id
|
|
85
83
|
self.client_secret: str = client_secret
|
|
86
84
|
self.redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = redirect_uri
|
|
@@ -89,6 +87,7 @@ class SSOBase:
|
|
|
89
87
|
self._generated_state: Optional[str] = None
|
|
90
88
|
|
|
91
89
|
if self.allow_insecure_http:
|
|
90
|
+
logger.debug("Initializing %s with allow_insecure_http=True", self.__class__.__name__)
|
|
92
91
|
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
|
93
92
|
|
|
94
93
|
# TODO: Remove use_state argument and attribute
|
|
@@ -100,7 +99,7 @@ class SSOBase:
|
|
|
100
99
|
),
|
|
101
100
|
DeprecationWarning,
|
|
102
101
|
)
|
|
103
|
-
self.
|
|
102
|
+
self._scope = scope or self.scope
|
|
104
103
|
self._refresh_token: Optional[str] = None
|
|
105
104
|
self._id_token: Optional[str] = None
|
|
106
105
|
self._state: Optional[str] = None
|
|
@@ -110,8 +109,7 @@ class SSOBase:
|
|
|
110
109
|
|
|
111
110
|
@property
|
|
112
111
|
def state(self) -> Optional[str]:
|
|
113
|
-
"""
|
|
114
|
-
Retrieves the state as it was returned from the server.
|
|
112
|
+
"""Retrieves the state as it was returned from the server.
|
|
115
113
|
|
|
116
114
|
Warning:
|
|
117
115
|
This will emit a warning if the state is unset, implying either that
|
|
@@ -131,8 +129,7 @@ class SSOBase:
|
|
|
131
129
|
|
|
132
130
|
@property
|
|
133
131
|
def oauth_client(self) -> WebApplicationClient:
|
|
134
|
-
"""
|
|
135
|
-
Retrieves the OAuth Client to aid in generating requests and parsing responses.
|
|
132
|
+
"""Retrieves the OAuth Client to aid in generating requests and parsing responses.
|
|
136
133
|
|
|
137
134
|
Raises:
|
|
138
135
|
NotImplementedError: If the provider is not supported or `client_id` is not set.
|
|
@@ -148,8 +145,7 @@ class SSOBase:
|
|
|
148
145
|
|
|
149
146
|
@property
|
|
150
147
|
def access_token(self) -> Optional[str]:
|
|
151
|
-
"""
|
|
152
|
-
Retrieves the access token from token endpoint.
|
|
148
|
+
"""Retrieves the access token from token endpoint.
|
|
153
149
|
|
|
154
150
|
Returns:
|
|
155
151
|
Optional[str]: The access token if available.
|
|
@@ -158,8 +154,7 @@ class SSOBase:
|
|
|
158
154
|
|
|
159
155
|
@property
|
|
160
156
|
def refresh_token(self) -> Optional[str]:
|
|
161
|
-
"""
|
|
162
|
-
Retrieves the refresh token if returned from provider.
|
|
157
|
+
"""Retrieves the refresh token if returned from provider.
|
|
163
158
|
|
|
164
159
|
Returns:
|
|
165
160
|
Optional[str]: The refresh token if available.
|
|
@@ -168,8 +163,7 @@ class SSOBase:
|
|
|
168
163
|
|
|
169
164
|
@property
|
|
170
165
|
def id_token(self) -> Optional[str]:
|
|
171
|
-
"""
|
|
172
|
-
Retrieves the id token if returned from provider.
|
|
166
|
+
"""Retrieves the id token if returned from provider.
|
|
173
167
|
|
|
174
168
|
Returns:
|
|
175
169
|
Optional[str]: The id token if available.
|
|
@@ -177,8 +171,7 @@ class SSOBase:
|
|
|
177
171
|
return self._id_token
|
|
178
172
|
|
|
179
173
|
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.
|
|
174
|
+
"""Converts a response from the provider's user info endpoint to an OpenID object.
|
|
182
175
|
|
|
183
176
|
Args:
|
|
184
177
|
response (dict): The response from the user info endpoint.
|
|
@@ -193,8 +186,7 @@ class SSOBase:
|
|
|
193
186
|
raise NotImplementedError(f"Provider {self.provider} not supported")
|
|
194
187
|
|
|
195
188
|
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
196
|
-
"""
|
|
197
|
-
Retrieves the discovery document containing useful URLs.
|
|
189
|
+
"""Retrieves the discovery document containing useful URLs.
|
|
198
190
|
|
|
199
191
|
Raises:
|
|
200
192
|
NotImplementedError: If the provider is not supported.
|
|
@@ -206,19 +198,19 @@ class SSOBase:
|
|
|
206
198
|
|
|
207
199
|
@property
|
|
208
200
|
async def authorization_endpoint(self) -> Optional[str]:
|
|
209
|
-
"""Return `authorization_endpoint` from discovery document"""
|
|
201
|
+
"""Return `authorization_endpoint` from discovery document."""
|
|
210
202
|
discovery = await self.get_discovery_document()
|
|
211
203
|
return discovery.get("authorization_endpoint")
|
|
212
204
|
|
|
213
205
|
@property
|
|
214
206
|
async def token_endpoint(self) -> Optional[str]:
|
|
215
|
-
"""Return `token_endpoint` from discovery document"""
|
|
207
|
+
"""Return `token_endpoint` from discovery document."""
|
|
216
208
|
discovery = await self.get_discovery_document()
|
|
217
209
|
return discovery.get("token_endpoint")
|
|
218
210
|
|
|
219
211
|
@property
|
|
220
212
|
async def userinfo_endpoint(self) -> Optional[str]:
|
|
221
|
-
"""Return `userinfo_endpoint` from discovery document"""
|
|
213
|
+
"""Return `userinfo_endpoint` from discovery document."""
|
|
222
214
|
discovery = await self.get_discovery_document()
|
|
223
215
|
return discovery.get("userinfo_endpoint")
|
|
224
216
|
|
|
@@ -229,8 +221,7 @@ class SSOBase:
|
|
|
229
221
|
params: Optional[Dict[str, Any]] = None,
|
|
230
222
|
state: Optional[str] = None,
|
|
231
223
|
) -> str:
|
|
232
|
-
"""
|
|
233
|
-
Generates and returns the prepared login URL.
|
|
224
|
+
"""Generates and returns the prepared login URL.
|
|
234
225
|
|
|
235
226
|
Args:
|
|
236
227
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
@@ -263,7 +254,7 @@ class SSOBase:
|
|
|
263
254
|
await self.authorization_endpoint,
|
|
264
255
|
redirect_uri=redirect_uri,
|
|
265
256
|
state=state,
|
|
266
|
-
scope=self.
|
|
257
|
+
scope=self._scope,
|
|
267
258
|
code_challenge=self._pkce_code_challenge,
|
|
268
259
|
code_challenge_method=self._pkce_challenge_method,
|
|
269
260
|
**params,
|
|
@@ -277,8 +268,7 @@ class SSOBase:
|
|
|
277
268
|
params: Optional[Dict[str, Any]] = None,
|
|
278
269
|
state: Optional[str] = None,
|
|
279
270
|
) -> RedirectResponse:
|
|
280
|
-
"""
|
|
281
|
-
Constructs and returns a redirect response to the login page of OAuth SSO provider.
|
|
271
|
+
"""Constructs and returns a redirect response to the login page of OAuth SSO provider.
|
|
282
272
|
|
|
283
273
|
Args:
|
|
284
274
|
redirect_uri (Optional[str]): Overrides the `redirect_uri` specified on this instance.
|
|
@@ -327,8 +317,7 @@ class SSOBase:
|
|
|
327
317
|
redirect_uri: Optional[str] = None,
|
|
328
318
|
convert_response: Union[Literal[True], Literal[False]] = True,
|
|
329
319
|
) -> 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.
|
|
320
|
+
"""Processes the login given a FastAPI (Starlette) Request object. This should be used for the /callback path.
|
|
332
321
|
|
|
333
322
|
Args:
|
|
334
323
|
request (Request): FastAPI or Starlette request object.
|
|
@@ -347,6 +336,12 @@ class SSOBase:
|
|
|
347
336
|
headers = headers or {}
|
|
348
337
|
code = request.query_params.get("code")
|
|
349
338
|
if code is None:
|
|
339
|
+
logger.debug(
|
|
340
|
+
"Callback request:\n\tURI: %s\n\tHeaders: %s\n\tQuery params: %s",
|
|
341
|
+
request.url,
|
|
342
|
+
request.headers,
|
|
343
|
+
request.query_params,
|
|
344
|
+
)
|
|
350
345
|
raise SSOLoginError(400, "'code' parameter was not found in callback request")
|
|
351
346
|
self._state = request.query_params.get("state")
|
|
352
347
|
pkce_code_verifier: Optional[str] = None
|
|
@@ -426,8 +421,7 @@ class SSOBase:
|
|
|
426
421
|
pkce_code_verifier: Optional[str] = None,
|
|
427
422
|
convert_response: Union[Literal[True], Literal[False]] = True,
|
|
428
423
|
) -> Union[Optional[OpenID], Optional[Dict[str, Any]]]:
|
|
429
|
-
"""
|
|
430
|
-
Processes login from the callback endpoint to verify the user and request user info endpoint.
|
|
424
|
+
"""Processes login from the callback endpoint to verify the user and request user info endpoint.
|
|
431
425
|
It's a lower-level method, typically, you should use `verify_and_process` instead.
|
|
432
426
|
|
|
433
427
|
Args:
|
|
@@ -446,7 +440,6 @@ class SSOBase:
|
|
|
446
440
|
Optional[OpenID]: User information in OpenID format if the login was successful (convert_response == True).
|
|
447
441
|
Optional[Dict[str, Any]]: Original userinfo API endpoint response.
|
|
448
442
|
"""
|
|
449
|
-
# pylint: disable=too-many-locals
|
|
450
443
|
if self._oauth_client is not None: # pragma: no cover
|
|
451
444
|
self._oauth_client = None
|
|
452
445
|
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/
|
|
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"),
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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"),
|
|
@@ -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()
|
|
@@ -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
|
-
|
|
15
|
+
scop: ClassVar = ["openid"]
|
|
16
16
|
version = "v2"
|
|
17
17
|
|
|
18
18
|
async def get_discovery_document(self) -> DiscoveryDocument:
|
|
@@ -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
|
|
|
@@ -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 {
|
|
@@ -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 {
|
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -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
|
[tool.poetry]
|
|
2
2
|
name = "fastapi-sso"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.15.0"
|
|
4
4
|
description = "FastAPI plugin to enable SSO to most common providers (such as Facebook login, Google login and login via Microsoft Office 365 Account)"
|
|
5
5
|
authors = ["Tomas Votava <info@tomasvotava.eu>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -37,10 +37,46 @@ addopts = [
|
|
|
37
37
|
[tool.black]
|
|
38
38
|
line-length = 120
|
|
39
39
|
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
target-version = "py38"
|
|
42
|
+
line-length = 120
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = [
|
|
46
|
+
#"D",
|
|
47
|
+
"E",
|
|
48
|
+
"F",
|
|
49
|
+
"B",
|
|
50
|
+
"I",
|
|
51
|
+
"N",
|
|
52
|
+
"UP",
|
|
53
|
+
"S",
|
|
54
|
+
"A",
|
|
55
|
+
"DTZ",
|
|
56
|
+
"PT",
|
|
57
|
+
"SIM",
|
|
58
|
+
"PTH",
|
|
59
|
+
"PD",
|
|
60
|
+
"RUF",
|
|
61
|
+
"T20",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
ignore = [
|
|
65
|
+
"B028", # allow warning without specifying `stacklevel`
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.pydocstyle]
|
|
69
|
+
convention = "google"
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.isort]
|
|
72
|
+
known-first-party = ["fastapi_sso"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"tests*/**/*.py" = ["S101"] # Allow asserts in tests
|
|
76
|
+
"**/__init__.py" = ["D104"] # Allow missing docstrings in __init__ files
|
|
40
77
|
|
|
41
78
|
[tool.poe.tasks]
|
|
42
|
-
|
|
43
|
-
todos = "pylint --disable all --enable fixme --rcfile .pylintrc fastapi_sso"
|
|
79
|
+
ruff = "ruff check fastapi_sso"
|
|
44
80
|
black = "black fastapi_sso"
|
|
45
81
|
isort = "isort --settings-path .isort.cfg fastapi_sso"
|
|
46
82
|
mypy = "mypy --config-file mypy.ini fastapi_sso"
|
|
@@ -48,7 +84,7 @@ black-check = "black --check fastapi_sso"
|
|
|
48
84
|
isort-check = "isort --settings-path .isort.cfg --check-only fastapi_sso"
|
|
49
85
|
|
|
50
86
|
format = ["black", "isort"]
|
|
51
|
-
lint = ["
|
|
87
|
+
lint = ["ruff", "mypy", "black-check", "isort-check"]
|
|
52
88
|
pre-commit = "pre-commit"
|
|
53
89
|
|
|
54
90
|
test = "pytest"
|
|
@@ -61,17 +97,17 @@ black = ">=23.7.0"
|
|
|
61
97
|
isort = "^5"
|
|
62
98
|
markdown-include = "^0.8.1"
|
|
63
99
|
mkdocs-material = { extras = ["imaging"], version = "^9.3.2" }
|
|
64
|
-
mkdocstrings = { extras = ["python"], version = ">=0.23,<0.
|
|
100
|
+
mkdocstrings = { extras = ["python"], version = ">=0.23,<0.26" }
|
|
65
101
|
mypy = "^1"
|
|
66
|
-
poethepoet = ">=0.21.1,<0.
|
|
102
|
+
poethepoet = ">=0.21.1,<0.27.0"
|
|
67
103
|
pre-commit = "^3"
|
|
68
|
-
pylint = ">=2,<4"
|
|
69
104
|
pytest = ">=7,<9"
|
|
70
105
|
pytest-asyncio = ">=0.21.1,<0.24.0"
|
|
71
106
|
pytest-cov = ">=4,<6"
|
|
72
107
|
pytest-xdist = "^3"
|
|
73
108
|
tox = "^4"
|
|
74
109
|
uvicorn = ">=0.23.1"
|
|
110
|
+
ruff = "^0.4.2"
|
|
75
111
|
|
|
76
112
|
[tool.poetry.dependencies]
|
|
77
113
|
fastapi = ">=0.80"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|