Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 90% coverage #284

Draft
wants to merge 47 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
0b745fe
fix: variable name for more redability
PascalDR Oct 22, 2024
c3946d1
Merge branch 'dev' of https://github.com/italia/eudi-wallet-it-satosa…
PascalDR Oct 22, 2024
5f63d42
fix: use already existent functions for unsafe parse
PascalDR Oct 22, 2024
7ea2ace
fix: wrong key
PascalDR Oct 22, 2024
97c9dd1
feat: decode_jwt_element refactoring
PascalDR Oct 22, 2024
cbc1297
chore: removed unused functions
PascalDR Oct 22, 2024
a0a4c32
fix: merged functions
PascalDR Oct 22, 2024
8fc1fc3
fix: find_vp_token_key refactoring
PascalDR Oct 22, 2024
0333221
test: added tests for jwt package
PascalDR Oct 22, 2024
f4a61e2
fix: added boundary check
PascalDR Oct 22, 2024
655b13e
feat: added as_public_dict
PascalDR Oct 23, 2024
9bc0493
feat: refactoring
PascalDR Oct 23, 2024
184fb36
tests: added federation to the config
PascalDR Oct 23, 2024
f6903a5
chore: added todo
PascalDR Oct 24, 2024
8daebd3
Merge branch 'dev' of https://github.com/italia/eudi-wallet-it-satosa…
PascalDR Oct 24, 2024
a98d782
feat: implemented methods for trust source
PascalDR Oct 24, 2024
ff51ca9
fix: method name
PascalDR Oct 24, 2024
c98974b
chore: module initialization
PascalDR Oct 24, 2024
0c2e6de
feat: implemented TrustSourceData model
PascalDR Oct 24, 2024
f1d527c
chore: initialized module
PascalDR Oct 24, 2024
d4e443a
feat: implemented TrustHandlerInterface
PascalDR Oct 24, 2024
15e33c1
feat: initial implementation of metadata handler
PascalDR Oct 24, 2024
12743d8
fix: class name
PascalDR Oct 24, 2024
16ea0af
feat: initial implementation of DirectTrustJWTHandler
PascalDR Oct 24, 2024
643bc8d
feat: refactoring of CombinedTrustEvaluator
PascalDR Oct 24, 2024
f4bf886
Update pyeudiw/jwt/verification.py
PascalDR Oct 24, 2024
59328ba
feat: added trust source methods and refactoring
PascalDR Oct 25, 2024
31604c9
feat: merged implementations
PascalDR Oct 25, 2024
0f2c70d
chore: moved type
PascalDR Oct 25, 2024
19d9a33
chore: updated config
PascalDR Oct 25, 2024
d1cbf29
fix: update mongo tests
PascalDR Oct 25, 2024
29ad2b5
fix: fixed model
PascalDR Oct 25, 2024
b66c44e
chore: removed unused code
PascalDR Oct 25, 2024
30b5fce
chore: removed unused function
PascalDR Oct 25, 2024
1fd9417
chore: string interpolation
PascalDR Oct 25, 2024
4a28df1
feat: dinamyc backend refactoring
PascalDR Oct 25, 2024
c28fa75
feat: updated inteface
PascalDR Oct 25, 2024
014732a
feat: added dummy Federation handler
PascalDR Oct 25, 2024
b135479
fix: CombinedTrustEvaluator instantiation
PascalDR Oct 25, 2024
33ce2ba
tests: added tests for trust module
PascalDR Oct 25, 2024
06e42d0
fix: parameter handling
PascalDR Oct 25, 2024
7a58805
Merge branch 'feature/coverage' of https://github.com/PascalDR/eudi-w…
PascalDR Oct 25, 2024
a71e45e
chore: removed unused files
PascalDR Oct 31, 2024
97047da
feat: db code refactoring
PascalDR Oct 31, 2024
cbbadaa
feat: migrated tests
PascalDR Oct 31, 2024
9ff52c2
fix: minor fixs
PascalDR Oct 31, 2024
56db57f
chore: removed unused code
PascalDR Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyeudiw/jwk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ def find_jwk_by_kid(kid: str, jwks: list[dict], as_dict: bool = True) -> dict |
if not kid:
raise InvalidKid("Kid cannot be empty")
for jwk in jwks:
valid_jwk = jwk.get("kid", None)
if valid_jwk and kid == valid_jwk:
jwk_kid = jwk.get("kid", None)
if jwk_kid and kid == jwk_kid:
return jwk if as_dict else JWK(jwk)

raise KidNotFoundError(f"Key with Kid {kid} not found")
3 changes: 3 additions & 0 deletions pyeudiw/jwt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ class JWSVerificationError(Exception):

class JWEEncryptionError(Exception):
pass

class JWTDecodeError(Exception):
pass
17 changes: 6 additions & 11 deletions pyeudiw/jwt/parse.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from dataclasses import dataclass
from jwcrypto.common import base64url_decode, json_decode
from pyeudiw.federation.trust_chain.parse import get_public_key_from_trust_chain
from pyeudiw.jwk import JWK
from pyeudiw.jwt.utils import is_jwt_format
from pyeudiw.x509.verify import get_public_key_from_x509_chain
from pyeudiw.jwt.utils import decode_jwt_header, decode_jwt_payload

KeyIdentifier_T = str

Expand All @@ -24,23 +24,18 @@ def parse(jws: str) -> 'DecodedJwt':
return unsafe_parse_jws(jws)


def _unsafe_decode_part(part: str) -> dict:
return json_decode(base64url_decode(part))


def unsafe_parse_jws(token: str) -> DecodedJwt:
"""Parse a token into it's component.
Correctness of this function is not guaranteed when the token is in a
derived format, such as sd-jwt and jwe.
"""
if not is_jwt_format(token):
raise ValueError(f"unable to parse {token}: not a jwt")
b64header, b64payload, signature, *_ = token.split(".")
head = {}
payload = {}

try:
head = _unsafe_decode_part(b64header)
payload = _unsafe_decode_part(b64payload)
head = decode_jwt_header(token)
payload = decode_jwt_payload(token)
signature = token.split(".")[2]
except Exception as e:
raise ValueError(f"unable to decode JWS part: {e}")
return DecodedJwt(token, head, payload, signature=signature)
Expand All @@ -51,7 +46,7 @@ def extract_key_identifier(token_header: dict) -> JWK | KeyIdentifier_T:
if "kid" in token_header.keys():
return KeyIdentifier_T(token_header["kid"])
if "trust_chain" in token_header.keys():
return get_public_key_from_trust_chain(token_header["kid"])
return get_public_key_from_trust_chain(token_header["trust_chain"])
if "x5c" in token_header.keys():
return get_public_key_from_x509_chain(token_header["x5c"])
raise ValueError(f"unable to infer identifying key from token head: searched among keys {token_header.keys()}")
68 changes: 20 additions & 48 deletions pyeudiw/jwt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Dict

from pyeudiw.jwk import find_jwk_by_kid
from pyeudiw.jwt.exceptions import JWTInvalidElementPosition
from pyeudiw.jwt.exceptions import JWTInvalidElementPosition, JWTDecodeError

# jwt regexp pattern is non terminating, hence it match jwt, sd-jwt and sd-jwt with kb
JWT_REGEXP = r'^[_\w\-]+\.[_\w\-]+\.[_\w\-]+'
Expand All @@ -24,17 +24,29 @@ def decode_jwt_element(jwt: str, position: int) -> dict:
:returns: a dict with the content of the decoded section.
:rtype: dict
"""
if position > 1 or position < 0:
if position < 0:
raise JWTInvalidElementPosition(
f"Cannot accept negative position {position}")

if position > 2:
raise JWTInvalidElementPosition(
f"Cannot accept position greater than 2 {position}")

splitted_jwt = jwt.split(".")

if (len(splitted_jwt) - 1) < position:
raise JWTInvalidElementPosition(
f"JWT has no element in position {position}")

if isinstance(jwt, bytes):
jwt = jwt.decode()
try:
if isinstance(jwt, bytes):
jwt = jwt.decode()

b = jwt.split(".")[position]
padded = f"{b}{'=' * divmod(len(b), 4)[1]}"
data = json.loads(base64.urlsafe_b64decode(padded))
return data
b64_data = jwt.split(".")[position]
data = json.loads(base64_urldecode(b64_data))
return data
except Exception as e:
raise JWTDecodeError(f"Unable to decode JWT element: {e}")


def decode_jwt_header(jwt: str) -> dict:
Expand Down Expand Up @@ -63,30 +75,6 @@ def decode_jwt_payload(jwt: str) -> dict:
return decode_jwt_element(jwt, position=1)


def get_jwk_from_jwt(jwt: str, provider_jwks: Dict[str, dict]) -> dict:
"""
Find the JWK inside the provider JWKs with the kid
specified in jwt header.

:param jwt: a string that represents the jwt.
:type jwt: str
:param provider_jwks: a dictionary that contains one or more JWKs with the KID as the key.
:type provider_jwks: Dict[str, dict]

:raises InvalidKid: if kid is None.
:raises KidNotFoundError: if kid is not in jwks list.

:returns: the jwk as dict.
:rtype: dict
"""
head = decode_jwt_header(jwt)
kid = head["kid"]
if isinstance(provider_jwks, dict) and provider_jwks.get('keys'):
provider_jwks = provider_jwks['keys']

return find_jwk_by_kid(kid, provider_jwks)


def is_jwt_format(jwt: str) -> bool:
"""
Check if a string is in JWT format.
Expand Down Expand Up @@ -124,22 +112,6 @@ def is_jwe_format(jwt: str):
return True


def is_jws_format(jwt: str):
"""
Check if a string is in JWS format.

:param jwt: a string that represents the jwt.
:type jwt: str

:returns: True if the string is a JWS, False otherwise.
:rtype: bool
"""
if not is_jwt_format(jwt):
return False

return not is_jwe_format(jwt)


def base64_urlencode(v: bytes) -> str:
"""Urlsafe base64 encoding without padding symbols

Expand Down
19 changes: 13 additions & 6 deletions pyeudiw/jwt/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pyeudiw.jwt.utils import decode_jwt_payload
from pyeudiw.tools.utils import iat_now


def verify_jws_with_key(jws: str, key: JWK) -> None:
"""
:raises JWSVerificationError: is signature verification fails for *any* reason
Expand All @@ -15,16 +14,24 @@ def verify_jws_with_key(jws: str, key: JWK) -> None:
except Exception as e:
raise JWSVerificationError(f"error during signature verification: {e}", e)

def is_jwt_expired(token: str) -> bool:
"""
Check if a jwt is expired.

:param token: a string that represents the jwt.
:type token: str

:returns: True if the token is expired, False otherwise.
:rtype: bool
"""

token_payload = decode_jwt_payload(token)

def is_payload_expired(token_payload: dict) -> bool:
exp = token_payload.get("exp", None)
if not exp:
return True
if exp < iat_now():
PascalDR marked this conversation as resolved.
Show resolved Hide resolved
return True
return False



def is_jwt_expired(token: str) -> bool:
payalod = decode_jwt_payload(token)
return is_payload_expired(payalod)
23 changes: 3 additions & 20 deletions pyeudiw/satosa/default/response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from pyeudiw.sd_jwt.schema import VerifierChallenge
from pyeudiw.storage.exceptions import StorageWriteError
from pyeudiw.tools.utils import iat_now
from pyeudiw.trust.interface import TrustEvaluator
from pyeudiw.tools.jwk_handling import find_vp_token_key


class ResponseHandler(ResponseHandlerInterface, BackendTrust):
Expand Down Expand Up @@ -184,7 +184,7 @@ def response_endpoint(self, context: Context, *args: tuple) -> Redirect | JsonRe
token_parser, token_verifier = self._vp_verifier_factory(authz_payload.presentation_submission, vp_token, request_session)
except ValueError as e:
return self._handle_400(context, f"VP parsing error: {e}")
pub_jwk = _find_vp_token_key(token_parser, self.trust_evaluator)
pub_jwk = find_vp_token_key(token_parser, self.trust_evaluator)
token_verifier.verify_signature(pub_jwk)
try:
token_verifier.verify_challenge()
Expand Down Expand Up @@ -303,21 +303,4 @@ def _vp_verifier_factory(self, presentation_submission: dict, token: str, sessio
return (token_processor, deepcopy(token_processor))

def _get_verifier_challenge(self, session_data: dict) -> VerifierChallenge:
return {"aud": self.client_id, "nonce": session_data["nonce"]}


def _find_vp_token_key(token_parser: VpTokenParser, key_source: TrustEvaluator) -> JWK:
# TODO: move somewhere appropriate: this doesn't HAVE to be in the response handler
issuer = token_parser.get_issuer_name()
trusted_pub_keys = key_source.get_public_keys(issuer)
verification_key = token_parser.get_signing_key()
if isinstance(verification_key, str):
# search by kid
kid = verification_key
pub_jwks = [key for key in trusted_pub_keys if key.get("kid", "") == kid]
if len(pub_jwks) != 1:
raise Exception(f"no unique valid trusted key with kid={kid} for issuer {issuer}")
return JWK(pub_jwks[0])
if isinstance(verification_key, dict):
raise NotImplementedError("TODO: matching of public key (ex. from x5c) with keys from trust source")
raise Exception(f"invalid state: key with type {type(verification_key)}")
return {"aud": self.client_id, "nonce": session_data["nonce"]}
3 changes: 3 additions & 0 deletions pyeudiw/tests/jwt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VALID_KID_JWT = "eyJraWQiOiIxMjM0NTYiLCJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.bjM57L1H4gB60_020lKBVvVEhiYCOeEWGzMVEt-XNjc"
VALID_TC_JWT = "eyJ0cnVzdF9jaGFpbiI6WyJleUpoYkdjaU9pSkZVekkxTmlJc0ltdHBaQ0k2SW1Gck5WQk9NR1IxV2pOQ2VWbFZVa1ZOTVdzeldtMVJNMVJGVm5sT1NFb3dXVEpXVkZsVVdrMVRTRkkwVldzeFNWRXhRazlVU0VwUVUxRWlMQ0owZVhBaU9pSmxiblJwZEhrdGMzUmhkR1Z0Wlc1MEsycDNkQ0o5LmV5SmxlSEFpT2pFM01qazVNRFF6TkRJc0ltbGhkQ0k2TVRjeU9UWXdORE0wTWl3aWFYTnpJam9pYUhSMGNITTZMeTlqY21Wa1pXNTBhV0ZzWDJsemMzVmxjaTVsZUdGdGNHeGxMbTl5WnlJc0luTjFZaUk2SW1oMGRIQnpPaTh2WTNKbFpHVnVkR2xoYkY5cGMzTjFaWEl1WlhoaGJYQnNaUzV2Y21jaUxDSnFkMnR6SWpwN0ltdGxlWE1pT2x0N0ltdDBlU0k2SWtWRElpd2lhMmxrSWpvaVlXczFVRTR3WkhWYU0wSjVXVlZTUlUweGF6TmFiVkV6VkVWV2VVNUlTakJaTWxaVVdWUmFUVk5JVWpSVmF6RkpVVEZDVDFSSVNsQlRVU0lzSW1Gc1p5STZJa1ZUTWpVMklpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lZakJJY21WNmJUVnhOMU16VUU5NlpWTm9iVTlXUmpKVlYxOHpibkp2UjBSTlduQmFlRmhsUzFCMFVTSXNJbmtpT2lJdE1FOUhWMHhuT0dOb2FWSXRRbmRQUTJwWmVuZzFNbTFNWmxFMWIzQlNWalZZUTBsVmFtbHBhVlJSSW4xZGZTd2liV1YwWVdSaGRHRWlPbnNpYjNCbGJtbGtYMk55WldSbGJuUnBZV3hmYVhOemRXVnlJanA3SW1wM2EzTWlPbnNpYTJWNWN5STZXM3NpYTNSNUlqb2lSVU1pTENKcmFXUWlPaUpOYmxFMFZVZEtibVZXVWxkWU1EbDVaV3BDVUdJeWVEQlZNVTUwWVVaYWJGZ3dNVTlQVlRsSVUwZDBNVlZwTVU1TlJFNVZWMGRzVTFKUklpd2lZV3huSWpvaVJWTXlOVFlpTENKamNuWWlPaUpRTFRJMU5pSXNJbmdpT2lKNlZIQmpORFl4TjFkTFNVRjBVVVZYV2xsWWVERkZSalpHT0VwblYzb3pkSGxsYUhjNE1VSjNiRzg0SWl3aWVTSTZJbU5JVHkxRGFEWnNlVVV5WW13ek1UTnJlbFJoUzNKRWJDMTROM1pYYmtVMGRrVTBWVGRXVVVGNWFrMGlmVjE5ZlN3aVptVmtaWEpoZEdsdmJsOWxiblJwZEhraU9uc2liM0puWVc1cGVtRjBhVzl1WDI1aGJXVWlPaUpQY0dWdVNVUWdRM0psWkdWdWRHbGhiQ0JKYzNOMVpYSWdaWGhoYlhCc1pTSXNJbWh2YldWd1lXZGxYM1Z5YVNJNkltaDBkSEJ6T2k4dlkzSmxaR1Z1ZEdsaGJGOXBjM04xWlhJdVpYaGhiWEJzWlM1dmNtY3ZhRzl0WlNJc0luQnZiR2xqZVY5MWNta2lPaUpvZEhSd2N6b3ZMMk55WldSbGJuUnBZV3hmYVhOemRXVnlMbVY0WVcxd2JHVXViM0puTDNCdmJHbGplU0lzSW14dloyOWZkWEpwSWpvaWFIUjBjSE02THk5amNtVmtaVzUwYVdGc1gybHpjM1ZsY2k1bGVHRnRjR3hsTG05eVp5OXpkR0YwYVdNdmJHOW5ieTV6ZG1jaUxDSmpiMjUwWVdOMGN5STZXeUowWldOb1FHTnlaV1JsYm5ScFlXeGZhWE56ZFdWeUxtVjRZVzF3YkdVdWIzSm5JbDE5ZlN3aVlYVjBhRzl5YVhSNVgyaHBiblJ6SWpwYkltaDBkSEJ6T2k4dmFXNTBaWEp0WldScFlYUmxMbVZwWkdGekxtVjRZVzF3YkdVdWIzSm5JbDE5LmtlNThMQ1NTRnZ5aTZkYW9hUlIzNDZhRjNUQ240bENBODZHWEhoRmEwOXVWRTZHa3Q2alVKaEI4dEZsdnZkWmJlcmhxYnZhdG9HRUNQQ1BlQ0syNk13IiwiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklrOVVUblJUUlRneVZsZDRZV0ZxU2pGV2JHeFhXVlJTVUdKSVdrUlpibEY1VVd4d1ptUXlWbWxpUlUweVZFVndWR1ZxUms1V1dHUlNXbmNpTENKMGVYQWlPaUpsYm5ScGRIa3RjM1JoZEdWdFpXNTBLMnAzZENKOS5leUpsZUhBaU9qRTNNams1TURRek5ESXNJbWxoZENJNk1UY3lPVFl3TkRNME1pd2lhWE56SWpvaWFIUjBjSE02THk5cGJuUmxjbTFsWkdsaGRHVXVaV2xrWVhNdVpYaGhiWEJzWlM1dmNtY2lMQ0p6ZFdJaU9pSm9kSFJ3Y3pvdkwyTnlaV1JsYm5ScFlXeGZhWE56ZFdWeUxtVjRZVzF3YkdVdWIzSm5JaXdpYW5kcmN5STZleUpyWlhseklqcGJleUpyZEhraU9pSkZReUlzSW10cFpDSTZJbUZyTlZCT01HUjFXak5DZVZsVlVrVk5NV3N6V20xUk0xUkZWbmxPU0Vvd1dUSldWRmxVV2sxVFNGSTBWV3N4U1ZFeFFrOVVTRXBRVTFFaUxDSmhiR2NpT2lKRlV6STFOaUlzSW1OeWRpSTZJbEF0TWpVMklpd2llQ0k2SW1Jd1NISmxlbTAxY1RkVE0xQlBlbVZUYUcxUFZrWXlWVmRmTTI1eWIwZEVUVnB3V25oWVpVdFFkRkVpTENKNUlqb2lMVEJQUjFkTVp6aGphR2xTTFVKM1QwTnFXWHA0TlRKdFRHWlJOVzl3VWxZMVdFTkpWV3BwYVdsVVVTSjlYWDE5LjltMWk5cWNETFNucGJ3aU5iR1pKb3pvdlJUeGhGNlFiLUV2U1pmWU5lN2NzbmhZX2F1VERLRGllWW9aQmZhaW5ZR2lITTJ4dzk4LXdna3lnTFY3S0h3IiwiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklsWnRkekpaYkdjMFRWUnJOV05XYkhSaVJXeEhVa1JPVmsxNlp6UlVWMXBQVFRCR1VXTXdOREZqTTBKSlpGVmtjMWxZUm05VGJWSkxUa0VpTENKMGVYQWlPaUpsYm5ScGRIa3RjM1JoZEdWdFpXNTBLMnAzZENKOS5leUpsZUhBaU9qRTNNams1TURRek5ESXNJbWxoZENJNk1UY3lPVFl3TkRNME1pd2lhWE56SWpvaWFIUjBjSE02THk5MGNuVnpkQzFoYm1Ob2IzSXVaWGhoYlhCc1pTNXZjbWNpTENKemRXSWlPaUpvZEhSd2N6b3ZMMmx1ZEdWeWJXVmthV0YwWlM1bGFXUmhjeTVsZUdGdGNHeGxMbTl5WnlJc0ltcDNhM01pT25zaWEyVjVjeUk2VzNzaWEzUjVJam9pUlVNaUxDSnJhV1FpT2lKUFZFNTBVMFU0TWxaWGVHRmhha294Vm14c1YxbFVVbEJpU0ZwRVdXNVJlVkZzY0daa01sWnBZa1ZOTWxSRmNGUmxha1pPVmxoa1VscDNJaXdpWVd4bklqb2lSVk15TlRZaUxDSmpjbllpT2lKUUxUSTFOaUlzSW5naU9pSnJOMVJNV1ZGMVNYRTVlR05uYkdWU2QwNXZZWEJHYzFRMWVEVmpkM0IwT0V4U1QyZDFNRWhTWkU4d0lpd2llU0k2SWxoNE1UQmhXblp4ZUZGclZXeEdaVVF4ZGt4MWJuaFdTbmR2YkdacFVHeHFRaTF3T1hSZlkwaExPV01pZlYxOWZRLmI3eHlHdERwMi1aTVdsTkJOT2pFZVVnRUNMX29QN1RRamRIbGoybWVfWTZqc19BZW9FaGxRLTJlTXpXdGN1WUs0R1Y4eExHb0g3Q2xuN3BGSTFPeFRnIiwiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNklsWnRkekpaYkdjMFRWUnJOV05XYkhSaVJXeEhVa1JPVmsxNlp6UlVWMXBQVFRCR1VXTXdOREZqTTBKSlpGVmtjMWxZUm05VGJWSkxUa0VpTENKMGVYQWlPaUpsYm5ScGRIa3RjM1JoZEdWdFpXNTBLMnAzZENKOS5leUpsZUhBaU9qRTNNams1TURRek5ESXNJbWxoZENJNk1UY3lPVFl3TkRNME1pd2lhWE56SWpvaWFIUjBjSE02THk5MGNuVnpkQzFoYm1Ob2IzSXVaWGhoYlhCc1pTNXZjbWNpTENKemRXSWlPaUpvZEhSd2N6b3ZMM1J5ZFhOMExXRnVZMmh2Y2k1bGVHRnRjR3hsTG05eVp5SXNJbXAzYTNNaU9uc2lhMlY1Y3lJNlczc2lhM1I1SWpvaVJVTWlMQ0pyYVdRaU9pSldiWGN5V1d4bk5FMVVhelZqVm14MFlrVnNSMUpFVGxaTmVtYzBWRmRhVDAwd1JsRmpNRFF4WXpOQ1NXUlZaSE5aV0VadlUyMVNTMDVCSWl3aVlXeG5Jam9pUlZNeU5UWWlMQ0pqY25ZaU9pSlFMVEkxTmlJc0luZ2lPaUpOUW14V1gxTm1YMU4yYVdzeFdqSjRaa3hrZGpKek5rZEhielp1UWxwWU1VTnBRVTlXV1Y5Q2EzTjNJaXdpZVNJNkltTkxkakV3WVRSblQySlZOVmx1YVUxMFpVMVFRVGRwWmpod2JEUnlaM2hUVFhKMGJDMVdOREJSVkhNaWZWMTlMQ0p0WlhSaFpHRjBZU0k2ZXlKbVpXUmxjbUYwYVc5dVgyVnVkR2wwZVNJNmV5Sm1aV1JsY21GMGFXOXVYMlpsZEdOb1gyVnVaSEJ2YVc1MElqb2lhSFIwY0hNNkx5OTBjblZ6ZEMxaGJtTm9iM0l1WlhoaGJYQnNaUzV2Y21jdlptVjBZMmdpTENKbVpXUmxjbUYwYVc5dVgzSmxjMjlzZG1WZlpXNWtjRzlwYm5RaU9pSm9kSFJ3Y3pvdkwzUnlkWE4wTFdGdVkyaHZjaTVsZUdGdGNHeGxMbTl5Wnk5eVpYTnZiSFpsSWl3aVptVmtaWEpoZEdsdmJsOXNhWE4wWDJWdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5MGNuVnpkQzFoYm1Ob2IzSXVaWGhoYlhCc1pTNXZjbWN2YkdsemRDSXNJbTl5WjJGdWFYcGhkR2x2Ymw5dVlXMWxJam9pVkVFZ1pYaGhiWEJzWlNJc0ltaHZiV1Z3WVdkbFgzVnlhU0k2SW1oMGRIQnpPaTh2ZEhKMWMzUXRZVzVqYUc5eUxtVjRZVzF3YkdVdWIzSm5MMmh2YldVaUxDSndiMnhwWTNsZmRYSnBJam9pYUhSMGNITTZMeTkwY25WemRDMWhibU5vYjNJdVpYaGhiWEJzWlM1dmNtY3ZjRzlzYVdONUlpd2liRzluYjE5MWNta2lPaUpvZEhSd2N6b3ZMM1J5ZFhOMExXRnVZMmh2Y2k1bGVHRnRjR3hsTG05eVp5OXpkR0YwYVdNdmJHOW5ieTV6ZG1jaUxDSmpiMjUwWVdOMGN5STZXeUowWldOb1FIUnlkWE4wTFdGdVkyaHZjaTVsZUdGdGNHeGxMbTl5WnlKZGZYMHNJbU52Ym5OMGNtRnBiblJ6SWpwN0ltMWhlRjl3WVhSb1gyeGxibWQwYUNJNk1YMTkuTWJwWGZlX05wUGdiZFdMX3pOMzBTWEE4OGFXcmV3YUp5TVdKRkFlZ05yTi04VnkydW1jcHEzTVFwaDdZejNaVGF3R2dpNk9HV1g3VVRERk9XV21mOXciXSwiYWxnIjoiSFMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.9SGvKJ6ucPo8pkxpg-VXIYwijHH6jyOeqezgkFP74ow"
VALID_JWE = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiIxOGIxY2Y3NThjMWQ0ZWM2YmRhNjU4OTM1N2FiZGQ4NSIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.gCbxP78o3DgpDTUQbuHniuGgYpATqgGkRGy7paC6hRrz7N7eIa6sAOWDO9Fhnj-c8ocMl4cF4Jb_mv5qRPCh9r57PBqx7jOhMIMPTwJGpjcyBaqtHlZlu1vupY5tQ3Y2jGz1Ti4BnywaeEHPyIPQJtN7F7hIAORzj7IY4sIKkVXtQJZgaKW8pEHq_GCqj8i5aaiM0uJnRG3GOh3livp9Npjv9doqp3gyPa1zjrg2H1RsOGn0j2QMGvtuVfkuNwF-SoPKFECyHOq0ZK1oH2sTO8-JwvHflbIZQr5xWTpS8q7MbUXEuqURtrg0Tj-2z6tdaOLT4b3UeDufK2ar3bBfRD4-nRALtoY0ekcMyGFOS7o1Mxl3hy5sIG-EySyWeuBVy68aDWDpi9qZoQuY1TbxxakjncCOGu_Gh1l1m_mK2l_IdyXCT_GCfzFq4ZTkPZ5eydNBAPZuxBLUb4BrMb5iDdZjT7AgGOlRre_wIRHmmKm8W9nDeQQRmbIXO23JuOw9.BDCarfq2r_Uk8DHNfsNwSQ.4DuQx1cfJXadHnudrVaBss45zxyd6iouuSzZUyOeM4ikF_7hDOgwmaCma-Z97_QZBJ5DzVn9SJhKUTAqpVR3BRGAxJ_HAXU5jaTjXqbvUaxsh7Z5TgZ9eck0FIoe1lkwv51xEvYqqQ_Xojr4MAEmLuME_9ArCK9mNaMADIzOj4VoQtaDP1l26ytocc-oENifBRYGu28LbJLkyQKzyQy6FuAOtWjLM0WCXV7-o_dvj6qfeYHNBD7YBSxyqdgD8dcxMBNd2sK73YsZPHEa0V1-8zz7hm3bH3tZelpwPWScqLLW_SUH586c0FVeI6ggvqzjfLZ_Y6eQibVSdXfOtJBk22QrLsuCXbRK8G1w9t23Pwu8ukUAw4v0l7HeaW_0SJyKSPQANRP83MyFbK7fmzTYaW9TYN2JrKN-PLpd2dIFSm2Ga_EfaCwNJBm4RDMzDNrf-O0AissvYyHb0WaALiCiFCogliYqLzRB6xDb-b4964M.J7WDOFLRRPJ7lLpTfN2mOiXLDg5xtaF-sLQ4mOeN5oc"
Loading
Loading