diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bb0333b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "jwt-cli" +version = "0.0.1" +authors = [ + { name="Walter Oggioni", email="oggioni.walter@gmail.com" }, +] +description = "JWT command line utilities" +readme = "README.md" +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ + "jwcrypto >= 1.5.6", + 'typing_extensions==4.7.1', + 'pwo >= 0.0.2' +] + +[project.urls] +"Homepage" = "https://gitea.woggioni.net/woggioni/jwt-cli" +"Bug Tracker" = "https://gitea.woggioni.net/woggioni/jwt-cli/issues" + +[project.scripts] +jwt = "jwt_cli:main" + +[tool.mypy] +python_version = "3.12" +disallow_untyped_defs = true +show_error_codes = true +no_implicit_optional = true +warn_return_any = true +warn_unused_ignores = true +exclude = ["scripts", "docs", "test"] +strict = true \ No newline at end of file diff --git a/src/jwt_cli.egg-info/PKG-INFO b/src/jwt_cli.egg-info/PKG-INFO new file mode 100644 index 0000000..65600fb --- /dev/null +++ b/src/jwt_cli.egg-info/PKG-INFO @@ -0,0 +1,14 @@ +Metadata-Version: 2.1 +Name: jwt-cli +Version: 0.0.1 +Summary: JWT command line utilities +Author-email: Walter Oggioni +Project-URL: Homepage, https://gitea.woggioni.net/woggioni/jwt-cli +Project-URL: Bug Tracker, https://gitea.woggioni.net/woggioni/jwt-cli/issues +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.12 +Description-Content-Type: text/markdown +Requires-Dist: jwcrypto>=1.5.6 +Requires-Dist: typing_extensions==4.7.1 diff --git a/src/jwt_cli.egg-info/SOURCES.txt b/src/jwt_cli.egg-info/SOURCES.txt new file mode 100644 index 0000000..200826f --- /dev/null +++ b/src/jwt_cli.egg-info/SOURCES.txt @@ -0,0 +1,12 @@ +pyproject.toml +src/jwt_cli/__init__.py +src/jwt_cli/create_key_command.py +src/jwt_cli/key.py +src/jwt_cli/key_command.py +src/jwt_cli/main.py +src/jwt_cli.egg-info/PKG-INFO +src/jwt_cli.egg-info/SOURCES.txt +src/jwt_cli.egg-info/dependency_links.txt +src/jwt_cli.egg-info/entry_points.txt +src/jwt_cli.egg-info/requires.txt +src/jwt_cli.egg-info/top_level.txt \ No newline at end of file diff --git a/src/jwt_cli.egg-info/dependency_links.txt b/src/jwt_cli.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/jwt_cli.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/jwt_cli.egg-info/entry_points.txt b/src/jwt_cli.egg-info/entry_points.txt new file mode 100644 index 0000000..0bef276 --- /dev/null +++ b/src/jwt_cli.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +jwt = jwt_cli:main diff --git a/src/jwt_cli.egg-info/requires.txt b/src/jwt_cli.egg-info/requires.txt new file mode 100644 index 0000000..4a86738 --- /dev/null +++ b/src/jwt_cli.egg-info/requires.txt @@ -0,0 +1,2 @@ +jwcrypto>=1.5.6 +typing_extensions==4.7.1 diff --git a/src/jwt_cli.egg-info/top_level.txt b/src/jwt_cli.egg-info/top_level.txt new file mode 100644 index 0000000..2f40e56 --- /dev/null +++ b/src/jwt_cli.egg-info/top_level.txt @@ -0,0 +1 @@ +jwt_cli diff --git a/src/jwt_cli/__init__.py b/src/jwt_cli/__init__.py new file mode 100644 index 0000000..b668da9 --- /dev/null +++ b/src/jwt_cli/__init__.py @@ -0,0 +1 @@ +from .main import main \ No newline at end of file diff --git a/src/jwt_cli/__pycache__/__init__.cpython-312.pyc b/src/jwt_cli/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..0b630bf Binary files /dev/null and b/src/jwt_cli/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/jwt_cli/__pycache__/create_key_command.cpython-312.pyc b/src/jwt_cli/__pycache__/create_key_command.cpython-312.pyc new file mode 100644 index 0000000..3e7d2cf Binary files /dev/null and b/src/jwt_cli/__pycache__/create_key_command.cpython-312.pyc differ diff --git a/src/jwt_cli/__pycache__/key.cpython-312.pyc b/src/jwt_cli/__pycache__/key.cpython-312.pyc new file mode 100644 index 0000000..61fa58b Binary files /dev/null and b/src/jwt_cli/__pycache__/key.cpython-312.pyc differ diff --git a/src/jwt_cli/__pycache__/key_command.cpython-312.pyc b/src/jwt_cli/__pycache__/key_command.cpython-312.pyc new file mode 100644 index 0000000..585ce05 Binary files /dev/null and b/src/jwt_cli/__pycache__/key_command.cpython-312.pyc differ diff --git a/src/jwt_cli/__pycache__/main.cpython-312.pyc b/src/jwt_cli/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..0e22967 Binary files /dev/null and b/src/jwt_cli/__pycache__/main.cpython-312.pyc differ diff --git a/src/jwt_cli/__pycache__/token.cpython-312.pyc b/src/jwt_cli/__pycache__/token.cpython-312.pyc new file mode 100644 index 0000000..4cec36c Binary files /dev/null and b/src/jwt_cli/__pycache__/token.cpython-312.pyc differ diff --git a/src/jwt_cli/__pycache__/token_command.cpython-312.pyc b/src/jwt_cli/__pycache__/token_command.cpython-312.pyc new file mode 100644 index 0000000..d07cea5 Binary files /dev/null and b/src/jwt_cli/__pycache__/token_command.cpython-312.pyc differ diff --git a/src/jwt_cli/create_key_command.py b/src/jwt_cli/create_key_command.py new file mode 100644 index 0000000..d79f055 --- /dev/null +++ b/src/jwt_cli/create_key_command.py @@ -0,0 +1,148 @@ +from argparse import ArgumentParser, Action +from enum import Enum +from jwcrypto.jwk import JWK +from typing import Optional, Any + +from .key import KeyFileType, write_key + + +class ECCurve(Enum): + p256 = 'P-256' + p384 = 'P-384' + p521 = 'P-521' + bp256 = 'BP-256' + bp384 = 'BP-384' + bp521 = 'BP-521' + secp256k1 = 'secp256k1' + + def __str__(self) -> str: + return self.value + + +class OKPCurve(Enum): + Ed25519 = 'Ed25519' + Ed448 = 'Ed448' + X25519 = 'X25519' + X448 = 'X448' + + def __str__(self) -> str: + return self.value + + +def _create_ec_key( + output_type: Optional[KeyFileType] = None, + curve: Optional[ECCurve] = None, + output_file: Optional[str] = None, + out_password: Optional[str] = None, + **kwargs: Any +) -> None: + jwk = JWK.generate(kty='EC', crv=curve.value) + write_key(jwk, output_file, output_type, out_password=out_password) + + +def _create_okp_key( + output_type: Optional[KeyFileType] = None, + curve: Optional[OKPCurve] = None, + output_file: Optional[str] = None, + out_password: Optional[str] = None, + **kwargs: Any +) -> None: + jwk = JWK.generate(kty='OKP', crv=curve.value) + write_key(jwk, output_file, output_type, out_password=out_password) + +def _create_oct_key( + output_type: Optional[KeyFileType] = None, + size: Optional[int] = None, + output_file: Optional[str] = None, + out_password: Optional[str] = None, + **kwargs: Any +) -> None: + jwk = JWK.generate(kty='oct', size=size) + write_key(jwk, output_file, output_type, out_password=out_password) + +def _create_rsa_key( + output_type: Optional[KeyFileType] = None, + exponent: Optional[int] = None, + size: Optional[int] = None, + output_file: Optional[str] = None, + out_password: Optional[str] = None, + **kwargs: Any +) -> None: + jwk = JWK.generate(kty='RSA', public_exponent=exponent, size=size) + write_key(jwk, output_file, output_type, out_password=out_password) + + +def _add_common_params(cmd: ArgumentParser) -> None: + cmd.add_argument('-P', '--out-password', help='Password for the input key') + cmd.add_argument( + '-O', + '--output-type', + help='Type of output key material', + type=KeyFileType, + choices=list(KeyFileType), + required=True + ) + cmd.add_argument( + '-o', + '--output-file', + help='Output file (defaults to stdout)' + ) + + +def create_key_command(subparsers: Action) -> ArgumentParser: + cmd: ArgumentParser = subparsers.add_parser("create", help="Create a new JWK key") + cmd_subparsers = cmd.add_subparsers() + + rsa_cmd = cmd_subparsers.add_parser("rsa", help="Create a new JWK RSA key") + _add_common_params(rsa_cmd) + rsa_cmd.add_argument( + '-e', + '--exponent', + type=int, + help='Public exponent', + default=65537 + ) + rsa_cmd.add_argument( + '-s', + '--size', + type=int, + help='Size', + default=1024 + ) + rsa_cmd.set_defaults(func=_create_rsa_key) + + ec_cmd = cmd_subparsers.add_parser("ec", help="Create a new JWK EC key") + _add_common_params(ec_cmd) + ec_cmd.add_argument( + '-c', + '--curve', + help='Elliptic curve name', + type=ECCurve, + choices=list(ECCurve), + default=ECCurve.p256 + ) + ec_cmd.set_defaults(func=_create_ec_key) + + okp_cmd = cmd_subparsers.add_parser("okp", help="Create a new JWK OKP key") + _add_common_params(okp_cmd) + okp_cmd.add_argument( + '-c', + '--curve', + help='Elliptic curve name', + type=OKPCurve, + choices=list(OKPCurve), + default=OKPCurve.Ed25519 + ) + okp_cmd.set_defaults(func=_create_okp_key) + + oct_cmd = cmd_subparsers.add_parser("oct", help="Create a new JWK oct key") + _add_common_params(oct_cmd) + oct_cmd.add_argument( + '-s', + '--size', + help='Key size', + type=int, + default=256 + ) + oct_cmd.set_defaults(func=_create_oct_key) + return cmd diff --git a/src/jwt_cli/key.py b/src/jwt_cli/key.py new file mode 100644 index 0000000..a135341 --- /dev/null +++ b/src/jwt_cli/key.py @@ -0,0 +1,85 @@ +import enum +import sys +from typing import Optional, BinaryIO, cast +from jwcrypto.jwk import JWK +from jwcrypto.common import json_decode +from cryptography.hazmat.primitives import serialization +from pwo import Maybe + + +@enum.unique +class KeyFileType(enum.Enum): + PEM = 'pem' + JSON = 'json' + DER = 'der' + + def __str__(self) -> str: + return self.value + + +def read_key( + input_file: Optional[str] = None, + input_type: Optional[KeyFileType] = None, + in_password: Optional[str] = None, +) -> JWK: + jwk = JWK() + + password = ( + Maybe.of_nullable(in_password) + .map(lambda it: it.encode('utf-8')) + .or_else(None) + ) + + def open_input_file() -> BinaryIO: + return open(input_file, 'rb') if input_file else sys.stdin.buffer + + if input_type == KeyFileType.PEM: + with open_input_file() as infile: + jwk.import_from_pem(infile.read().decode('utf-8'), password) + elif input_type == KeyFileType.JSON: + with open_input_file() as infile: + jwk.import_key(**json_decode(infile.read().decode('utf-8'))) + elif input_type == KeyFileType.DER: + with open_input_file() as infile: + der_obj = serialization.load_der_private_key(infile.read(), password) + jwk.import_from_pyca(der_obj) + return jwk + + +def write_key( + jwk: JWK, + output_file: Optional[str] = None, + output_type: Optional[KeyFileType] = None, + out_password: Optional[str] = None, + public: bool = False +) -> None: + + password = ( + Maybe.of_nullable(out_password) + .map(lambda it: it.encode('utf-8')) + .or_none() + ) + def open_output_file() -> BinaryIO: + return cast(BinaryIO, open(output_file, 'wb')) if output_file else sys.stdout.buffer + + private_key = not public + if output_type == KeyFileType.PEM: + with open_output_file() as outfile: + outfile.write(jwk.export_to_pem(private_key=private_key, password=password)) + elif output_type == KeyFileType.JSON: + with open_output_file() as outfile: + outfile.write(jwk.export(private_key=private_key).encode('utf-8')) + elif output_type == KeyFileType.DER: + with open_output_file() as outfile: + pem_obj = serialization.load_pem_private_key( + jwk.export_to_pem(private_key=private_key, password=None), + password=None + ) + der_data = pem_obj.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(password) + if password + else serialization.NoEncryption() + ) + outfile.write(der_data) diff --git a/src/jwt_cli/key_command.py b/src/jwt_cli/key_command.py new file mode 100644 index 0000000..5eb156b --- /dev/null +++ b/src/jwt_cli/key_command.py @@ -0,0 +1,62 @@ +from argparse import ArgumentParser +from typing import Optional + +from .create_key_command import create_key_command +from .key import read_key, write_key, KeyFileType + + +def key_command( + input_file: Optional[str] = None, + output_file: Optional[str] = None, + input_type: Optional[KeyFileType] = None, + output_type: Optional[KeyFileType] = None, + in_password: Optional[str] = None, + out_password: Optional[str] = None, + public: bool = False, + **kwargs +) -> None: + jwk = read_key(input_file, input_type, in_password) + write_key(jwk, output_file, output_type, out_password, public) + +def add_key_command(subparsers: ArgumentParser) -> ArgumentParser: + cmd: ArgumentParser = subparsers.add_parser("key", help="Manage JWKs") + key_subparsers = cmd.add_subparsers(description="subcommands") + convert: ArgumentParser = key_subparsers.add_parser("convert", help="Convert JWKs in/from different formats") + convert.add_argument('-p', '--in-password', + help='Password for the input key') + convert.add_argument('-P', '--out-password', + help='Password for the input key') + convert.add_argument( + '-I', + '--input-type', + help='Type of input key material', + type=KeyFileType, + choices=list(KeyFileType), + required=True + ) + convert.add_argument( + '-O', + '--output-type', + help='Type of output key material', + type=KeyFileType, + choices=list(KeyFileType), + required=True + ) + convert.add_argument( + '-i', + '--input-file', + help='Input file (defaults to stdin)' + ) + convert.add_argument( + '-o', + '--output-file', + help='Output file (defaults to stdout)' + ) + convert.add_argument( + '--public', + help='Only output public key', + action='store_true' + ) + convert.set_defaults(func=key_command) + create_key_command(key_subparsers) + return cmd diff --git a/src/jwt_cli/main.py b/src/jwt_cli/main.py new file mode 100644 index 0000000..0010d80 --- /dev/null +++ b/src/jwt_cli/main.py @@ -0,0 +1,15 @@ +import argparse + +from typing import List, Optional +from .key_command import add_key_command +from .token_command import add_token_command + + +def main(args: Optional[List[str]] = None): + # Create the parser + parser = argparse.ArgumentParser(description="A simple CLI program to manage JWT") + subparsers = parser.add_subparsers(title="subcommands", description="valid subcommands") + key_command = add_key_command(subparsers) + token_command = add_token_command(subparsers) + args = parser.parse_args(args) + args.func(**vars(args)) diff --git a/src/jwt_cli/sign_command.py b/src/jwt_cli/sign_command.py new file mode 100644 index 0000000..52102cb --- /dev/null +++ b/src/jwt_cli/sign_command.py @@ -0,0 +1,48 @@ +from argparse import ArgumentParser +from typing import Optional + +from .key import read_key, KeyFileType +def signature_command( + input_file: Optional[str] = None, + input_type: Optional[KeyFileType] = None, + in_password: Optional[str] = None, + output_file: Optional[str] = None, + key: Optional[str] = None, + keys: Optional[str] = None, + **kwargs +) -> None: + jwk = read_key(input_file, input_type, in_password) + jwt = read_token(input_file, jwk=key, jwks=keys) + write_token(jwt, output_file) + + +def add_signature_command(subparsers: ArgumentParser) -> ArgumentParser: + cmd: ArgumentParser = subparsers.add_parser("signature", help="Manage JWSs") + signature_subparsers = cmd.add_subparsers(description="subcommands") + convert: ArgumentParser = signature_subparsers.add_parser("sign", help="Sign a payload") + convert.add_argument( + '-i', + '--input-file', + help='Input file (defaults to stdin)' + ) + convert.add_argument('-p', '--key-password', + help='Password for the signing key') + convert.add_argument( + '-I', + '--input-type', + help='Type of input key material', + type=KeyFileType, + choices=list(KeyFileType), + required=True + ) + convert.add_argument( + '-o', + '--output-file', + help='Output file (defaults to stdout)' + ) + convert.add_argument( + '--key', + help='Sign using the provided JWK', + ) + convert.set_defaults(func=signature_command) + return cmd \ No newline at end of file diff --git a/src/jwt_cli/signature.py b/src/jwt_cli/signature.py new file mode 100644 index 0000000..e69de29 diff --git a/src/jwt_cli/token.py b/src/jwt_cli/token.py new file mode 100644 index 0000000..269dfa5 --- /dev/null +++ b/src/jwt_cli/token.py @@ -0,0 +1,82 @@ +from jwcrypto.jwt import JWT +from jwcrypto.jws import JWS +from jwcrypto.jwk import JWKSet, JWK +import json + +from pwo import Maybe +from typing import Optional, BinaryIO, TextIO, cast +import sys + +from pwo import retry, ExceptionHandlerOutcome +from urllib.request import Request, urlretrieve, urlopen + + +class HttpException(Exception): + http_status_code: int + message: Optional[str] + + def __init__(self, http_status_code: int, msg: Optional[str] = None): + self.message = msg + self.http_status_code = http_status_code + + def __repr__(self) -> str: + return f'HTTP status {self.http_status_code}' + f': {self.message}' if self.message else '' + + +def error_handler(err): + return ExceptionHandlerOutcome.THROW + + +@retry(max_attempts=5, initial_delay=1.0, exception_handler=error_handler) +def fetch_keyset(url): + with urlopen(url) as response: + return JWKSet.from_json(response.read()) + + +@retry(max_attempts=5, initial_delay=1.0, exception_handler=error_handler) +def fetch_key(url): + with urlopen(url) as response: + return JWK.from_json(response.read()) + + +def read_token( + input_file: Optional[str] = None, + jwks: Optional[str] = None, + jwk: Optional[str] = None +) -> JWT: + jwt = JWT() + if jwks and jwk: + raise ValueError('Only one between key and keyset must be provided') + + keys = (Maybe.of_nullable(jwks) + .map(lambda uri: fetch_keyset(uri))) + key = (Maybe.of_nullable(jwk) + .map(lambda uri: fetch_key(uri))) + + def open_input_file() -> TextIO: + return open(input_file, 'r') if input_file else sys.stdin + + with open_input_file() as infile: + content = infile.read() + jwt.deserialize(content, key=(keys or key).or_none()) + return jwt + + +def write_token( + jwt: JWT, + output_file: Optional[str] = None, +) -> None: + def open_output_file() -> TextIO: + return cast(TextIO, open(output_file, 'wb')) if output_file else sys.stdout.buffer + + with open_output_file() as outfile: + # jws = cast(JWS, jwt.token) + token = jwt.token + if isinstance(token, JWS): + jws = cast(JWS, token) + if jws.is_valid: + outfile.write(jws.payload) + else: + outfile.write(jws.objects['payload']) + # header = jws.jose_header + # issuer = jws.objects['payload'] diff --git a/src/jwt_cli/token_command.py b/src/jwt_cli/token_command.py new file mode 100644 index 0000000..a510809 --- /dev/null +++ b/src/jwt_cli/token_command.py @@ -0,0 +1,42 @@ +from argparse import ArgumentParser +from typing import Optional + +from .create_key_command import create_key_command +from .token import read_token, write_token + + +def token_command( + input_file: Optional[str] = None, + output_file: Optional[str] = None, + key: Optional[str] = None, + keys: Optional[str] = None, + **kwargs +) -> None: + jwt = read_token(input_file, jwk=key, jwks=keys) + write_token(jwt, output_file) + + +def add_token_command(subparsers: ArgumentParser) -> ArgumentParser: + cmd: ArgumentParser = subparsers.add_parser("token", help="Manage JWTs") + token_subparsers = cmd.add_subparsers(description="subcommands") + convert: ArgumentParser = token_subparsers.add_parser("parse", help="Parse a JWT") + convert.add_argument( + '-i', + '--input-file', + help='Input file (defaults to stdin)' + ) + convert.add_argument( + '-o', + '--output-file', + help='Output file (defaults to stdout)' + ) + convert.add_argument( + '--key', + help='Verify or decrypt using the provided JWK', + ) + convert.add_argument( + '--keys', + help='Verify or decrypt using the provided JWKS', + ) + convert.set_defaults(func=token_command) + return cmd diff --git a/tests/__pycache__/test_token.cpython-312.pyc b/tests/__pycache__/test_token.cpython-312.pyc new file mode 100644 index 0000000..5b4cedb Binary files /dev/null and b/tests/__pycache__/test_token.cpython-312.pyc differ diff --git a/tests/auth_token.txt b/tests/auth_token.txt new file mode 100644 index 0000000..33d8144 --- /dev/null +++ b/tests/auth_token.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4MW40Z2k4Sm1HVDNnN3p3YnpKcmVsMm1LY0FvdURTVVphR3g4b3ZDZjAwIn0.eyJleHAiOjE3MTkxMTk3NTUsImlhdCI6MTcxOTExOTI3NSwiYXV0aF90aW1lIjoxNzE5MTE4ODgyLCJqdGkiOiI3NmY1NmY1Mi1mODBjLTQxZmUtODhhMS03YzQyNzM0OTUzYzgiLCJpc3MiOiJodHRwczovL3dvZ2dpb25pLm5ldC9hdXRoL3JlYWxtcy93b2dnaW9uaS5uZXQiLCJhdWQiOlsianBhY3JlcG8iLCJhY2NvdW50Il0sInN1YiI6ImIxYjAyZDY0LTZmYzctNDdkMC1iNGU5LTNjMTY3YzQ3Mzc4ZCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImpwYWNyZXBvLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiJiYWJhMGZhZi01ZWIwLTQwYTYtODhmZi1iMDI2MzI1ZGI4YjkiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm5leHRjbG91ZCIsImdpdGVhIiwiZGVmYXVsdC1yb2xlcy13b2dnaW9uaS5uZXQiLCJ0ZXN0LXJvbGUiLCJqY2hhdCIsIm9mZmxpbmVfYWNjZXNzIiwiamVua2lucyIsInVtYV9hdXRob3JpemF0aW9uIiwianBhY3JlcG8iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwic2lkIjoiYmFiYTBmYWYtNWViMC00MGE2LTg4ZmYtYjAyNjMyNWRiOGI5IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJXYWx0ZXIgT2dnaW9uaSIsInByZWZlcnJlZF91c2VybmFtZSI6IndvZ2dpb25pIiwiZ2l2ZW5fbmFtZSI6IldhbHRlciIsImZhbWlseV9uYW1lIjoiT2dnaW9uaSIsImVtYWlsIjoib2dnaW9uaS53YWx0ZXJAZ21haWwuY29tIn0.YOLAaRlcW1tNeVDa4Uq_2PSJCG4huwjVuSDbZb6xapn8oIYw2phZ4R3dCR7gxRR76_xnJeitFlxMj2M_HazzbY761hhv9H3yM0f7SqgQNoGAQr4vDsKMzeLubYVX1wk77D3n8uAA_aMv1tBq8Rmkno9uDvNaofCh2Py1-zuaiSHNygnIhYYIeqU1uwORA05FVU5vcgj4bWLioH_v_5AGyTdQvP4ZWmK0MIRpAOhQd43WgBm3nrPAT0qbrT9X1yIkR-dvrN4YFVvGcscVGsZNkBN4Im4rbrl8SE3Ow5Q1-imuQhg2jtWCATjQK8IqPh8DFMD8lXTVZZnS9GgF_5Jtyw \ No newline at end of file diff --git a/tests/jwks.json b/tests/jwks.json new file mode 100644 index 0000000..02382f2 --- /dev/null +++ b/tests/jwks.json @@ -0,0 +1 @@ +{"keys":[{"kid":"PTj6Jw4JQn9UbZwv0macMf9KtkpjmzYvG8Hm2qQzuWw","kty":"EC","alg":"ES512","use":"sig","crv":"P-521","x":"AfpT-4wcXIA_AxFnQur9N_vkBf3sR6WQmyN0ePAQ1SHYJWcJk462HJBkpcLP6eHec2SYzAm21HcBK7uJsjX1-loS","y":"ARz7Fo9z8nDhroUaNoOpI1nwHU6s6PdJt131i2SUzOCvayiGHEG5rYlr_TDz1x2d-zEGGpKgfFEuUbJjFIuqCk2X"},{"kid":"81n4gi8JmGT3g7zwbzJrel2mKcAouDSUZaGx8ovCf00","kty":"RSA","alg":"RS256","use":"sig","n":"rj0pE8WO3Oh53FRHkrQwu1lqUsnN0UKSr1YFmzHAgIdx4lrQR4SGFoF0rZswKhbmWCLxAOGl44mo-WUkY2068yy3Py8-uY9k3So6BtL5yWEQI-Wko7qUgG3qvI99CLaXb8GiwuxWBcNkIc-5oCNgg7_ITHs2tSXzHJaVPj76FFfyWXTEgeIuz_UIWfyqMbDWN-gsFIsMHIMSs9hJGz4B1uu5hww3wHiT86E7j1HrquJYr-vd-i1M5Q3_GgZk5p_iXfVBD3RKkuUpbIt7SC4FhVicLWA01fF76plYyFGYRKxonF5X3rvHrPw6v-tUvyWhS3foG3TCJYE3aBm7n3Bvpw","e":"AQAB","x5c":["MIICpzCCAY8CBgGFemD2SjANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwHhcNMjMwMTA0MDEyMjMzWhcNMzMwMTA0MDEyNDEzWjAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuPSkTxY7c6HncVEeStDC7WWpSyc3RQpKvVgWbMcCAh3HiWtBHhIYWgXStmzAqFuZYIvEA4aXjiaj5ZSRjbTrzLLc/Lz65j2TdKjoG0vnJYRAj5aSjupSAbeq8j30ItpdvwaLC7FYFw2Qhz7mgI2CDv8hMeza1JfMclpU+PvoUV/JZdMSB4i7P9QhZ/KoxsNY36CwUiwwcgxKz2EkbPgHW67mHDDfAeJPzoTuPUeuq4liv6936LUzlDf8aBmTmn+Jd9UEPdEqS5Slsi3tILgWFWJwtYDTV8XvqmVjIUZhErGicXlfeu8es/Dq/61S/JaFLd+gbdMIlgTdoGbufcG+nAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEkwgZM11AY7KenIsNxl9hDT7pzWe32IEox7LkrqfZnW+xtEH7JaTb09vyfoQGeiirGvgGDmDazSqilBjB/c9+mG8BxDccdI+xK7cF5PdCJkAcSc3tZIkObNBcHVK/OzNF0mUk8bSRQlviANr66wQK+q/BhOBhjeJ6t88WxgYvZ9k8kE9Px53WKmV6Tiu802cERQJ2b2GOuHBG2xT7XASTW4e2tH3G860dqOJrL9pT9/TEQy+9JZ2qPNS7sBRjQVHSJK8PTPiEbGPXFZXwmWXIIkidAScfHb+mLBR0NOQiEEeaUarINCElE+nQtLsn+9c1bbW/WC111GQALsY3RKO+g="],"x5t":"xKOwEtrzggHOcv0i_jKCGKmqwrs","x5t#S256":"IM26XWYVm5YA0dRB9Ib5cmBFxu0g93g1G_MtAlmS2cY"},{"kid":"54sZtgDpS3hpWGcrgQrGP2tkfuypOrRZ0TdfldMArYw","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"vS33BggvOpUd03OsexWpaumnoYq6WPNCTD2tXwaJSVHh1g0O18gKWG-NxD_gxe5hYLieIXcDqEtKS402kYo3Vcr2Rfv8m7ARDoO95ezzCcgXJZCGSP7kSUAQAc4KUY37FjrHTKCBqf94VfScG95BBQSAilscVQNntqMd7JAtMZrZCGsjeRzYH6UVg5r11T7bD1foCBKlNRSYXJtlxFi-Y5WPjtjeAXQ1rnn8oPEWda6mOjaccqBS-RdzZtMldPcXdX_OCs3Cd_hvaRsutZeYUo8Xtt56g1N9coZPwEOtrcQZhXUmcaHUvmVMeYBC-CSIWxW0nQMI4VVuAtQPJDNHCQ","e":"AQAB","x5c":["MIICpzCCAY8CBgGFemD3mDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwHhcNMjMwMTA0MDEyMjMzWhcNMzMwMTA0MDEyNDEzWjAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9LfcGCC86lR3Tc6x7Falq6aehirpY80JMPa1fBolJUeHWDQ7XyApYb43EP+DF7mFguJ4hdwOoS0pLjTaRijdVyvZF+/ybsBEOg73l7PMJyBclkIZI/uRJQBABzgpRjfsWOsdMoIGp/3hV9Jwb3kEFBICKWxxVA2e2ox3skC0xmtkIayN5HNgfpRWDmvXVPtsPV+gIEqU1FJhcm2XEWL5jlY+O2N4BdDWuefyg8RZ1rqY6NpxyoFL5F3Nm0yV09xd1f84KzcJ3+G9pGy61l5hSjxe23nqDU31yhk/AQ62txBmFdSZxodS+ZUx5gEL4JIhbFbSdAwjhVW4C1A8kM0cJAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALfhHfHLVMSrKbZlqj3HOzS8YhMnBw8h7qd21jhC+rx7Dh5N7UusRjz+sTbuExr7gYyL/j6zKzbie3R7I0TQ/VV+Pb8513VdsDN+eNP0Hk2p8rkLldgv+HvFRq8zDiAqfr4YSHmwEc+zeYBxaVyLbxK3p4ywsdRE/IMJ7W7E8GM3PB8P3W86JSS/ZW6TzseSKZklg0USMUY2IoMQxGZRX1K4UT1etvzvKXuAkyu+BBkXj2WHgmE64ZB3Qttv1XVivdHbuta+/U654t+57/iUHBiIQsxGrk5L7wwxCs5WKTqZzTTxTt328GD3U29G6AK84hWrnGfMURkZxHo8ZqvWHVM="],"x5t":"ZlXuozF3f6j2fSPzwB0iLNzJosY","x5t#S256":"iA0_j0ix9p1u5vlG3j1smMVKz7AQwWvypoUcJNJfTQ0"},{"kid":"SOIHFtFtSUrL1zn6IytQJX5Ls2FoAbS75mCUv5X7HY4","kty":"EC","alg":"ES256","use":"sig","crv":"P-256","x":"T01OQ6-HYegpiaVi-G2Umgi75z5cubuJZNrptPGuqdA","y":"hbmd9n6o-6RhxsmEUyykBJVdn1HmHwx8z7xx91OGqj0"},{"kid":"CxMLvSC16KJme1zTb3pUCPBcnNyaL-_ttS1lOiVvqac","kty":"RSA","alg":"PS512","use":"sig","n":"ragrqqAm-xRhOD0EmaGANRtGOJ6hvdhCold1q14knFr8cluEKLNJDp1EpRhjy-gvggVgKyfM0VXjjnpceEBVxeToy8BUgvO70hyjkmBY1G-LvZt2ug2QW8V5q0Y9_jAJg5NhGTnnS7k_vGnaAy4fyjIwYdlJQnxzaD0w6x1WTEEbDc1hAuEEmmsjRVTNAwlvQLKq0lWQpPjeh9AL1PwYCOq9n05WMLNF6O5krXXAV9uVgqjgdNL7tMqk2QeVYI1IkEVDFvdQHK1ousswe2fvZhXe-Px2-FjE6DadRXLZdEsSmNJ7sk3gOdX3xpgq3Wt9nDp-KFgZQRd6xU-Ssg4UUQ","e":"AQAB","x5c":["MIICpzCCAY8CBgGOBHKEOjANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwHhcNMjQwMzAzMTMxMTM3WhcNMzQwMzAzMTMxMzE3WjAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtqCuqoCb7FGE4PQSZoYA1G0Y4nqG92EKiV3WrXiScWvxyW4Qos0kOnUSlGGPL6C+CBWArJ8zRVeOOelx4QFXF5OjLwFSC87vSHKOSYFjUb4u9m3a6DZBbxXmrRj3+MAmDk2EZOedLuT+8adoDLh/KMjBh2UlCfHNoPTDrHVZMQRsNzWEC4QSaayNFVM0DCW9AsqrSVZCk+N6H0AvU/BgI6r2fTlYws0Xo7mStdcBX25WCqOB00vu0yqTZB5VgjUiQRUMW91AcrWi6yzB7Z+9mFd74/Hb4WMToNp1Fctl0SxKY0nuyTeA51ffGmCrda32cOn4oWBlBF3rFT5KyDhRRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAECCU04aKND42xVnItvyl2C31yt5yIDIvObNw/clBo07a4IIJmwHxE3w4lIBRo+JdJpAzUsWzfeduKGyxeNj09/L3eW4zAFAzlo/lZCotTEh+8T4cprkb8uCiw5CfwDXEJ7aTxxtpKA4Lz22ftUQdwlBrNn/1xeUUV6SU8QHwekvDlh74YC3baIqtQcDxFSdbR6scW+SLPj/63gWupcM0TK0bpgEhlaZmSMueczb6XR17MVOh37eugQ3xrD3dgxgxRCCVWEdhbFXwxWe9TK8/AMeTTuB2/TEVK0u4QcF5zHrHafByXphtA0jRXCUufe/ywgNGMriDqyHrUlEe2g9xxM="],"x5t":"F_Rc44m2zTxJYA8q4BFybLr4Vn4","x5t#S256":"rr9vn5VxR8T_zWwSbTfwj7lC3bbRZ9-wL-XtXAiE2YA"},{"kid":"wYJdIXKYQxtiYtPvI6FrkbZVWl_4HDElU8FPHPwF_k4","kty":"RSA","alg":"RS512","use":"sig","n":"qIv3sk46zb8Z6V045RP1QjaQTfrsHT46Fmhk9Pq9Wa6Y_ZBpXRYVNdIfSlwK3npSz0BHzPxKJEvuqL3DdguTV6a5XJCkv0kH4r4Qutr0k7OojZGOBI67ZXEwf1T7kIkXMvzJ527HQLlnWMQXLKtOFGjYA_gSvU_XDkwAO2lSN-V2KwrZuV1pkcg5eJnSzB7vK2adO-3IX2kHMhcCvcGiougRDbDdO7x32ic3wwRW3cQtnz8hiALu1U_BgfygkhriUxXgWIsT5Yo55D81Yb_xulVtqV1FS9tbm-Q4PLw_ZZXTzTsugzAhuu7bDnDrl1qfagb2_Xohu6EfDp9JEvBs4Q","e":"AQAB","x5c":["MIICpzCCAY8CBgGFtDbO4TANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwHhcNMjMwMTE1MDY1NDI4WhcNMzMwMTE1MDY1NjA4WjAXMRUwEwYDVQQDDAx3b2dnaW9uaS5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoi/eyTjrNvxnpXTjlE/VCNpBN+uwdPjoWaGT0+r1Zrpj9kGldFhU10h9KXAreelLPQEfM/EokS+6ovcN2C5NXprlckKS/SQfivhC62vSTs6iNkY4EjrtlcTB/VPuQiRcy/MnnbsdAuWdYxBcsq04UaNgD+BK9T9cOTAA7aVI35XYrCtm5XWmRyDl4mdLMHu8rZp077chfaQcyFwK9waKi6BENsN07vHfaJzfDBFbdxC2fPyGIAu7VT8GB/KCSGuJTFeBYixPlijnkPzVhv/G6VW2pXUVL21ub5Dg8vD9lldPNOy6DMCG67tsOcOuXWp9qBvb9eiG7oR8On0kS8GzhAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHtyt52caOt/vn2YSI2FUZUvKGu/CYrr21M7WWIRG+dkqHDXHMH3LCFnNxaLMaqeEa9+mvSBxNAcRGi5Eo9vyz1VNOkCBNfc3wH4KwK+kJGEgVzJehl9DFTAtXRboFINHkVWa5Xz6o+XpYgPKLi3axl4I5bH5wk4SUjAg2nlSBJCpSyM+fgJ0KFwgrcDfQvLXWWzc9LXSFm0Jgu9Qaj3ST5Wt82sbZIX7THFZ493lx03XP4gpH6SM7IDPNc2mFhtpz240AZ2HtOgirJXbS+Jksjd7LKmI/HNwRHVIwOYldI+hCjKmqYmeJwR6VQZjLbtL3sP9i7efnQ4GANyObve/is="],"x5t":"yPeuQ53KWoLVI5DRUzkpmDH8mHw","x5t#S256":"7XaAdB30AaoRIwTDt1cC5oPCXwN8l5ZCRsnjY3zh-1w"}]} \ No newline at end of file diff --git a/tests/test_token.py b/tests/test_token.py new file mode 100644 index 0000000..22e49e7 --- /dev/null +++ b/tests/test_token.py @@ -0,0 +1,27 @@ +import unittest +import tempfile +from jwcrypto.jwt import JWT, JWTExpired + +from src.jwt_cli.main import main +from asyncio.runners import run +from pwo import async_test, tmpdir +from pathlib import Path +import sys +import json + + +class TokenTest(unittest.TestCase): + @tmpdir + def test_parse_token(self, temp_dir): + result = temp_dir / 'output.json' + + with self.assertRaises(JWTExpired) as _: + main(['token', 'parse', '-i', 'auth_token.txt', + '-o', str(result), + '--keys', 'file:jwks.json']) + main(['token', 'parse', '-i', 'auth_token.txt', + '-o', str(result)]) + with open(result, 'r') as infile: + # print(infile.read()) + jwt = json.load(infile) + json.dump(jwt, sys.stdout, indent=4)