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.
Files changed (40) hide show
  1. {pycrossauth-0.0.2/src/pycrossauth.egg-info → pycrossauth-0.0.3}/PKG-INFO +1 -1
  2. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/pyproject.toml +2 -2
  3. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/__init__.py +1 -1
  4. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/client.py +215 -36
  5. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/__init__.py +1 -1
  6. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapioauthclient.py +42 -3
  7. {pycrossauth-0.0.2 → pycrossauth-0.0.3/src/pycrossauth.egg-info}/PKG-INFO +1 -1
  8. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/LICENSE +0 -0
  9. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/NOTICE +0 -0
  10. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/README.md +0 -0
  11. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/setup.cfg +0 -0
  12. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/setup.py +0 -0
  13. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/auth.py +0 -0
  14. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/__init__.py +0 -0
  15. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/error.py +0 -0
  16. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/interfaces.py +0 -0
  17. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/jwt.py +0 -0
  18. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/common/logger.py +0 -0
  19. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/cookieauth.py +0 -0
  20. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/crypto.py +0 -0
  21. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/__init__.py +0 -0
  22. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/clientmanager.py +0 -0
  23. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/resserver.py +0 -0
  24. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/tokenconsumer.py +0 -0
  25. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/oauth/wellknown.py +0 -0
  26. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/session.py +0 -0
  27. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storage.py +0 -0
  28. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storageimpl/__init__.py +0 -0
  29. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storageimpl/inmemorystorage.py +0 -0
  30. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/storageimpl/sqlalchemystorage.py +0 -0
  31. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_backend/utils.py +0 -0
  32. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapiresserver.py +0 -0
  33. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapiserver.py +0 -0
  34. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapiserverbase.py +0 -0
  35. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapisession.py +0 -0
  36. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/crossauth_fastapi/fastapisessionadapter.py +0 -0
  37. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/pycrossauth.egg-info/SOURCES.txt +0 -0
  38. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/pycrossauth.egg-info/dependency_links.txt +0 -0
  39. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/pycrossauth.egg-info/requires.txt +0 -0
  40. {pycrossauth-0.0.2 → pycrossauth-0.0.3}/src/pycrossauth.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycrossauth
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Cross platform web authentication and OAuth framework
5
5
  Author-email: Matthew Baker <matt@mattbaker.ch>
6
6
  License:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pycrossauth"
7
- version = "0.0.2"
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.2"
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.2"
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 start_authorization_code_flow(self, scope : str | None = None, pkce : bool = False):
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(self.state)}&redirect_uri={urllib.parse.quote(self._redirect_uri)}"
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
- self._code_verifier = self.random_value(self._verifier_length)
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"] = self._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
- if (await self.validate_id_token(ret["id_token"]) is None):
482
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- return cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
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
- if (await self.validate_id_token(ret["id_token"]) is None):
581
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- if (await self.validate_id_token(otpResp["id_token"]) is None):
727
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
728
- return cast(OAuthTokenResponse, {
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
- if (await self.validate_id_token(resp["id_token"]) is None):
840
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
841
- return cast(OAuthTokenResponse, {
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
- if (await self.validate_id_token(ret["id_token"]) is None):
890
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- if (await self.validate_id_token(ret["id_token"]) is None):
933
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- if (await self.validate_id_token(ret["id_token"]) is None):
981
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- resp = await session.post(url, json=params, headers={
1003
- 'Accept': 'application/json',
1004
- 'Content-Type': 'application/json',
1005
- **headers,
1006
- })
1007
- return await resp.json()
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.2"
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
- ret = await self.start_authorization_code_flow(request.query_params.get("scope"))
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
- ret = await self.start_authorization_code_flow(request.query_params.get("scope"), True)
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
- resp = await self.redirect_endpoint(request.query_params.get("code"), request.query_params.get("state"), request.query_params.get("error"), request.query_params.get("error_description"))
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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycrossauth
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Cross platform web authentication and OAuth framework
5
5
  Author-email: Matthew Baker <matt@mattbaker.ch>
6
6
  License:
File without changes
File without changes
File without changes
File without changes
File without changes