pycrossauth 0.0.2__tar.gz → 0.0.3__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.3}/PKG-INFO +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/pyproject.toml +2 -2
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/__init__.py +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/client.py +215 -36
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/__init__.py +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapioauthclient.py +42 -3
- {pycrossauth-0.0.2 → pycrossauth-0.0.3/src/pycrossauth.egg-info}/PKG-INFO +1 -1
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/LICENSE +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/NOTICE +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/README.md +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/setup.cfg +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/setup.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/auth.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/__init__.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/error.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/interfaces.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/jwt.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/logger.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/cookieauth.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/crypto.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/__init__.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/clientmanager.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/resserver.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/tokenconsumer.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/wellknown.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/session.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storage.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storageimpl/__init__.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storageimpl/inmemorystorage.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storageimpl/sqlalchemystorage.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/utils.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapiresserver.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapiserver.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapiserverbase.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapisession.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapisessionadapter.py +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/pycrossauth.egg-info/SOURCES.txt +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/pycrossauth.egg-info/dependency_links.txt +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/pycrossauth.egg-info/requires.txt +0 -0
- {pycrossauth-0.0.2 → pycrossauth-0.0.3}/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.3"
|
|
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.3"
|
|
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,11 @@ 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
|
|
296
313
|
|
|
297
314
|
self.auth_server_base_url = auth_server_base_url
|
|
298
315
|
set_parameter("client_id", ParamType.String, self, options, "OAUTH_CLIENT_ID", required=True, protected=True)
|
|
@@ -309,6 +326,11 @@ class OAuthClient:
|
|
|
309
326
|
set_parameter("auth_server_mode", ParamType.String, self, options, "OAUTH_AUTH_SERVER_MODE", protected=True)
|
|
310
327
|
set_parameter("auth_server_headers", ParamType.Json, self, options, "OAUTH_AUTH_SERVER_HEADERS", protected=True)
|
|
311
328
|
if (self._device_authorization_url[0:1] == "/"): self._device_authorization_url = self._device_authorization_url[1:]
|
|
329
|
+
set_parameter("oauth_post_type", ParamType.Json, self, options, "OAUTH_POST_TYPE", protected=True)
|
|
330
|
+
if (self._oauth_post_type != "json" and self._oauth_post_type != "form"):
|
|
331
|
+
raise CrossauthError(ErrorCode.Configuration, "oauth_post_type must be json or form")
|
|
332
|
+
set_parameter("oauth_use_user_info_endpoint", ParamType.Json, self, options, "OAUTH_USE_USER_INFO_ENDPOINT", protected=True)
|
|
333
|
+
|
|
312
334
|
|
|
313
335
|
async def load_config(self, oidc_config : OpenIdConfiguration|None=None):
|
|
314
336
|
"""
|
|
@@ -378,7 +400,13 @@ class OAuthClient:
|
|
|
378
400
|
|
|
379
401
|
return Crypto.sha256(plaintext)
|
|
380
402
|
|
|
381
|
-
async def
|
|
403
|
+
async def code_challenge_and_verifier(self):
|
|
404
|
+
code_verifier = self.random_value(self._verifier_length)
|
|
405
|
+
code_challenge = await self.sha256(code_verifier) if self._code_challenge_method == "S256" else code_verifier
|
|
406
|
+
return {"code_verifier": code_verifier, "code_challenge": code_challenge}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
async def start_authorization_code_flow(self, state: str, scope : str | None = None, code_challenge: str|None = None, pkce : bool = False):
|
|
382
410
|
"""
|
|
383
411
|
Initiates the authorization code flow
|
|
384
412
|
|
|
@@ -402,7 +430,6 @@ class OAuthClient:
|
|
|
402
430
|
"error": "server_error",
|
|
403
431
|
"error_description": "Cannot get authorize endpoint"
|
|
404
432
|
}
|
|
405
|
-
self.state = self.random_value(self._state_length)
|
|
406
433
|
if not self._client_id:
|
|
407
434
|
return {
|
|
408
435
|
"error": "invalid_request",
|
|
@@ -415,19 +442,17 @@ class OAuthClient:
|
|
|
415
442
|
}
|
|
416
443
|
|
|
417
444
|
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(
|
|
445
|
+
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
446
|
|
|
420
447
|
if scope:
|
|
421
448
|
url += f"&scope={urllib.parse.quote(scope)}"
|
|
422
449
|
|
|
423
450
|
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}"
|
|
451
|
+
url += f"&code_challenge={code_challenge}"
|
|
427
452
|
|
|
428
453
|
return {"url": url}
|
|
429
454
|
|
|
430
|
-
async def redirect_endpoint(self, code : str|None = None, state : str|None = None, error : str|None =None, error_description : str|None=None) -> OAuthTokenResponse:
|
|
455
|
+
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
456
|
"""
|
|
432
457
|
For calling in a Redirect Uri endpoint
|
|
433
458
|
|
|
@@ -449,8 +474,6 @@ class OAuthClient:
|
|
|
449
474
|
if error_description is None:
|
|
450
475
|
error_description = "Unknown error"
|
|
451
476
|
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
477
|
self.authzCode = code
|
|
455
478
|
|
|
456
479
|
if "authorization_code" not in self._oidc_config["grant_types_supported"]:
|
|
@@ -474,12 +497,24 @@ class OAuthClient:
|
|
|
474
497
|
}
|
|
475
498
|
if client_secret:
|
|
476
499
|
params["client_secret"] = client_secret
|
|
477
|
-
params["code_verifier"] =
|
|
500
|
+
params["code_verifier"] = code_verifier
|
|
478
501
|
try:
|
|
479
502
|
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
480
503
|
if ("id_token" in ret):
|
|
481
|
-
|
|
482
|
-
|
|
504
|
+
access_token : str|None = None
|
|
505
|
+
if "access_token" in ret:
|
|
506
|
+
access_token = ret["access_token"]
|
|
507
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
508
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
509
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
510
|
+
if (error1 is not None):
|
|
511
|
+
return {
|
|
512
|
+
"error": error1,
|
|
513
|
+
"error_description": error_description1
|
|
514
|
+
}
|
|
515
|
+
if ("id_payload" in user_info):
|
|
516
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
517
|
+
|
|
483
518
|
return ret
|
|
484
519
|
except Exception as e:
|
|
485
520
|
CrossauthLogger.logger().error(j({"cerr": e}))
|
|
@@ -526,7 +561,22 @@ class OAuthClient:
|
|
|
526
561
|
if scope:
|
|
527
562
|
params["scope"] = scope
|
|
528
563
|
try:
|
|
529
|
-
|
|
564
|
+
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
565
|
+
if ("id_token" in ret):
|
|
566
|
+
access_token : str|None = None
|
|
567
|
+
if "access_token" in ret:
|
|
568
|
+
access_token = ret["access_token"]
|
|
569
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
570
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
571
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
572
|
+
if (error1 is not None):
|
|
573
|
+
return {
|
|
574
|
+
"error": error1,
|
|
575
|
+
"error_description": error_description1
|
|
576
|
+
}
|
|
577
|
+
if ("id_payload" in user_info):
|
|
578
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
579
|
+
return ret
|
|
530
580
|
except Exception as e:
|
|
531
581
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
532
582
|
return {
|
|
@@ -577,8 +627,19 @@ class OAuthClient:
|
|
|
577
627
|
try:
|
|
578
628
|
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
579
629
|
if ("id_token" in ret):
|
|
580
|
-
|
|
581
|
-
|
|
630
|
+
access_token : str|None = None
|
|
631
|
+
if "access_token" in ret:
|
|
632
|
+
access_token = ret["access_token"]
|
|
633
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
634
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
635
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
636
|
+
if (error1 is not None):
|
|
637
|
+
return {
|
|
638
|
+
"error": error1,
|
|
639
|
+
"error_description": error_description1
|
|
640
|
+
}
|
|
641
|
+
if ("id_payload" in user_info):
|
|
642
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
582
643
|
return ret
|
|
583
644
|
except Exception as e:
|
|
584
645
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -722,10 +783,22 @@ class OAuthClient:
|
|
|
722
783
|
"otp": otp,
|
|
723
784
|
"scope": scope,
|
|
724
785
|
}, self._auth_server_headers)
|
|
786
|
+
id_token : Mapping[str,Any]|None = None
|
|
725
787
|
if ("id_token" in otpResp):
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
788
|
+
access_token : str|None = None
|
|
789
|
+
if "access_token" in otpResp:
|
|
790
|
+
access_token = otpResp["access_token"]
|
|
791
|
+
user_info = await self._get_id_payload(otpResp["id_token"], access_token)
|
|
792
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
793
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
794
|
+
if (error1 is not None):
|
|
795
|
+
return {
|
|
796
|
+
"error": error1,
|
|
797
|
+
"error_description": error_description1
|
|
798
|
+
}
|
|
799
|
+
if ("id_payload" in user_info):
|
|
800
|
+
id_token = user_info["id_payload"]
|
|
801
|
+
ret = cast(OAuthTokenResponse, {
|
|
729
802
|
"id_token": otpResp.get("id_token"),
|
|
730
803
|
"access_token": otpResp.get("access_token"),
|
|
731
804
|
"refresh_token": otpResp.get("refresh_token"),
|
|
@@ -735,6 +808,9 @@ class OAuthClient:
|
|
|
735
808
|
"error": otpResp.get("error"),
|
|
736
809
|
"error_description": otpResp.get("error_description"),
|
|
737
810
|
})
|
|
811
|
+
if (id_token is not None):
|
|
812
|
+
ret["id_payload"] = id_token
|
|
813
|
+
return ret
|
|
738
814
|
|
|
739
815
|
async def mfa_oob_request(self, mfa_token: str, authenticator_id: str) -> OAuthMfaAuthenticatorsResponse:
|
|
740
816
|
"""
|
|
@@ -835,10 +911,22 @@ class OAuthClient:
|
|
|
835
911
|
"error": MapGetter[str].get(resp, "error", ""),
|
|
836
912
|
"error_description": MapGetter[str].get(resp, "error_description", ""),
|
|
837
913
|
}
|
|
914
|
+
id_token : Mapping[str,Any]|None = None
|
|
838
915
|
if ("id_token" in resp):
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
916
|
+
access_token : str|None = None
|
|
917
|
+
if "access_token" in resp:
|
|
918
|
+
access_token = resp["access_token"]
|
|
919
|
+
user_info = await self._get_id_payload(resp["id_token"], access_token)
|
|
920
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
921
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
922
|
+
if (error1 is not None):
|
|
923
|
+
return {
|
|
924
|
+
"error": error1,
|
|
925
|
+
"error_description": error_description1
|
|
926
|
+
}
|
|
927
|
+
if ("id_payload" in user_info):
|
|
928
|
+
id_token = user_info["id_payload"]
|
|
929
|
+
ret = cast(OAuthTokenResponse, {
|
|
842
930
|
"id_token": resp.get("id_token"),
|
|
843
931
|
"access_token": resp.get("access_token"),
|
|
844
932
|
"refresh_token": resp.get("refresh_token"),
|
|
@@ -846,6 +934,9 @@ class OAuthClient:
|
|
|
846
934
|
"scope": resp.get("scope"),
|
|
847
935
|
"token_type": resp.get("token_type"),
|
|
848
936
|
})
|
|
937
|
+
if (id_token is not None):
|
|
938
|
+
ret["id_payload"] = id_token
|
|
939
|
+
return ret
|
|
849
940
|
|
|
850
941
|
async def refresh_token_flow(self, refresh_token: str) -> OAuthTokenResponse:
|
|
851
942
|
"""
|
|
@@ -886,8 +977,19 @@ class OAuthClient:
|
|
|
886
977
|
try:
|
|
887
978
|
ret = cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
|
|
888
979
|
if ("id_token" in ret):
|
|
889
|
-
|
|
890
|
-
|
|
980
|
+
access_token : str|None = None
|
|
981
|
+
if "access_token" in ret:
|
|
982
|
+
access_token = ret["access_token"]
|
|
983
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
984
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
985
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
986
|
+
if (error1 is not None):
|
|
987
|
+
return {
|
|
988
|
+
"error": error1,
|
|
989
|
+
"error_description": error_description1
|
|
990
|
+
}
|
|
991
|
+
if ("id_payload" in user_info):
|
|
992
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
891
993
|
return ret
|
|
892
994
|
except Exception as e:
|
|
893
995
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -929,8 +1031,19 @@ class OAuthClient:
|
|
|
929
1031
|
try:
|
|
930
1032
|
ret = cast(OAuthDeviceAuthorizationResponse, await self._post(url, params, self._auth_server_headers))
|
|
931
1033
|
if ("id_token" in ret):
|
|
932
|
-
|
|
933
|
-
|
|
1034
|
+
access_token : str|None = None
|
|
1035
|
+
if "access_token" in ret:
|
|
1036
|
+
access_token = ret["access_token"]
|
|
1037
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
1038
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
1039
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
1040
|
+
if (error1 is not None):
|
|
1041
|
+
return {
|
|
1042
|
+
"error": error1,
|
|
1043
|
+
"error_description": error_description1
|
|
1044
|
+
}
|
|
1045
|
+
if ("id_payload" in user_info):
|
|
1046
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
934
1047
|
return ret
|
|
935
1048
|
except Exception as e:
|
|
936
1049
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -977,8 +1090,19 @@ class OAuthClient:
|
|
|
977
1090
|
resp = await self._post(self._oidc_config["token_endpoint"], params, self._auth_server_headers)
|
|
978
1091
|
ret = cast(OAuthDeviceResponse, resp)
|
|
979
1092
|
if ("id_token" in ret):
|
|
980
|
-
|
|
981
|
-
|
|
1093
|
+
access_token : str|None = None
|
|
1094
|
+
if "access_token" in ret:
|
|
1095
|
+
access_token = ret["access_token"]
|
|
1096
|
+
user_info = await self._get_id_payload(ret["id_token"], access_token)
|
|
1097
|
+
error1 : str|None = user_info["error"] if "error" in user_info else None
|
|
1098
|
+
error_description1 : str|None = user_info["error_description"] if "error_description" in user_info else ""
|
|
1099
|
+
if (error1 is not None):
|
|
1100
|
+
return {
|
|
1101
|
+
"error": error1,
|
|
1102
|
+
"error_description": error_description1
|
|
1103
|
+
}
|
|
1104
|
+
if ("id_payload" in user_info):
|
|
1105
|
+
ret["id_payload"] = user_info["id_payload"]
|
|
982
1106
|
return ret
|
|
983
1107
|
except Exception as e:
|
|
984
1108
|
CrossauthLogger.logger().error(j({"err": str(e)}))
|
|
@@ -987,6 +1111,52 @@ class OAuthClient:
|
|
|
987
1111
|
"error_description": "Error connecting to authorization server"
|
|
988
1112
|
}
|
|
989
1113
|
|
|
1114
|
+
#################################################################3
|
|
1115
|
+
## UserInfo
|
|
1116
|
+
|
|
1117
|
+
async def user_info_endpoint(self, access_token : str) -> Mapping[str, Any]:
|
|
1118
|
+
if (not self._oidc_config or "token_endpoint" not in self._oidc_config):
|
|
1119
|
+
CrossauthLogger.logger().warn(j({"msg": "Not fetching user info as the endpoint is not defined in the OIDC Config"}))
|
|
1120
|
+
return {
|
|
1121
|
+
"error": "server_error",
|
|
1122
|
+
"error_description": "Cannot get token endpoint"
|
|
1123
|
+
}
|
|
1124
|
+
url = self._oidc_config["token_endpoint"]
|
|
1125
|
+
|
|
1126
|
+
resp = await self._post(url, {}, {"authorization": "Bearer " + access_token})
|
|
1127
|
+
return resp
|
|
1128
|
+
|
|
1129
|
+
async def _get_id_payload(self, id_token: str, access_token : str|None) -> IdTokenReturn:
|
|
1130
|
+
ret : IdTokenReturn = {}
|
|
1131
|
+
try:
|
|
1132
|
+
|
|
1133
|
+
payload = await self.validate_id_token(id_token)
|
|
1134
|
+
if (not payload):
|
|
1135
|
+
ret["error"] = "access_denied"
|
|
1136
|
+
ret["error_description"] = "Invalid ID token received"
|
|
1137
|
+
return ret
|
|
1138
|
+
ret["id_payload"] = payload
|
|
1139
|
+
if (access_token):
|
|
1140
|
+
if (self._oauth_use_user_info_endpoint):
|
|
1141
|
+
user_info = await self.user_info_endpoint(access_token)
|
|
1142
|
+
if ("error" in user_info):
|
|
1143
|
+
ret["error"] = user_info["error"]
|
|
1144
|
+
ret["error_description"] = "Failed getting user info: "
|
|
1145
|
+
if ("error_description" in user_info):
|
|
1146
|
+
ret["error_description"] += user_info["error_description"]
|
|
1147
|
+
else:
|
|
1148
|
+
ret["error_description"] += "Unknown error"
|
|
1149
|
+
payload = {**payload, **user_info}
|
|
1150
|
+
ret["id_payload"] = payload
|
|
1151
|
+
return ret
|
|
1152
|
+
except Exception as e:
|
|
1153
|
+
ce = CrossauthError.as_crossauth_error(e)
|
|
1154
|
+
CrossauthLogger.logger().debug(j({"err": ce}))
|
|
1155
|
+
CrossauthLogger.logger().error(j({"msg": "Couldn't get user info", "cerr": ce}));
|
|
1156
|
+
ret["error"] = ce.oauthErrorCode
|
|
1157
|
+
ret["error_description"] = "Couldn't get user info: " + ce.message
|
|
1158
|
+
return ret
|
|
1159
|
+
|
|
990
1160
|
async def _post(self, url: str, params: Mapping[str, Any], headers: Dict[str, Any] = {}) -> Mapping[str, Any]:
|
|
991
1161
|
CrossauthLogger.logger().debug(j({
|
|
992
1162
|
"msg": "Fetch POST",
|
|
@@ -999,12 +1169,21 @@ class OAuthClient:
|
|
|
999
1169
|
if self._auth_server_mode:
|
|
1000
1170
|
options["mode"] = self._auth_server_mode
|
|
1001
1171
|
async with aiohttp.ClientSession() as session:
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1172
|
+
if (self._oauth_post_type == "json"):
|
|
1173
|
+
resp = await session.post(url, json=params, headers={
|
|
1174
|
+
'Accept': 'application/json',
|
|
1175
|
+
'Content-Type': 'application/json',
|
|
1176
|
+
**headers,
|
|
1177
|
+
})
|
|
1178
|
+
return await resp.json()
|
|
1179
|
+
else:
|
|
1180
|
+
resp = await session.post(url, data=params, headers={
|
|
1181
|
+
'Accept': 'application/json',
|
|
1182
|
+
'Content-Type': 'application/x-www-form-urlencodedc',
|
|
1183
|
+
**headers,
|
|
1184
|
+
})
|
|
1185
|
+
return await resp.json()
|
|
1186
|
+
|
|
1008
1187
|
|
|
1009
1188
|
async def _get(self, url: str, headers: Mapping[str, Any] = {}) -> Mapping[str, Any] | List[Any]:
|
|
1010
1189
|
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.3}/src/crossauth_backend/storageimpl/inmemorystorage.py
RENAMED
|
File without changes
|
{pycrossauth-0.0.2 → pycrossauth-0.0.3}/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
|