initial commit
This commit is contained in:
40
pyproject.toml
Normal file
40
pyproject.toml
Normal file
@@ -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
|
14
src/jwt_cli.egg-info/PKG-INFO
Normal file
14
src/jwt_cli.egg-info/PKG-INFO
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: jwt-cli
|
||||||
|
Version: 0.0.1
|
||||||
|
Summary: JWT command line utilities
|
||||||
|
Author-email: Walter Oggioni <oggioni.walter@gmail.com>
|
||||||
|
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
|
12
src/jwt_cli.egg-info/SOURCES.txt
Normal file
12
src/jwt_cli.egg-info/SOURCES.txt
Normal file
@@ -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
|
1
src/jwt_cli.egg-info/dependency_links.txt
Normal file
1
src/jwt_cli.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
2
src/jwt_cli.egg-info/entry_points.txt
Normal file
2
src/jwt_cli.egg-info/entry_points.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
jwt = jwt_cli:main
|
2
src/jwt_cli.egg-info/requires.txt
Normal file
2
src/jwt_cli.egg-info/requires.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
jwcrypto>=1.5.6
|
||||||
|
typing_extensions==4.7.1
|
1
src/jwt_cli.egg-info/top_level.txt
Normal file
1
src/jwt_cli.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
jwt_cli
|
1
src/jwt_cli/__init__.py
Normal file
1
src/jwt_cli/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .main import main
|
BIN
src/jwt_cli/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
src/jwt_cli/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/jwt_cli/__pycache__/create_key_command.cpython-312.pyc
Normal file
BIN
src/jwt_cli/__pycache__/create_key_command.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/jwt_cli/__pycache__/key.cpython-312.pyc
Normal file
BIN
src/jwt_cli/__pycache__/key.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/jwt_cli/__pycache__/key_command.cpython-312.pyc
Normal file
BIN
src/jwt_cli/__pycache__/key_command.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/jwt_cli/__pycache__/main.cpython-312.pyc
Normal file
BIN
src/jwt_cli/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/jwt_cli/__pycache__/token.cpython-312.pyc
Normal file
BIN
src/jwt_cli/__pycache__/token.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/jwt_cli/__pycache__/token_command.cpython-312.pyc
Normal file
BIN
src/jwt_cli/__pycache__/token_command.cpython-312.pyc
Normal file
Binary file not shown.
148
src/jwt_cli/create_key_command.py
Normal file
148
src/jwt_cli/create_key_command.py
Normal file
@@ -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
|
85
src/jwt_cli/key.py
Normal file
85
src/jwt_cli/key.py
Normal file
@@ -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)
|
62
src/jwt_cli/key_command.py
Normal file
62
src/jwt_cli/key_command.py
Normal file
@@ -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
|
15
src/jwt_cli/main.py
Normal file
15
src/jwt_cli/main.py
Normal file
@@ -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))
|
48
src/jwt_cli/sign_command.py
Normal file
48
src/jwt_cli/sign_command.py
Normal file
@@ -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
|
0
src/jwt_cli/signature.py
Normal file
0
src/jwt_cli/signature.py
Normal file
82
src/jwt_cli/token.py
Normal file
82
src/jwt_cli/token.py
Normal file
@@ -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']
|
42
src/jwt_cli/token_command.py
Normal file
42
src/jwt_cli/token_command.py
Normal file
@@ -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
|
BIN
tests/__pycache__/test_token.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_token.cpython-312.pyc
Normal file
Binary file not shown.
1
tests/auth_token.txt
Normal file
1
tests/auth_token.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4MW40Z2k4Sm1HVDNnN3p3YnpKcmVsMm1LY0FvdURTVVphR3g4b3ZDZjAwIn0.eyJleHAiOjE3MTkxMTk3NTUsImlhdCI6MTcxOTExOTI3NSwiYXV0aF90aW1lIjoxNzE5MTE4ODgyLCJqdGkiOiI3NmY1NmY1Mi1mODBjLTQxZmUtODhhMS03YzQyNzM0OTUzYzgiLCJpc3MiOiJodHRwczovL3dvZ2dpb25pLm5ldC9hdXRoL3JlYWxtcy93b2dnaW9uaS5uZXQiLCJhdWQiOlsianBhY3JlcG8iLCJhY2NvdW50Il0sInN1YiI6ImIxYjAyZDY0LTZmYzctNDdkMC1iNGU5LTNjMTY3YzQ3Mzc4ZCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImpwYWNyZXBvLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiJiYWJhMGZhZi01ZWIwLTQwYTYtODhmZi1iMDI2MzI1ZGI4YjkiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm5leHRjbG91ZCIsImdpdGVhIiwiZGVmYXVsdC1yb2xlcy13b2dnaW9uaS5uZXQiLCJ0ZXN0LXJvbGUiLCJqY2hhdCIsIm9mZmxpbmVfYWNjZXNzIiwiamVua2lucyIsInVtYV9hdXRob3JpemF0aW9uIiwianBhY3JlcG8iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwic2lkIjoiYmFiYTBmYWYtNWViMC00MGE2LTg4ZmYtYjAyNjMyNWRiOGI5IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJXYWx0ZXIgT2dnaW9uaSIsInByZWZlcnJlZF91c2VybmFtZSI6IndvZ2dpb25pIiwiZ2l2ZW5fbmFtZSI6IldhbHRlciIsImZhbWlseV9uYW1lIjoiT2dnaW9uaSIsImVtYWlsIjoib2dnaW9uaS53YWx0ZXJAZ21haWwuY29tIn0.YOLAaRlcW1tNeVDa4Uq_2PSJCG4huwjVuSDbZb6xapn8oIYw2phZ4R3dCR7gxRR76_xnJeitFlxMj2M_HazzbY761hhv9H3yM0f7SqgQNoGAQr4vDsKMzeLubYVX1wk77D3n8uAA_aMv1tBq8Rmkno9uDvNaofCh2Py1-zuaiSHNygnIhYYIeqU1uwORA05FVU5vcgj4bWLioH_v_5AGyTdQvP4ZWmK0MIRpAOhQd43WgBm3nrPAT0qbrT9X1yIkR-dvrN4YFVvGcscVGsZNkBN4Im4rbrl8SE3Ow5Q1-imuQhg2jtWCATjQK8IqPh8DFMD8lXTVZZnS9GgF_5Jtyw
|
1
tests/jwks.json
Normal file
1
tests/jwks.json
Normal file
File diff suppressed because one or more lines are too long
27
tests/test_token.py
Normal file
27
tests/test_token.py
Normal file
@@ -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)
|
Reference in New Issue
Block a user