pycrossauth 0.0.2__tar.gz → 0.0.4__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.
- {pycrossauth-0.0.2/src/pycrossauth.egg-info → pycrossauth-0.0.4}/PKG-INFO +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/pyproject.toml +2 -2
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/__init__.py +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/client.py +218 -36
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/__init__.py +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapioauthclient.py +42 -3
- {pycrossauth-0.0.2 → pycrossauth-0.0.4/src/pycrossauth.egg-info}/PKG-INFO +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/LICENSE +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/NOTICE +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/README.md +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/setup.cfg +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/setup.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/auth.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/__init__.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/error.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/interfaces.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/jwt.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/logger.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/cookieauth.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/crypto.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/__init__.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/clientmanager.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/resserver.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/tokenconsumer.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/wellknown.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/session.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storage.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/__init__.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/inmemorystorage.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/sqlalchemystorage.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/utils.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapiresserver.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapiserver.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapiserverbase.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapisession.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapisessionadapter.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/pycrossauth.egg-info/SOURCES.txt +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/pycrossauth.egg-info/dependency_links.txt +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/pycrossauth.egg-info/requires.txt +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/pycrossauth.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pycrossauth"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4"
|
|
8
8
|
description = "Cross platform web authentication and OAuth framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Matthew Baker", email = "matt@mattbaker.ch" }]
|
|
@@ -56,7 +56,7 @@ requires-python = ">=3.12"
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
[tool.bumpver]
|
|
59
|
-
current_version = "0.0.
|
|
59
|
+
current_version = "0.0.4"
|
|
60
60
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
61
61
|
commit_message = "bump version {old_version} -> {new_version}"
|
|
62
62
|
commit = true
|
|
@@ -32,7 +32,7 @@ from .cookieauth import DoubleSubmitCsrfToken, DoubleSubmitCsrfTokenOptions, Ses
|
|
|
32
32
|
from .session import SessionManager, SessionManagerOptions
|
|
33
33
|
|
|
34
34
|
# Version of realpython-reader package
|
|
35
|
-
__version__ = "0.0.
|
|
35
|
+
__version__ = "0.0.3"
|
|
36
36
|
|
|
37
37
|
__all__ = (
|
|
38
38
|
"ErrorCode", "CrossauthError",
|
|
@@ -141,6 +141,11 @@ class OAuthFlows:
|
|
|
141
141
|
case _:
|
|
142
142
|
raise CrossauthError(ErrorCode.BadRequest, "Invalid OAuth flow " + oauthFlow)
|
|
143
143
|
|
|
144
|
+
class IdTokenReturn(TypedDict, total=False):
|
|
145
|
+
id_payload: Mapping[str, Any]
|
|
146
|
+
error: str
|
|
147
|
+
error_description: str
|
|
148
|
+
|
|
144
149
|
class OAuthTokenResponse(TypedDict, total=False):
|
|
145
150
|
"""
|
|
146
151
|
These are the fields that can be returned in the JSON from an OAuth call.
|
|
@@ -149,6 +154,7 @@ class OAuthTokenResponse(TypedDict, total=False):
|
|
|
149
154
|
access_token : str
|
|
150
155
|
refresh_token : str
|
|
151
156
|
id_token : str
|
|
157
|
+
id_payload : Mapping[str, Any]
|
|
152
158
|
token_type : str
|
|
153
159
|
expires_in : int
|
|
154
160
|
error : str
|
|
@@ -253,6 +259,18 @@ class OAuthClientOptions(OAuthTokenConsumerOptions, total=False):
|
|
|
253
259
|
Default `device_authorization`
|
|
254
260
|
"""
|
|
255
261
|
|
|
262
|
+
oauth_post_type : Literal["json", "form"]
|
|
263
|
+
"""
|
|
264
|
+
If set to JSON, make calls to the token endpoint as JSON, otherwise
|
|
265
|
+
as x-www-form-urlencoded.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
oauth_use_user_info_endpoint : bool
|
|
269
|
+
"""
|
|
270
|
+
If your authorization server only returns certain claims in the userinfo
|
|
271
|
+
endpoint, rather than in the id token, set this to true
|
|
272
|
+
"""
|
|
273
|
+
|
|
256
274
|
class OAuthClient:
|
|
257
275
|
"""
|
|
258
276
|
Base class for OAuth clients.
|
|
@@ -287,12 +305,12 @@ class OAuthClient:
|
|
|
287
305
|
self._auth_server_credentials : Literal["include", "omit", "same-origin" ] | None = None
|
|
288
306
|
self._auth_server_mode : Literal["no-cors", "cors", "same-origin" ] | None = None
|
|
289
307
|
self._auth_server_headers : Dict[str, str] = {}
|
|
290
|
-
self._code_challenge : str|None = None
|
|
291
|
-
self._code_verifier : str|None = None
|
|
292
|
-
self._state = ""
|
|
293
308
|
self._authz_code = ""
|
|
294
309
|
self._oidc_config : OpenIdConfiguration | None = None
|
|
295
310
|
self._device_authorization_url : str = "device_authorization"
|
|
311
|
+
self._oauth_post_type = "json"
|
|
312
|
+
self._oauth_use_user_info_endpoint = False
|
|
313
|
+
self.auth_server_redirect_base_url : str|None = None
|
|
296
314
|
|
|
297
315
|
self.auth_server_base_url = auth_server_base_url
|
|
298
316
|
set_parameter("client_id", ParamType.String, self, options, "OAUTH_CLIENT_ID", required=True, protected=True)
|
|
@@ -309,6 +327,13 @@ class OAuthClient:
|
|
|
309
327
|
set_parameter("auth_server_mode", ParamType.String, self, options, "OAUTH_AUTH_SERVER_MODE", protected=True)
|
|
310
328
|
set_parameter("auth_server_headers", ParamType.Json, self, options, "OAUTH_AUTH_SERVER_HEADERS", protected=True)
|
|
311
329
|
if (self._device_authorization_url[0:1] == "/"): self._device_authorization_url = self._device_authorization_url[1:]
|
|
330
|
+
set_parameter("oauth_post_type", ParamType.Json, self, options, "OAUTH_POST_TYPE", protected=True)
|
|
331
|
+
if (self._oauth_post_type != "json" and self._oauth_post_type != "form"):
|
|
332
|
+
raise CrossauthError(ErrorCode.Configuration, "oauth_post_type must be json or form")
|
|
333
|
+
set_parameter("oauth_use_user_info_endpoint", ParamType.Json, self, options, "OAUTH_USE_USER_INFO_ENDPOINT", protected=True)
|
|
334
|
+
set_parameter("auth_server_redirect_base_url", ParamType.Json, self, options, "AUTH_SERVER_REDIRECT_BASE_URL")
|
|
335
|
+
if (self.auth_server_redirect_base_url == None):
|
|
336
|
+
self.auth_server_redirect_base_url = self.auth_server_base_url
|
|
312
337
|
|
|
313
338
|
async def load_config(self, oidc_config : OpenIdConfiguration|None=None):
|
|
314
339
|
"""
|
|
@@ -378,7 +403,13 @@ class OAuthClient:
|
|
|
378
403
|
|
|
379
404
|
return Crypto.sha256(plaintext)
|
|
380
405
|
|
|
381
|
-
async def
|
|
406
|
+
async def code_challenge_and_verifier(self):
|
|
407
|
+
code_verifier = self.random_value(self._verifier_length)
|
|
408
|
+
code_challenge = await self.sha256(code_verifier) if self._code_challenge_method == "S256" else code_verifier
|
|
409
|
+
return {"code_verifier": code_verifier, "code_challenge": code_challenge}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
async def start_authorization_code_flow(self, state: str, scope : str | None = None, code_challenge: str|None = None, pkce : bool = False):
|
|
382
413
|
"""
|
|
383
414
|
Initiates the authorization code flow
|
|
384
415
|
|
|
@@ -402,7 +433,6 @@ class OAuthClient:
|
|
|
402
433
|
"error": "server_error",
|
|
403
434
|
"error_description": "Cannot get authorize endpoint"
|
|
404
435
|
}
|
|
405
|
-
self.state = self.random_value(self._state_length)
|
|
406
436
|
if not self._client_id:
|
|
407
437
|
return {
|
|
408
438
|
"error": "invalid_request",
|
|
@@ -415,19 +445,17 @@ class OAuthClient:
|
|
|
415
445
|
}
|
|
416
446
|
|
|
417
447
|
base = self._oidc_config["authorization_endpoint"]
|
|
418
|
-
url = f"{base}?response_type=code&client_id={urllib.parse.quote(self._client_id)}&state={urllib.parse.quote(
|
|
448
|
+
url = f"{base}?response_type=code&client_id={urllib.parse.quote(self._client_id)}&state={urllib.parse.quote(state)}&redirect_uri={urllib.parse.quote(self._redirect_uri)}"
|
|
419
449
|
|
|
420
450
|
if scope:
|
|
421
451
|
url += f"&scope={urllib.parse.quote(scope)}"
|
|
422
452
|
|
|
423
453
|
if pkce:
|
|
424
|
-
|
|
425
|
-
self._code_challenge = await self.sha256(self._code_verifier) if self._code_challenge_method == "S256" else self._code_verifier
|
|
426
|
-
url += f"&code_challenge={self._code_challenge}"
|
|
454
|
+
url += f"&code_challenge={code_challenge}"
|
|
427
455
|
|
|
428
456
|
return {"url": url}
|
|
429
457
|
|
|
430
|
-
async def redirect_endpoint(self, code : str|None = None, state : str|None = None, error : str|None =None, error_description : str|None=None) -> OAuthTokenResponse:
|
|
458
|
+
async def redirect_endpoint(self, code : str|None = None, state : str|None = None, code_verifier: str|None = None, error : str|None =None, error_description : str|None=None) -> OAuthTokenResponse:
|
|
431
459
|
"""
|
|
432
460
|
For calling in a Redirect Uri endpoint
|
|
433
461
|
|
|
@@ -449,8 +477,6 @@ class OAuthClient:
|
|
|
449
477
|
if error_description is None:
|
|
450
478
|
error_description = "Unknown error"
|
|
451
479
|
return {"error": error, "error_description": error_description}
|
|
452
|
-
if self._state and state != self._state:
|
|
453
|
-
return {"error": "access_denied", "error_description": "State is not valid"}
|
|
454
480
|
self.authzCode = code
|
|
455
481
|
|
|
456
482
|
if "authorization_code" not in self._oidc_config["grant_types_supported"]:
|
|
@@ -474,12 +500,24 @@ class OAuthClient:
|
|
|
474
500
|
}
|
|
475
501
|
if client_secret:
|
|
476
502
|
params["client_secret"] = client_secret
|
|
477
|
-
params["code_verifier"] =
|
|
503
|
+
params["code_verifier"] = code_verifier
|
|
478
504
|
try:
|
|
479
505
|
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
480
506
|
if ("id_token" in ret):
|
|
481
|
-
|
|
482
|
-
|
|
507
|
+
access_token : str|None = None
|
|
508
|
+
if "access_token" in ret:
|
|
509
|
+
access_token = ret["access_token"]
|
|
510
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
511
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
512
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
513
|
+
if (error1 is not None):
|
|
514
|
+
return {
|
|
515
|
+
"error": error1,
|
|
516
|
+
"error_description": error_description1
|
|
517
|
+
}
|
|
518
|
+
if ("id_payload" in user_info):
|
|
519
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
520
|
+
|
|
483
521
|
return ret
|
|
484
522
|
except Exception as e:
|
|
485
523
|
CrossauthLogger.logger().error(j({"cerr": e}))
|
|
@@ -526,7 +564,22 @@ class OAuthClient:
|
|
|
526
564
|
if scope:
|
|
527
565
|
params["scope"] = scope
|
|
528
566
|
try:
|
|
529
|
-
|
|
567
|
+
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
568
|
+
if ("id_token" in ret):
|
|
569
|
+
access_token : str|None = None
|
|
570
|
+
if "access_token" in ret:
|
|
571
|
+
access_token = ret["access_token"]
|
|
572
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
573
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
574
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
575
|
+
if (error1 is not None):
|
|
576
|
+
return {
|
|
577
|
+
"error": error1,
|
|
578
|
+
"error_description": error_description1
|
|
579
|
+
}
|
|
580
|
+
if ("id_payload" in user_info):
|
|
581
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
582
|
+
return ret
|
|
530
583
|
except Exception as e:
|
|
531
584
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
532
585
|
return {
|
|
@@ -577,8 +630,19 @@ class OAuthClient:
|
|
|
577
630
|
try:
|
|
578
631
|
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
579
632
|
if ("id_token" in ret):
|
|
580
|
-
|
|
581
|
-
|
|
633
|
+
access_token : str|None = None
|
|
634
|
+
if "access_token" in ret:
|
|
635
|
+
access_token = ret["access_token"]
|
|
636
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
637
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
638
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
639
|
+
if (error1 is not None):
|
|
640
|
+
return {
|
|
641
|
+
"error": error1,
|
|
642
|
+
"error_description": error_description1
|
|
643
|
+
}
|
|
644
|
+
if ("id_payload" in user_info):
|
|
645
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
582
646
|
return ret
|
|
583
647
|
except Exception as e:
|
|
584
648
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -722,10 +786,22 @@ class OAuthClient:
|
|
|
722
786
|
"otp": otp,
|
|
723
787
|
"scope": scope,
|
|
724
788
|
}, self._auth_server_headers)
|
|
789
|
+
id_token : Mapping[str,Any]|None = None
|
|
725
790
|
if ("id_token" in otpResp):
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
791
|
+
access_token : str|None = None
|
|
792
|
+
if "access_token" in otpResp:
|
|
793
|
+
access_token = otpResp["access_token"]
|
|
794
|
+
user_info = await self._get_id_payload(otpResp["id_token"], access_token)
|
|
795
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
796
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
797
|
+
if (error1 is not None):
|
|
798
|
+
return {
|
|
799
|
+
"error": error1,
|
|
800
|
+
"error_description": error_description1
|
|
801
|
+
}
|
|
802
|
+
if ("id_payload" in user_info):
|
|
803
|
+
id_token = user_info["id_payload"]
|
|
804
|
+
ret = cast(OAuthTokenResponse, {
|
|
729
805
|
"id_token": otpResp.get("id_token"),
|
|
730
806
|
"access_token": otpResp.get("access_token"),
|
|
731
807
|
"refresh_token": otpResp.get("refresh_token"),
|
|
@@ -735,6 +811,9 @@ class OAuthClient:
|
|
|
735
811
|
"error": otpResp.get("error"),
|
|
736
812
|
"error_description": otpResp.get("error_description"),
|
|
737
813
|
})
|
|
814
|
+
if (id_token is not None):
|
|
815
|
+
ret["id_payload"] = id_token
|
|
816
|
+
return ret
|
|
738
817
|
|
|
739
818
|
async def mfa_oob_request(self, mfa_token: str, authenticator_id: str) -> OAuthMfaAuthenticatorsResponse:
|
|
740
819
|
"""
|
|
@@ -835,10 +914,22 @@ class OAuthClient:
|
|
|
835
914
|
"error": MapGetter[str].get(resp, "error", ""),
|
|
836
915
|
"error_description": MapGetter[str].get(resp, "error_description", ""),
|
|
837
916
|
}
|
|
917
|
+
id_token : Mapping[str,Any]|None = None
|
|
838
918
|
if ("id_token" in resp):
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
919
|
+
access_token : str|None = None
|
|
920
|
+
if "access_token" in resp:
|
|
921
|
+
access_token = resp["access_token"]
|
|
922
|
+
user_info = await self._get_id_payload(resp["id_token"], access_token)
|
|
923
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
924
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
925
|
+
if (error1 is not None):
|
|
926
|
+
return {
|
|
927
|
+
"error": error1,
|
|
928
|
+
"error_description": error_description1
|
|
929
|
+
}
|
|
930
|
+
if ("id_payload" in user_info):
|
|
931
|
+
id_token = user_info["id_payload"]
|
|
932
|
+
ret = cast(OAuthTokenResponse, {
|
|
842
933
|
"id_token": resp.get("id_token"),
|
|
843
934
|
"access_token": resp.get("access_token"),
|
|
844
935
|
"refresh_token": resp.get("refresh_token"),
|
|
@@ -846,6 +937,9 @@ class OAuthClient:
|
|
|
846
937
|
"scope": resp.get("scope"),
|
|
847
938
|
"token_type": resp.get("token_type"),
|
|
848
939
|
})
|
|
940
|
+
if (id_token is not None):
|
|
941
|
+
ret["id_payload"] = id_token
|
|
942
|
+
return ret
|
|
849
943
|
|
|
850
944
|
async def refresh_token_flow(self, refresh_token: str) -> OAuthTokenResponse:
|
|
851
945
|
"""
|
|
@@ -886,8 +980,19 @@ class OAuthClient:
|
|
|
886
980
|
try:
|
|
887
981
|
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
888
982
|
if ("id_token" in ret):
|
|
889
|
-
|
|
890
|
-
|
|
983
|
+
access_token : str|None = None
|
|
984
|
+
if "access_token" in ret:
|
|
985
|
+
access_token = ret["access_token"]
|
|
986
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
987
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
988
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
989
|
+
if (error1 is not None):
|
|
990
|
+
return {
|
|
991
|
+
"error": error1,
|
|
992
|
+
"error_description": error_description1
|
|
993
|
+
}
|
|
994
|
+
if ("id_payload" in user_info):
|
|
995
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
891
996
|
return ret
|
|
892
997
|
except Exception as e:
|
|
893
998
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -929,8 +1034,19 @@ class OAuthClient:
|
|
|
929
1034
|
try:
|
|
930
1035
|
ret = cast(OAuthDeviceAuthorizationResponse, await self._post(url, params, self._auth_server_headers))
|
|
931
1036
|
if ("id_token" in ret):
|
|
932
|
-
|
|
933
|
-
|
|
1037
|
+
access_token : str|None = None
|
|
1038
|
+
if "access_token" in ret:
|
|
1039
|
+
access_token = ret["access_token"]
|
|
1040
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
1041
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
1042
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
1043
|
+
if (error1 is not None):
|
|
1044
|
+
return {
|
|
1045
|
+
"error": error1,
|
|
1046
|
+
"error_description": error_description1
|
|
1047
|
+
}
|
|
1048
|
+
if ("id_payload" in user_info):
|
|
1049
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
934
1050
|
return ret
|
|
935
1051
|
except Exception as e:
|
|
936
1052
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -977,8 +1093,19 @@ class OAuthClient:
|
|
|
977
1093
|
resp = await self._post(self._oidc_config["token_endpoint"], params, self._auth_server_headers)
|
|
978
1094
|
ret = cast(OAuthDeviceResponse, resp)
|
|
979
1095
|
if ("id_token" in ret):
|
|
980
|
-
|
|
981
|
-
|
|
1096
|
+
access_token : str|None = None
|
|
1097
|
+
if "access_token" in ret:
|
|
1098
|
+
access_token = ret["access_token"]
|
|
1099
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
1100
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
1101
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
1102
|
+
if (error1 is not None):
|
|
1103
|
+
return {
|
|
1104
|
+
"error": error1,
|
|
1105
|
+
"error_description": error_description1
|
|
1106
|
+
}
|
|
1107
|
+
if ("id_payload" in user_info):
|
|
1108
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
982
1109
|
return ret
|
|
983
1110
|
except Exception as e:
|
|
984
1111
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -987,6 +1114,52 @@ class OAuthClient:
|
|
|
987
1114
|
"error_description": "Error connecting to authorization server"
|
|
988
1115
|
}
|
|
989
1116
|
|
|
1117
|
+
#################################################################3
|
|
1118
|
+
## UserInfo
|
|
1119
|
+
|
|
1120
|
+
async def user_info_endpoint(self, access_token : str) -> Mapping[str, Any]:
|
|
1121
|
+
if (not self._oidc_config or "token_endpoint" not in self._oidc_config):
|
|
1122
|
+
CrossauthLogger.logger().warn(j({"msg": "Not fetching user info as the endpoint is not defined in the OIDC Config"}))
|
|
1123
|
+
return {
|
|
1124
|
+
"error": "server_error",
|
|
1125
|
+
"error_description": "Cannot get token endpoint"
|
|
1126
|
+
}
|
|
1127
|
+
url = self._oidc_config["token_endpoint"]
|
|
1128
|
+
|
|
1129
|
+
resp = await self._post(url, {}, {"authorization": "Bearer " + access_token})
|
|
1130
|
+
return resp
|
|
1131
|
+
|
|
1132
|
+
async def _get_id_payload(self, id_token: str, access_token : str|None) -> IdTokenReturn:
|
|
1133
|
+
ret : IdTokenReturn = {}
|
|
1134
|
+
try:
|
|
1135
|
+
|
|
1136
|
+
payload = await self.validate_id_token(id_token)
|
|
1137
|
+
if (not payload):
|
|
1138
|
+
ret["error"] = "access_denied"
|
|
1139
|
+
ret["error_description"] = "Invalid ID token received"
|
|
1140
|
+
return ret
|
|
1141
|
+
ret["id_payload"] = payload
|
|
1142
|
+
if (access_token):
|
|
1143
|
+
if (self._oauth_use_user_info_endpoint):
|
|
1144
|
+
user_info = await self.user_info_endpoint(access_token)
|
|
1145
|
+
if ("error" in user_info):
|
|
1146
|
+
ret["error"] = user_info["error"]
|
|
1147
|
+
ret["error_description"] = "Failed getting user info: "
|
|
1148
|
+
if ("error_description" in user_info):
|
|
1149
|
+
ret["error_description"] += user_info["error_description"]
|
|
1150
|
+
else:
|
|
1151
|
+
ret["error_description"] += "Unknown error"
|
|
1152
|
+
payload = {**payload, **user_info}
|
|
1153
|
+
ret["id_payload"] = payload
|
|
1154
|
+
return ret
|
|
1155
|
+
except Exception as e:
|
|
1156
|
+
ce = CrossauthError.as_crossauth_error(e)
|
|
1157
|
+
CrossauthLogger.logger().debug(j({"err": ce}))
|
|
1158
|
+
CrossauthLogger.logger().error(j({"msg": "Couldn't get user info", "cerr": ce}));
|
|
1159
|
+
ret["error"] = ce.oauthErrorCode
|
|
1160
|
+
ret["error_description"] = "Couldn't get user info: " + ce.message
|
|
1161
|
+
return ret
|
|
1162
|
+
|
|
990
1163
|
async def _post(self, url: str, params: Mapping[str, Any], headers: Dict[str, Any] = {}) -> Mapping[str, Any]:
|
|
991
1164
|
CrossauthLogger.logger().debug(j({
|
|
992
1165
|
"msg": "Fetch POST",
|
|
@@ -999,12 +1172,21 @@ class OAuthClient:
|
|
|
999
1172
|
if self._auth_server_mode:
|
|
1000
1173
|
options["mode"] = self._auth_server_mode
|
|
1001
1174
|
async with aiohttp.ClientSession() as session:
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1175
|
+
if (self._oauth_post_type == "json"):
|
|
1176
|
+
resp = await session.post(url, json=params, headers={
|
|
1177
|
+
'Accept': 'application/json',
|
|
1178
|
+
'Content-Type': 'application/json',
|
|
1179
|
+
**headers,
|
|
1180
|
+
})
|
|
1181
|
+
return await resp.json()
|
|
1182
|
+
else:
|
|
1183
|
+
resp = await session.post(url, data=params, headers={
|
|
1184
|
+
'Accept': 'application/json',
|
|
1185
|
+
'Content-Type': 'application/x-www-form-urlencodedc',
|
|
1186
|
+
**headers,
|
|
1187
|
+
})
|
|
1188
|
+
return await resp.json()
|
|
1189
|
+
|
|
1008
1190
|
|
|
1009
1191
|
async def _get(self, url: str, headers: Mapping[str, Any] = {}) -> Mapping[str, Any] | List[Any]:
|
|
1010
1192
|
CrossauthLogger.logger().debug(j({"msg": "Fetch GET", "url": url}))
|
|
@@ -9,7 +9,7 @@ from .fastapiresserver import FastApiOAuthResourceServer, FastApiOAuthResourceSe
|
|
|
9
9
|
from .fastapiserver import FastApiServer, FastApiErrorFn
|
|
10
10
|
|
|
11
11
|
# Version of realpython-reader package
|
|
12
|
-
__version__ = "0.0.
|
|
12
|
+
__version__ = "0.0.3"
|
|
13
13
|
|
|
14
14
|
__all__ = (
|
|
15
15
|
"FastApiSessionServerOptions", "FastApiSessionServer",
|
|
@@ -713,7 +713,10 @@ class FastApiOAuthClient(OAuthClient):
|
|
|
713
713
|
"ip": request.client.host if request.client else None,
|
|
714
714
|
"user": FastApiSessionServer.username(request)
|
|
715
715
|
}))
|
|
716
|
-
|
|
716
|
+
state = self.random_value(self._state_length)
|
|
717
|
+
session_data = {"scope": request.query_params.get("scope"), "state": state}
|
|
718
|
+
await self.store_session_data(session_data, request, response)
|
|
719
|
+
ret = await self.start_authorization_code_flow(state, request.query_params.get("scope"))
|
|
717
720
|
if "error" in ret or "url" not in ret:
|
|
718
721
|
ce = CrossauthError.from_oauth_error(ret["error"] or "server_error", ret["error_description"])
|
|
719
722
|
return await self.__error_fn(self.server, request, response, ce)
|
|
@@ -736,7 +739,14 @@ class FastApiOAuthClient(OAuthClient):
|
|
|
736
739
|
"ip": request.client.host if request.client is not None else None,
|
|
737
740
|
"user": FastApiSessionServer.username(request)
|
|
738
741
|
}))
|
|
739
|
-
|
|
742
|
+
state = self.random_value(self._state_length)
|
|
743
|
+
challengeVerifier = await self.code_challenge_and_verifier()
|
|
744
|
+
session_data = {"scope": request.query_params.get("scope"),
|
|
745
|
+
"state": state,
|
|
746
|
+
"code_challenge": challengeVerifier["code_challenge"],
|
|
747
|
+
"code_verifier": challengeVerifier["code_verifier"]}
|
|
748
|
+
await self.store_session_data(session_data, request, response)
|
|
749
|
+
ret = await self.start_authorization_code_flow(state, request.query_params.get("scope"), challengeVerifier["code_challenge"], True)
|
|
740
750
|
if "error" in ret or "url" not in ret:
|
|
741
751
|
ce = CrossauthError.from_oauth_error(ret["error"] or "server_error", ret["error_description"])
|
|
742
752
|
return await self.__error_fn(self.server, request, response, ce)
|
|
@@ -756,7 +766,14 @@ class FastApiOAuthClient(OAuthClient):
|
|
|
756
766
|
"ip": request.client.host if request.client else None,
|
|
757
767
|
"user": FastApiSessionServer.username(request)
|
|
758
768
|
}))
|
|
759
|
-
|
|
769
|
+
|
|
770
|
+
oauth_data = await self.server.get_session_data(request, self.session_data_name)
|
|
771
|
+
if (oauth_data is None or "state" not in oauth_data or oauth_data["state"] != request.query_params.get("state")):
|
|
772
|
+
raise CrossauthError(ErrorCode.Unauthorized, "State does not match")
|
|
773
|
+
verifier : str|None = None
|
|
774
|
+
if (oauth_data and "code_verifier" in oauth_data):
|
|
775
|
+
verifier = oauth_data["code_verifier"]
|
|
776
|
+
resp = await self.redirect_endpoint(request.query_params.get("code"), request.query_params.get("state"), verifier, request.query_params.get("error"), request.query_params.get("error_description"))
|
|
760
777
|
try:
|
|
761
778
|
if ("id_token" in resp):
|
|
762
779
|
# This token is intended for us, so validate it
|
|
@@ -1909,6 +1926,20 @@ class FastApiOAuthClient(OAuthClient):
|
|
|
1909
1926
|
CrossauthLogger.logger().debug(json.dumps({"err": str(e)}))
|
|
1910
1927
|
return await self.__error_fn(self.server, request, response, ce)
|
|
1911
1928
|
|
|
1929
|
+
async def store_session_data(self, session_data : Dict[str, Any], request: Request, response: Response|None):
|
|
1930
|
+
if self.server.have_session_server:
|
|
1931
|
+
session_cookie_value = self.server.get_session_cookie_value(request)
|
|
1932
|
+
if not session_cookie_value and response is not None:
|
|
1933
|
+
session_cookie_value = await self.server.create_anonymous_session(request, response, {
|
|
1934
|
+
self.session_data_name: session_data
|
|
1935
|
+
})
|
|
1936
|
+
else:
|
|
1937
|
+
await self.server.update_session_data(request, self.session_data_name, session_data)
|
|
1938
|
+
else:
|
|
1939
|
+
if not self.server.have_session_adapter:
|
|
1940
|
+
raise CrossauthError(ErrorCode.Configuration, "Cannot get session data if sessions not enabled")
|
|
1941
|
+
await self.server.update_session_data(request, self.session_data_name, session_data)
|
|
1942
|
+
|
|
1912
1943
|
|
|
1913
1944
|
##############################################################
|
|
1914
1945
|
## Default functions
|
|
@@ -1976,6 +2007,8 @@ def log_tokens(oauth_response: OAuthTokenResponse|OAuthDeviceResponse, client: F
|
|
|
1976
2007
|
try:
|
|
1977
2008
|
jwt = instance.decode(oauth_response["access_token"], None, do_verify=False, do_time_check=False)
|
|
1978
2009
|
jti : str|None = jwt.get("jti")
|
|
2010
|
+
if (jti == None):
|
|
2011
|
+
jti = jwt.get("sid")
|
|
1979
2012
|
hash_value = Crypto.hash(jti) if jti else None
|
|
1980
2013
|
CrossauthLogger.logger().debug(j({"msg": "Got access token", "accessTokenHash": hash_value}))
|
|
1981
2014
|
except Exception as e:
|
|
@@ -1985,6 +2018,8 @@ def log_tokens(oauth_response: OAuthTokenResponse|OAuthDeviceResponse, client: F
|
|
|
1985
2018
|
try:
|
|
1986
2019
|
jwt = instance.decode(oauth_response["id_token"], None, do_verify=False, do_time_check=False)
|
|
1987
2020
|
jti : str|None = jwt.get("jti")
|
|
2021
|
+
if (jti == None):
|
|
2022
|
+
jti = jwt.get("sid")
|
|
1988
2023
|
hash_value = Crypto.hash(jti) if jti else None
|
|
1989
2024
|
CrossauthLogger.logger().debug(j({"msg": "Got id token", "idTokenHash": hash_value}))
|
|
1990
2025
|
except Exception as e:
|
|
@@ -1994,6 +2029,8 @@ def log_tokens(oauth_response: OAuthTokenResponse|OAuthDeviceResponse, client: F
|
|
|
1994
2029
|
try:
|
|
1995
2030
|
jwt = instance.decode(oauth_response["refresh_token"], None, do_verify=False, do_time_check=False)
|
|
1996
2031
|
jti : str|None = jwt.get("jti")
|
|
2032
|
+
if (jti == None):
|
|
2033
|
+
jti = jwt.get("sid")
|
|
1997
2034
|
hash_value = Crypto.hash(jti) if jti else None
|
|
1998
2035
|
CrossauthLogger.logger().debug(j({"msg": "Got refresh token", "refreshTokenHash": hash_value}))
|
|
1999
2036
|
except Exception as e:
|
|
@@ -2071,6 +2108,7 @@ async def update_session_data(oauth_response: OAuthTokenResponse,
|
|
|
2071
2108
|
id_payload = decode_payload(oauth_response["id_token"])
|
|
2072
2109
|
session_data["id_payload"] = id_payload
|
|
2073
2110
|
|
|
2111
|
+
await client.store_session_data(session_data, request, response)
|
|
2074
2112
|
if client.server.have_session_server:
|
|
2075
2113
|
session_cookie_value = client.server.get_session_cookie_value(request)
|
|
2076
2114
|
if not session_cookie_value and response is not None:
|
|
@@ -2183,3 +2221,4 @@ async def save_in_session_and_redirect(oauth_response: OAuthTokenResponse|OAuthD
|
|
|
2183
2221
|
"error_messages": ce.messages,
|
|
2184
2222
|
"error_code_name": ce.code_name
|
|
2185
2223
|
}, status_code=ce.http_status, headers=response.headers)
|
|
2224
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/inmemorystorage.py
RENAMED
|
File without changes
|
{pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/sqlalchemystorage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|