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.
Files changed (40) hide show
  1. {pycrossauth-0.0.2/src/pycrossauth.egg-info → pycrossauth-0.0.4}/PKG-INFO +1 -1
  2. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/pyproject.toml +2 -2
  3. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/__init__.py +1 -1
  4. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/client.py +218 -36
  5. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/__init__.py +1 -1
  6. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapioauthclient.py +42 -3
  7. {pycrossauth-0.0.2 → pycrossauth-0.0.4/src/pycrossauth.egg-info}/PKG-INFO +1 -1
  8. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/LICENSE +0 -0
  9. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/NOTICE +0 -0
  10. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/README.md +0 -0
  11. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/setup.cfg +0 -0
  12. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/setup.py +0 -0
  13. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/auth.py +0 -0
  14. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/__init__.py +0 -0
  15. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/error.py +0 -0
  16. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/interfaces.py +0 -0
  17. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/jwt.py +0 -0
  18. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/common/logger.py +0 -0
  19. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/cookieauth.py +0 -0
  20. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/crypto.py +0 -0
  21. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/__init__.py +0 -0
  22. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/clientmanager.py +0 -0
  23. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/resserver.py +0 -0
  24. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/tokenconsumer.py +0 -0
  25. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/oauth/wellknown.py +0 -0
  26. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/session.py +0 -0
  27. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storage.py +0 -0
  28. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/__init__.py +0 -0
  29. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/inmemorystorage.py +0 -0
  30. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/storageimpl/sqlalchemystorage.py +0 -0
  31. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_backend/utils.py +0 -0
  32. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapiresserver.py +0 -0
  33. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapiserver.py +0 -0
  34. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapiserverbase.py +0 -0
  35. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapisession.py +0 -0
  36. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/crossauth_fastapi/fastapisessionadapter.py +0 -0
  37. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/pycrossauth.egg-info/SOURCES.txt +0 -0
  38. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/pycrossauth.egg-info/dependency_links.txt +0 -0
  39. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/src/pycrossauth.egg-info/requires.txt +0 -0
  40. {pycrossauth-0.0.2 → pycrossauth-0.0.4}/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.4
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.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.2"
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.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,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 start_authorization_code_flow(self, scope : str | None = None, pkce : bool = False):
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(self.state)}&redirect_uri={urllib.parse.quote(self._redirect_uri)}"
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
- 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}"
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"] = self._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
- if (await self.validate_id_token(ret["id_token"]) is None):
482
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- return cast(OAuthTokenResponse, await self._post(url, params, self._auth_server_headers))
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
- if (await self.validate_id_token(ret["id_token"]) is None):
581
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- if (await self.validate_id_token(otpResp["id_token"]) is None):
727
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
728
- return cast(OAuthTokenResponse, {
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
- if (await self.validate_id_token(resp["id_token"]) is None):
840
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
841
- return cast(OAuthTokenResponse, {
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
- if (await self.validate_id_token(ret["id_token"]) is None):
890
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- if (await self.validate_id_token(ret["id_token"]) is None):
933
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- if (await self.validate_id_token(ret["id_token"]) is None):
981
- raise CrossauthError(ErrorCode.InvalidToken, "Invalid ID token")
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
- 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()
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.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.4
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