reshut 0.0.3__py3-none-any.whl
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.
- reshut/__init__.py +11 -0
- reshut/__main__.py +113 -0
- reshut/authorization.py +85 -0
- reshut/middleware/AsgiAuthorizationMiddleware.py +42 -0
- reshut/middleware/AuthorizationEvaluator.py +80 -0
- reshut/middleware/TokenEvaluator.py +76 -0
- reshut/middleware/WsgiAuthorizationMiddleware.py +42 -0
- reshut/middleware/__init__.py +15 -0
- reshut/py.typed +0 -0
- reshut/utils.py +172 -0
- reshut-0.0.3.dist-info/METADATA +191 -0
- reshut-0.0.3.dist-info/RECORD +15 -0
- reshut-0.0.3.dist-info/WHEEL +5 -0
- reshut-0.0.3.dist-info/licenses/LICENSE +21 -0
- reshut-0.0.3.dist-info/top_level.txt +1 -0
reshut/__init__.py
ADDED
reshut/__main__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .utils import Algorithm, keygen, tokenize, validate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __write_key_files(basename:str, prikey:str, pubkey:Optional[str]) -> None:
|
|
14
|
+
base_path = Path(basename)
|
|
15
|
+
if pubkey is None:
|
|
16
|
+
# shared secret
|
|
17
|
+
out_path = base_path.with_suffix('.b64')
|
|
18
|
+
out_path.write_text(prikey, encoding='utf-8')
|
|
19
|
+
print(f'Wrote secret key to {out_path}')
|
|
20
|
+
else:
|
|
21
|
+
# keypair
|
|
22
|
+
prikey_path = base_path.with_name(f'{base_path.name}_prikey.pem')
|
|
23
|
+
pubkey_path = base_path.with_name(f'{base_path.name}_pubkey.pem')
|
|
24
|
+
prikey_path.write_text(prikey, encoding='utf-8')
|
|
25
|
+
print(f'Wrote private key to {prikey_path}')
|
|
26
|
+
pubkey_path.write_text(pubkey, encoding='utf-8')
|
|
27
|
+
print(f'Wrote public key to {pubkey_path}')
|
|
28
|
+
|
|
29
|
+
def __cmd_keygen(args: argparse.Namespace) -> None:
|
|
30
|
+
try:
|
|
31
|
+
alg = Algorithm(str(args.type).upper())
|
|
32
|
+
except ValueError as exc:
|
|
33
|
+
sys.stderr.write(f'Unsupported algorithm: {args.type}\n')
|
|
34
|
+
raise SystemExit(2) from exc
|
|
35
|
+
|
|
36
|
+
prikey, pubkey = keygen(alg) # type: ignore[arg-type]
|
|
37
|
+
__write_key_files(args.output, prikey, pubkey)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __read_key_file(path: Path) -> str:
|
|
41
|
+
try:
|
|
42
|
+
return path.read_text(encoding='utf-8').strip()
|
|
43
|
+
except OSError as exc:
|
|
44
|
+
sys.stderr.write(f'Unable to read key file {path}: {exc}\n')
|
|
45
|
+
raise SystemExit(3) from exc
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __cmd_tokenize(args: argparse.Namespace) -> None:
|
|
49
|
+
try:
|
|
50
|
+
alg = Algorithm(str(args.type).upper())
|
|
51
|
+
except ValueError as exc:
|
|
52
|
+
sys.stderr.write(f'Unsupported algorithm: {args.type}\n')
|
|
53
|
+
raise SystemExit(4) from exc
|
|
54
|
+
try:
|
|
55
|
+
claims = json.loads(args.claims)
|
|
56
|
+
if not isinstance(claims, dict):
|
|
57
|
+
raise TypeError
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
sys.stderr.write('Claims must be a JSON object.\n')
|
|
60
|
+
raise SystemExit(5) from exc
|
|
61
|
+
private_key = __read_key_file(Path(args.key))
|
|
62
|
+
token = tokenize(alg, private_key, claims)
|
|
63
|
+
print(token)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __cmd_validate(args: argparse.Namespace) -> None:
|
|
67
|
+
try:
|
|
68
|
+
alg = Algorithm(str(args.type).upper())
|
|
69
|
+
except ValueError as exc:
|
|
70
|
+
sys.stderr.write(f'Unsupported algorithm: {args.type}\n')
|
|
71
|
+
raise SystemExit(6) from exc
|
|
72
|
+
public_key = __read_key_file(Path(args.key))
|
|
73
|
+
try:
|
|
74
|
+
claims = validate(alg, public_key, args.token)
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
sys.stderr.write(f'Validation failed: {exc}\n')
|
|
77
|
+
raise SystemExit(7) from exc
|
|
78
|
+
|
|
79
|
+
print(json.dumps(claims, indent=2, sort_keys=True))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(argv:list[str]) -> None:
|
|
83
|
+
parser = argparse.ArgumentParser(
|
|
84
|
+
prog='python -m reshut',
|
|
85
|
+
description='Utility for generating keys, creating and validating JWTs.'
|
|
86
|
+
)
|
|
87
|
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
|
88
|
+
|
|
89
|
+
# reshut-keygen
|
|
90
|
+
reshut_keygen = subparsers.add_parser('keygen', help='Generate a secret or key‑pair.')
|
|
91
|
+
reshut_keygen.add_argument('--type', required=True, help='Algorithm name (e.g. HS256, RS256).')
|
|
92
|
+
reshut_keygen.add_argument('--output', required=True, help='Base filename (prefix) for generated key files.')
|
|
93
|
+
reshut_keygen.set_defaults(func=__cmd_keygen)
|
|
94
|
+
|
|
95
|
+
# reshut-tokenize
|
|
96
|
+
reshut_tokenize = subparsers.add_parser('tokenize', help='Create a JWT from claims.')
|
|
97
|
+
reshut_tokenize.add_argument('--type', required=True, help='Algorithm name.')
|
|
98
|
+
reshut_tokenize.add_argument('--claims', required=True, help='JSON string representing the claim set.')
|
|
99
|
+
reshut_tokenize.add_argument('--key', required=True, help='Path to the private key / secret file.')
|
|
100
|
+
reshut_tokenize.set_defaults(func=__cmd_tokenize)
|
|
101
|
+
|
|
102
|
+
# reshut-validate
|
|
103
|
+
reshut_validate = subparsers.add_parser('validate', help='Validate a JWT.')
|
|
104
|
+
reshut_validate.add_argument('--type', required=True, help='Algorithm name.')
|
|
105
|
+
reshut_validate.add_argument('--token', required=True, help='JWT string to validate.')
|
|
106
|
+
reshut_validate.add_argument('--key', required=True, help='Path to the public key / secret file.')
|
|
107
|
+
reshut_validate.set_defaults(func=__cmd_validate)
|
|
108
|
+
|
|
109
|
+
args = parser.parse_args(argv)
|
|
110
|
+
args.func(args) # type: ignore[attr-defined]
|
|
111
|
+
|
|
112
|
+
if __name__ == '__main__':
|
|
113
|
+
main(sys.argv[1:])
|
reshut/authorization.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any, Callable, Optional, TypeAlias, cast
|
|
6
|
+
|
|
7
|
+
ClaimEvaluator:TypeAlias = Callable[[Any], bool]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def allow_anonymous(func:Callable[..., Any]) -> Callable[..., Any]:
|
|
11
|
+
"""
|
|
12
|
+
Indicates that a handler does not require Authorization.
|
|
13
|
+
|
|
14
|
+
:param func: The handler function.
|
|
15
|
+
:return: The handler function (not wrapped.)
|
|
16
|
+
"""
|
|
17
|
+
org = inspect.unwrap(func)
|
|
18
|
+
setattr(org, '__reshut_noauth', True)
|
|
19
|
+
return func
|
|
20
|
+
|
|
21
|
+
def allow_claim(func:Callable[..., Any], claim_name:str, claim_check:Optional[Any|ClaimEvaluator] = None, is_required:bool = False) -> Callable[..., Any]:
|
|
22
|
+
"""
|
|
23
|
+
Adds an ALLOW claim rule to a handler.
|
|
24
|
+
|
|
25
|
+
When at least one ALLOW claim rule is defined on a handler, then access is denied if at least one of the allowed claims is not presented.
|
|
26
|
+
|
|
27
|
+
:param func: The handler function.
|
|
28
|
+
:param claim_name: Claim name.
|
|
29
|
+
:param claim_check: Optional literal value that the claim must present, or a ``ClaimEvaluator`` that checks the claim is a match.
|
|
30
|
+
:param is_required: Optional boolean indicating that the claim is required, forming a "REQUIRED claim rule".
|
|
31
|
+
:return: The handler function (not wrapped.)
|
|
32
|
+
"""
|
|
33
|
+
bag_name = '__reshut_require' if is_required else '__reshut_allow'
|
|
34
|
+
org = inspect.unwrap(func)
|
|
35
|
+
if not hasattr(org, bag_name):
|
|
36
|
+
setattr(org, bag_name, dict[str,Any]({
|
|
37
|
+
claim_name: claim_check
|
|
38
|
+
}))
|
|
39
|
+
else:
|
|
40
|
+
allow_list = cast(dict[str,Any],getattr(org, bag_name))
|
|
41
|
+
allow_list[claim_name] = claim_check
|
|
42
|
+
return func
|
|
43
|
+
|
|
44
|
+
def deny_claim(func:Callable[..., Any], claim_name:str, claim_check:Optional[Any|ClaimEvaluator] = None) -> Callable[..., Any]:
|
|
45
|
+
"""
|
|
46
|
+
Adds a DENY claim rule to a handler.
|
|
47
|
+
|
|
48
|
+
When any presented claim matches a DENY claim rule, then access is denied.
|
|
49
|
+
|
|
50
|
+
:param func: The handler function.
|
|
51
|
+
:param claim_name: Claim name.
|
|
52
|
+
:param claim_check: Optional literal value that the claim must NOT present, or a ``ClaimEvaluator`` that checks the claim is a match.
|
|
53
|
+
:return: The handler function (not wrapped.)
|
|
54
|
+
"""
|
|
55
|
+
org = inspect.unwrap(func)
|
|
56
|
+
if not hasattr(org, '__reshut_deny'):
|
|
57
|
+
setattr(org, '__reshut_deny', {
|
|
58
|
+
claim_name: claim_check
|
|
59
|
+
})
|
|
60
|
+
else:
|
|
61
|
+
deny_list = cast(dict[str,Any],getattr(org, '__reshut_deny'))
|
|
62
|
+
deny_list[claim_name] = claim_check
|
|
63
|
+
return func
|
|
64
|
+
|
|
65
|
+
def require_claim(func:Callable[..., Any], claim_name:str, claim_check:Optional[Any|ClaimEvaluator] = None) -> Callable[..., Any]:
|
|
66
|
+
"""
|
|
67
|
+
Adds a REQUIRED claim rule to a handler.
|
|
68
|
+
|
|
69
|
+
When all required claims are presented, then access is granted.
|
|
70
|
+
|
|
71
|
+
:param func: The handler function.
|
|
72
|
+
:param claim_name: Claim name.
|
|
73
|
+
:param claim_check: Optional literal value that the claim must present, or a ``ClaimEvaluator`` that checks the claim is a match.
|
|
74
|
+
:return: The handler function (not wrapped.)
|
|
75
|
+
"""
|
|
76
|
+
return allow_claim(func, claim_name, claim_check, True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
'ClaimEvaluator',
|
|
81
|
+
'allow_anonymous',
|
|
82
|
+
'allow_claim',
|
|
83
|
+
'deny_claim',
|
|
84
|
+
'require_claim'
|
|
85
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import falcon
|
|
5
|
+
from falcon._typing import AsgiMiddlewareWithProcessResource
|
|
6
|
+
import inspect
|
|
7
|
+
from typing import Any, Mapping, Optional, cast
|
|
8
|
+
from .AuthorizationEvaluator import AuthorizationEvaluator
|
|
9
|
+
from .TokenEvaluator import TokenEvaluator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsgiAuthorizationMiddleware(AsgiMiddlewareWithProcessResource):
|
|
13
|
+
"""
|
|
14
|
+
ASGI-compatible Authorization Middleware
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
Intercepts resource requests to apply Authorization logic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__authorization_evaluator:AuthorizationEvaluator
|
|
21
|
+
|
|
22
|
+
def __init__(self, apikey_token_evaluator:Optional[TokenEvaluator], basic_token_evaluator:Optional[TokenEvaluator], bearer_token_evaluator:Optional[TokenEvaluator]) -> None:
|
|
23
|
+
self.__authorization_evaluator = AuthorizationEvaluator(apikey_token_evaluator=apikey_token_evaluator, basic_token_evaluator=basic_token_evaluator, bearer_token_evaluator=bearer_token_evaluator)
|
|
24
|
+
|
|
25
|
+
async def process_resource(self, req:falcon.Request, resp:falcon.Response, resource:object, params: Mapping[str, Any]) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Intercept for ``process_resource`` that evaluates authorization data in the request against authorization requirements of the resource handler.
|
|
28
|
+
"""
|
|
29
|
+
handler_name = f'on_{req.method.lower()}'
|
|
30
|
+
handler = getattr(resource, handler_name, None)
|
|
31
|
+
if handler is None:
|
|
32
|
+
return
|
|
33
|
+
handler = inspect.unwrap(cast(Any, handler))
|
|
34
|
+
if getattr(handler, '__reshut_noauth', False):
|
|
35
|
+
# access granted
|
|
36
|
+
return
|
|
37
|
+
self.__authorization_evaluator.evaluate(req, handler)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
'AsgiAuthorizationMiddleware'
|
|
42
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import falcon
|
|
5
|
+
from typing import Any, Optional, cast
|
|
6
|
+
from ..authorization import ClaimEvaluator
|
|
7
|
+
from .TokenEvaluator import TokenEvaluator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthorizationEvaluator:
|
|
11
|
+
"""
|
|
12
|
+
Evaluates the authorization data in a request against the authorization requirements of a resource handler.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__apikey_token_evaluator:Optional[TokenEvaluator]
|
|
16
|
+
__basic_token_evaluator:Optional[TokenEvaluator]
|
|
17
|
+
__bearer_token_evaluator:Optional[TokenEvaluator]
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
apikey_token_evaluator:Optional[TokenEvaluator],
|
|
22
|
+
basic_token_evaluator:Optional[TokenEvaluator],
|
|
23
|
+
bearer_token_evaluator:Optional[TokenEvaluator]
|
|
24
|
+
) -> None:
|
|
25
|
+
self.__apikey_token_evaluator = apikey_token_evaluator
|
|
26
|
+
self.__basic_token_evaluator = basic_token_evaluator
|
|
27
|
+
self.__bearer_token_evaluator = bearer_token_evaluator
|
|
28
|
+
|
|
29
|
+
def evaluate(self, req:falcon.Request, handler:Any) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Evaluate authorization data in ``req`` against authorization requirements of ``handler``.
|
|
32
|
+
|
|
33
|
+
:raises falcon.HTTPBadRequest: When the request has missing or invalid Authorization data.
|
|
34
|
+
:raises falcon.HTTPUnauthorized: When claim rule checks fail.
|
|
35
|
+
"""
|
|
36
|
+
scheme:str|None = None
|
|
37
|
+
token:str|None = None
|
|
38
|
+
deny_claims = {} if not hasattr(handler, '__reshut_deny') else cast(dict[str,str|ClaimEvaluator], getattr(handler, '__reshut_deny'))
|
|
39
|
+
allow_claims = {} if not hasattr(handler, '__reshut_allow') else cast(dict[str,str|ClaimEvaluator], getattr(handler, '__reshut_allow'))
|
|
40
|
+
require_claims = {} if not hasattr(handler, '__reshut_require') else cast(dict[str,str|ClaimEvaluator], getattr(handler, '__reshut_require'))
|
|
41
|
+
# check for Authorization header
|
|
42
|
+
authorization = req.get_header('Authorization', False, None)
|
|
43
|
+
if authorization is not None:
|
|
44
|
+
scheme, token = authorization.split(' ', 1)
|
|
45
|
+
scheme = scheme.lower()
|
|
46
|
+
else:
|
|
47
|
+
# check for X-API-Key header
|
|
48
|
+
token = req.get_header('X-API-Key', False, None)
|
|
49
|
+
if token is not None and len(token) > 0:
|
|
50
|
+
scheme = 'apikey'
|
|
51
|
+
if scheme is None or token is None:
|
|
52
|
+
raise falcon.HTTPBadRequest(
|
|
53
|
+
title='Authorization Required',
|
|
54
|
+
description=f'Missing Authorization',
|
|
55
|
+
)
|
|
56
|
+
match scheme:
|
|
57
|
+
case 'apikey':
|
|
58
|
+
if self.__apikey_token_evaluator is not None and self.__apikey_token_evaluator.evaluate(token, deny_claims, allow_claims, require_claims):
|
|
59
|
+
return
|
|
60
|
+
case 'basic':
|
|
61
|
+
if self.__basic_token_evaluator is not None and self.__basic_token_evaluator.evaluate(token, deny_claims, allow_claims, require_claims):
|
|
62
|
+
return
|
|
63
|
+
case 'bearer':
|
|
64
|
+
if self.__bearer_token_evaluator is not None and self.__bearer_token_evaluator.evaluate(token, deny_claims, allow_claims, require_claims):
|
|
65
|
+
return
|
|
66
|
+
case _:
|
|
67
|
+
raise falcon.HTTPBadRequest(
|
|
68
|
+
title='Authorization Unsupported',
|
|
69
|
+
description=f'Scheme "{scheme}" not supported.',
|
|
70
|
+
)
|
|
71
|
+
# reject.
|
|
72
|
+
raise falcon.HTTPUnauthorized(
|
|
73
|
+
title='Authorization Failed',
|
|
74
|
+
description='Unsuccessful',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
'AuthorizationEvaluator'
|
|
80
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import falcon
|
|
5
|
+
from typing import cast
|
|
6
|
+
from ..authorization import ClaimEvaluator
|
|
7
|
+
from ..utils import Algorithm, validate
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TokenEvaluator:
|
|
11
|
+
"""
|
|
12
|
+
Evaluates a token given an Algorithm and Key.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__algorithm:Algorithm
|
|
16
|
+
__key:str
|
|
17
|
+
|
|
18
|
+
def __init__(self, algorithm:Algorithm, key:str) -> None:
|
|
19
|
+
self.__algorithm = algorithm
|
|
20
|
+
self.__key = key
|
|
21
|
+
|
|
22
|
+
def evaluate(
|
|
23
|
+
self,
|
|
24
|
+
token:str,
|
|
25
|
+
deny_claims:dict[str,str|ClaimEvaluator],
|
|
26
|
+
allow_claims:dict[str,str|ClaimEvaluator],
|
|
27
|
+
require_claims:dict[str,str|ClaimEvaluator]
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Evaluate a token against the supplied claim rules.
|
|
31
|
+
|
|
32
|
+
:param token: The token to be evaluated.
|
|
33
|
+
:param deny_claims: Claim DENY rules.
|
|
34
|
+
:param allow_claims: Claim ALLOW rules.
|
|
35
|
+
:param require_claims: Claim REQUIRE rules.
|
|
36
|
+
:raises falcon.HTTPUnauthorized: When claim rule checks fail.
|
|
37
|
+
:return: A boolean indicating success or failure, on failure the calling code should raise an appopriate exception.
|
|
38
|
+
"""
|
|
39
|
+
claims = validate(self.__algorithm, self.__key, token)
|
|
40
|
+
# check for denied claims (if any match, access denied)
|
|
41
|
+
for k,claim_check in deny_claims.items():
|
|
42
|
+
claim_value = claims.get(k, None)
|
|
43
|
+
if claim_value is not None and (claim_check == None or claim_value == claim_check or (callable(claim_check) and cast(ClaimEvaluator, claim_check)(claim_value))):
|
|
44
|
+
# access denied
|
|
45
|
+
raise falcon.HTTPUnauthorized(
|
|
46
|
+
title='Authorization Denied',
|
|
47
|
+
description='DENY'
|
|
48
|
+
)
|
|
49
|
+
# check for required claims (if any not present, access denied]
|
|
50
|
+
for k,claim_check in require_claims.items():
|
|
51
|
+
claim_value = claims.get(k, None)
|
|
52
|
+
if claim_value is None or (claim_value != claim_check and (not callable(claim_check) or not cast(ClaimEvaluator, claim_check)(claim_value))):
|
|
53
|
+
# access denied
|
|
54
|
+
raise falcon.HTTPUnauthorized(
|
|
55
|
+
title='Authorization Missing',
|
|
56
|
+
description='REQUIRE'
|
|
57
|
+
)
|
|
58
|
+
# check for allowed claims (if none match, access denied)
|
|
59
|
+
if len(allow_claims) > 0:
|
|
60
|
+
for k,claim_check in allow_claims.items():
|
|
61
|
+
claim_value = claims.get(k, None)
|
|
62
|
+
if claim_value is not None and (claim_value == claim_check or (callable(claim_check) and cast(ClaimEvaluator, claim_check)(claim_value))):
|
|
63
|
+
# access granted
|
|
64
|
+
return True
|
|
65
|
+
# access denied
|
|
66
|
+
raise falcon.HTTPUnauthorized(
|
|
67
|
+
title='Authorization Disallowed',
|
|
68
|
+
description='ALLOW'
|
|
69
|
+
)
|
|
70
|
+
# access granted
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
'TokenEvaluator'
|
|
76
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import falcon
|
|
5
|
+
from falcon._typing import WsgiMiddlewareWithProcessResource
|
|
6
|
+
import inspect
|
|
7
|
+
from typing import Any, Mapping, Optional, cast
|
|
8
|
+
from .AuthorizationEvaluator import AuthorizationEvaluator
|
|
9
|
+
from .TokenEvaluator import TokenEvaluator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WsgiAuthorizationMiddleware(WsgiMiddlewareWithProcessResource):
|
|
13
|
+
"""
|
|
14
|
+
WSGI-compatible Authorization Middleware
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
Intercepts resource requests to apply Authorization logic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__authorization_evaluator:AuthorizationEvaluator
|
|
21
|
+
|
|
22
|
+
def __init__(self, apikey_token_evaluator:Optional[TokenEvaluator], basic_token_evaluator:Optional[TokenEvaluator], bearer_token_evaluator:Optional[TokenEvaluator]) -> None:
|
|
23
|
+
self.__authorization_evaluator = AuthorizationEvaluator(apikey_token_evaluator=apikey_token_evaluator, basic_token_evaluator=basic_token_evaluator, bearer_token_evaluator=bearer_token_evaluator)
|
|
24
|
+
|
|
25
|
+
def process_resource(self, req:falcon.Request, resp:falcon.Response, resource:object, params: Mapping[str, Any]) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Intercept for ``process_resource`` that evaluates authorization data in the request against authorization requirements of the resource handler.
|
|
28
|
+
"""
|
|
29
|
+
handler_name = f'on_{req.method.lower()}'
|
|
30
|
+
handler = getattr(resource, handler_name, None)
|
|
31
|
+
if handler is None:
|
|
32
|
+
return
|
|
33
|
+
handler = inspect.unwrap(cast(Any, handler))
|
|
34
|
+
if getattr(handler, '__reshut_noauth', False):
|
|
35
|
+
# access granted
|
|
36
|
+
return
|
|
37
|
+
self.__authorization_evaluator.evaluate(req, handler)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
'WsgiAuthorizationMiddleware'
|
|
42
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from .AsgiAuthorizationMiddleware import AsgiAuthorizationMiddleware
|
|
5
|
+
from .AuthorizationEvaluator import AuthorizationEvaluator
|
|
6
|
+
from .TokenEvaluator import TokenEvaluator
|
|
7
|
+
from .WsgiAuthorizationMiddleware import WsgiAuthorizationMiddleware
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'AsgiAuthorizationMiddleware',
|
|
12
|
+
'AuthorizationEvaluator',
|
|
13
|
+
'TokenEvaluator',
|
|
14
|
+
'WsgiAuthorizationMiddleware'
|
|
15
|
+
]
|
reshut/py.typed
ADDED
|
File without changes
|
reshut/utils.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, ed448, rsa
|
|
7
|
+
from enum import StrEnum, unique
|
|
8
|
+
import secrets
|
|
9
|
+
import jwt
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@unique
|
|
14
|
+
class Algorithm(StrEnum):
|
|
15
|
+
"""
|
|
16
|
+
Enumeration of Algorithms that can be used for tokenizing claims.
|
|
17
|
+
"""
|
|
18
|
+
HS256 = 'HS256'
|
|
19
|
+
"""HMAC (256-bit, symmetric, fastest)"""
|
|
20
|
+
HS384 = 'HS384'
|
|
21
|
+
"""HMAC (384-bit, symmetric, fastest)"""
|
|
22
|
+
HS512 = 'HS512'
|
|
23
|
+
"""HMAC (512-bit, symmetric, fastest)"""
|
|
24
|
+
RS256 = 'RS256'
|
|
25
|
+
"""RSA (256-bit, asymmetric, slow)"""
|
|
26
|
+
RS384 = 'RS384'
|
|
27
|
+
"""RSA (384-bit, asymmetric, slow)"""
|
|
28
|
+
RS512 = 'RS512'
|
|
29
|
+
"""RSA (512-bit, asymmetric, slow)"""
|
|
30
|
+
ES256 = 'ES256'
|
|
31
|
+
"""Elliptic-curve (256-bit, asymmetric, fast)"""
|
|
32
|
+
ES384 = 'ES384'
|
|
33
|
+
"""Elliptic-curve (384-bit, asymmetric, fast)"""
|
|
34
|
+
ES512 = 'ES512'
|
|
35
|
+
"""Elliptic-curve (512-bit, asymmetric, fast)"""
|
|
36
|
+
ED25519 = 'ED25519'
|
|
37
|
+
"""Edwards-curve (256-bit, asymmetric, faster)"""
|
|
38
|
+
ED448 = 'ED448'
|
|
39
|
+
"""Edwards-curve (448-bit, asymmetric, faster)"""
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return self.value
|
|
42
|
+
|
|
43
|
+
def keygen(algorithm:Algorithm, key_size:Optional[int] = None) -> tuple[str,str|None]:
|
|
44
|
+
"""
|
|
45
|
+
Generates a key (or keypair) for the specified algorithm.
|
|
46
|
+
|
|
47
|
+
:param algorithm: The algorithm to use.
|
|
48
|
+
:param key_size: If provided, overrides the size of the generated key(s), in bits, if supported by the algorithm. Typically you would not do this.
|
|
49
|
+
:raises NotImplementedError: Raised when an unsupported algorithm is specified.
|
|
50
|
+
:return: A tuple containing the key(s) that were generated. When returning a keypair the first value is the private key, the second value is the public key.
|
|
51
|
+
"""
|
|
52
|
+
match algorithm:
|
|
53
|
+
case Algorithm.HS256 | Algorithm.HS384 | Algorithm.HS512:
|
|
54
|
+
if key_size is None:
|
|
55
|
+
match algorithm:
|
|
56
|
+
case Algorithm.HS256:
|
|
57
|
+
key_size = 256
|
|
58
|
+
case Algorithm.HS384:
|
|
59
|
+
key_size = 384
|
|
60
|
+
case Algorithm.HS512:
|
|
61
|
+
key_size = 512
|
|
62
|
+
return (base64.b64encode(secrets.token_bytes(key_size)).decode('ascii'), None)
|
|
63
|
+
case Algorithm.RS256 | Algorithm.RS384 | Algorithm.RS512:
|
|
64
|
+
if key_size is None:
|
|
65
|
+
match algorithm:
|
|
66
|
+
case Algorithm.RS256:
|
|
67
|
+
key_size = 2048
|
|
68
|
+
case Algorithm.RS384:
|
|
69
|
+
key_size = 3072
|
|
70
|
+
case Algorithm.RS512:
|
|
71
|
+
key_size = 4096
|
|
72
|
+
rsa_obj = rsa.generate_private_key(
|
|
73
|
+
public_exponent=65537,
|
|
74
|
+
key_size=key_size
|
|
75
|
+
)
|
|
76
|
+
prikey_pem = rsa_obj.private_bytes(
|
|
77
|
+
encoding=serialization.Encoding.PEM,
|
|
78
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
79
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
80
|
+
).decode('utf-8')
|
|
81
|
+
pubkey_pem = rsa_obj.public_key().public_bytes(
|
|
82
|
+
encoding=serialization.Encoding.PEM,
|
|
83
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
84
|
+
).decode('utf-8')
|
|
85
|
+
return (prikey_pem, pubkey_pem)
|
|
86
|
+
case Algorithm.ES256 | Algorithm.ES384 | Algorithm.ES512:
|
|
87
|
+
curve:ec.EllipticCurve
|
|
88
|
+
match algorithm:
|
|
89
|
+
case Algorithm.ES256:
|
|
90
|
+
curve = ec.SECP256R1()
|
|
91
|
+
case Algorithm.ES384:
|
|
92
|
+
curve = ec.SECP384R1()
|
|
93
|
+
case Algorithm.ES512:
|
|
94
|
+
curve = ec.SECP521R1()
|
|
95
|
+
ec_obj = ec.generate_private_key(curve)
|
|
96
|
+
prikey_pem = ec_obj.private_bytes(
|
|
97
|
+
encoding=serialization.Encoding.PEM,
|
|
98
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
99
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
100
|
+
).decode('utf-8')
|
|
101
|
+
pubkey_pem = ec_obj.public_key().public_bytes(
|
|
102
|
+
encoding=serialization.Encoding.PEM,
|
|
103
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
104
|
+
).decode('utf-8')
|
|
105
|
+
return (prikey_pem, pubkey_pem)
|
|
106
|
+
case Algorithm.ED25519 | Algorithm.ED448:
|
|
107
|
+
eddsa_obj = (ed25519.Ed25519PrivateKey if algorithm == Algorithm.ED25519 else ed448.Ed448PrivateKey).generate()
|
|
108
|
+
prikey_pem = eddsa_obj.private_bytes(
|
|
109
|
+
encoding=serialization.Encoding.PEM,
|
|
110
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
111
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
112
|
+
).decode('utf-8')
|
|
113
|
+
pubkey_pem = eddsa_obj.public_key().public_bytes(
|
|
114
|
+
encoding=serialization.Encoding.PEM,
|
|
115
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
116
|
+
).decode('utf-8')
|
|
117
|
+
return (prikey_pem, pubkey_pem)
|
|
118
|
+
case _:
|
|
119
|
+
raise NotImplementedError(f'Unsupported Algorithm "{algorithm}"')
|
|
120
|
+
|
|
121
|
+
def tokenize(algorithm:Algorithm, private_key:str, claims:dict[str,Any], *, audience:Optional[str|list[str]] = None, issuer:Optional[str] = None) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Tokenize the provided claims, optionally injecting ``aud`` and ``iss`` claims into the claims before tokenizing.
|
|
124
|
+
|
|
125
|
+
:param algorithm: The algorithm to use when signing the token.
|
|
126
|
+
:param private_key: The private key (or secret) used for signing.
|
|
127
|
+
:param claims: The claims to be tokenized.
|
|
128
|
+
:param audience: An optional value to be used as the ``aud`` claim.
|
|
129
|
+
:param issuer: An optional value to be used as the ``iss`` claim.
|
|
130
|
+
:return: The claims tokenized as a "compact‑serialization" JWT.
|
|
131
|
+
:raises Exception: If there is an error creating a token.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
if audience is not None:
|
|
135
|
+
claims |= { 'aud': audience }
|
|
136
|
+
if issuer is not None:
|
|
137
|
+
claims |= { 'iss': issuer }
|
|
138
|
+
match algorithm:
|
|
139
|
+
case Algorithm.ED25519 | Algorithm.ED448:
|
|
140
|
+
return jwt.encode(claims, private_key, 'EdDSA')
|
|
141
|
+
case Algorithm.HS256 | Algorithm.HS384 | Algorithm.HS512:
|
|
142
|
+
return jwt.encode(claims, base64.b64decode(private_key), algorithm.value)
|
|
143
|
+
case _:
|
|
144
|
+
return jwt.encode(claims, private_key, algorithm.value)
|
|
145
|
+
except Exception as ex:
|
|
146
|
+
raise Exception()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def validate(algorithm:Algorithm, public_key:str, token:str, *, audience:Optional[str|list[str]] = None, issuer:Optional[str] = None) -> dict[str, Any]:
|
|
150
|
+
"""
|
|
151
|
+
Verify a token and return the contained claims.
|
|
152
|
+
|
|
153
|
+
If ``audience`` or ``issuer`` are provided `validate(...)` also checks the ``aud`` and ``iss`` claims.
|
|
154
|
+
|
|
155
|
+
:param algorithm: The algorithm that was used to sign the token.
|
|
156
|
+
:param public_key: The public key (or secret) used for verification.
|
|
157
|
+
:param token: The compact‑serialization JWT string to be validated.
|
|
158
|
+
:param audience: Expected ``aud`` claim. Omit to skip validating ``aud`` claim.
|
|
159
|
+
:param issuer: Expected ``iss`` claim. Omit to skip validating ``iss`` claim.
|
|
160
|
+
:return: The decoded claims (as a ``dict``).
|
|
161
|
+
:raises Exception: If the token is invalid, expired, or claims do not match.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
match algorithm:
|
|
165
|
+
case Algorithm.ED25519 | Algorithm.ED448:
|
|
166
|
+
return jwt.decode(token, public_key, algorithms=['EdDSA'], audience=audience, issuer=issuer, options={"verify_aud": audience is not None})
|
|
167
|
+
case Algorithm.HS256 | Algorithm.HS384 | Algorithm.HS512:
|
|
168
|
+
return jwt.decode(token, base64.b64decode(public_key), algorithms=[algorithm.value], audience=audience, issuer=issuer, options={"verify_aud": audience is not None})
|
|
169
|
+
case _:
|
|
170
|
+
return jwt.decode(token, public_key, algorithms=[algorithm.value], audience=audience, issuer=issuer)
|
|
171
|
+
except Exception as ex:
|
|
172
|
+
raise Exception()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reshut
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: ..an authorization library for Falcon.
|
|
5
|
+
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: falcon,auth,bearer,apikey,hmac,rsa,ed25519,ed448
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: cryptography>=48
|
|
14
|
+
Requires-Dist: falcon<5,>=4.2
|
|
15
|
+
Requires-Dist: pyjwt<3.0.0,>=2.12.1
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: coverage; extra == "dev"
|
|
18
|
+
Requires-Dist: lxml-stubs==0.5.1; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy; extra == "dev"
|
|
20
|
+
Requires-Dist: punit<2.0.0,>=1.3.7; extra == "dev"
|
|
21
|
+
Requires-Dist: twine; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/reshut/) [](https://reshut.readthedocs.io)
|
|
26
|
+
|
|
27
|
+
**reshut** (רשות) is a decorative auth library for [Falcon](https://falconframework.org/).
|
|
28
|
+
|
|
29
|
+
This README is only a high-level introduction to **reshut**. For more detailed documentation, please view the official docs at [https://reshut.readthedocs.io](https://reshut.readthedocs.io).
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
You can install `reshut` from [PyPI](https://pypi.org/project/reshut/) through usual means, such as `pip`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install reshut
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
To use `reshut` two things must be done; first you must add an authorization middleware, and second you must apply one or more authorization decorators to a request handler method. Consider the following example:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
|
|
45
|
+
import falcon
|
|
46
|
+
from reshut import middleware, utils
|
|
47
|
+
from .api.v3.FakeApi import FakeApi
|
|
48
|
+
|
|
49
|
+
# you create a falcon app
|
|
50
|
+
app = asgi.App()
|
|
51
|
+
# you register the middleware, which applies Authorization checks to ALL requests
|
|
52
|
+
symmetric_key = utils.keygen(Algorithm.HS256)
|
|
53
|
+
asymmetric_key = utils.keygen(Algorithm.ED448)
|
|
54
|
+
app.add_middleware(middleware.AsgiAuthorizationMiddleware(
|
|
55
|
+
apikey_evaluater=TokenEvaluator(Algorithm.HS256, key),
|
|
56
|
+
bearer_evaluater=TokenEvaluator(Algorithm.ED448, key)
|
|
57
|
+
))
|
|
58
|
+
# you add some routes to your app
|
|
59
|
+
app.add_route('/api/v3/fakes', api.v3.FakeApi())
|
|
60
|
+
app.add_route('/api/v3/fakes/{id:int}', api.v3.FakeApi())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Elsewhere in your project, you defined `FakeApi` and decorated at least one handler to customize Authorization:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
|
|
67
|
+
import falcon
|
|
68
|
+
from reshut.authorization import allow_anonymous, allow_claim, deny_claim, require_claim
|
|
69
|
+
|
|
70
|
+
class FakeApi:
|
|
71
|
+
|
|
72
|
+
def initialize(self) -> None:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# access granted to any caller:
|
|
76
|
+
@allow_anonymous
|
|
77
|
+
async def on_delete(self, id:str) -> None:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# access denied to callers with `fake_restricted==yes`:
|
|
81
|
+
@deny_claim('fake_restricted', 'yes')
|
|
82
|
+
# access granted to callers having either `fake_reader` OR `fake_writer`:
|
|
83
|
+
@allow_claim('fake_reader')
|
|
84
|
+
@allow_claim('fake_writer')
|
|
85
|
+
async def on_get(self, id:str) -> None:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# access granted to callers having `roles` claim, where one
|
|
89
|
+
# of the roles is "ADMIN", performed via `ClaimEvaluator` callback:
|
|
90
|
+
@allow_claim('roles', lambda roles: 'ADMIN' in roles)
|
|
91
|
+
# access granted to callers having `readonly_user` with a `False` value:
|
|
92
|
+
@allow_claim('readonly_user', False)
|
|
93
|
+
async def on_put(self, id:str) -> None:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# access granted to callers having BOTH `department_id` of 123, 234, or 345
|
|
97
|
+
# and-also having `can_create==True`:
|
|
98
|
+
@require_claim('department_id', lambda x: x in [123,234,345])
|
|
99
|
+
@require_claim('can_create', True)
|
|
100
|
+
async def on_post(self, id:str) -> None:
|
|
101
|
+
pass
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
In the above example:
|
|
105
|
+
|
|
106
|
+
`@allow_anonymous` will grant access to all callers, authenticated or not.
|
|
107
|
+
|
|
108
|
+
`@allow_claim` specifies which claims will grant access, at least one of them must be satisfied by the request.
|
|
109
|
+
|
|
110
|
+
`@deny_claim` specifies which claims will deny access.
|
|
111
|
+
|
|
112
|
+
`@require_claim` specifies which claims are required for "access granted", ALL specified claims must be satisfied by the request or access is denied.
|
|
113
|
+
|
|
114
|
+
All authorization decorators optionally allow matching a specific literal value or a Claim Evaluator function. Claim Evaluator functions are useful for checking complex claim types like dates, dicts, lists, etc while literal values are useful for checking for well-known/individual values.
|
|
115
|
+
|
|
116
|
+
## Generating Keys, Tokenizing Claims, and Validating Tokens
|
|
117
|
+
|
|
118
|
+
If you need to generate keys there is a CLI tool `reshut-keygen` you can use:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# these generate PRIVATE SECRETS only to be used
|
|
122
|
+
# by you or your organization. they are NOT to be
|
|
123
|
+
# shared with a third party.
|
|
124
|
+
|
|
125
|
+
# generate an hs256 secret, output ios written
|
|
126
|
+
# to a "my_secret.b64" file
|
|
127
|
+
reshut-keygen --type RS256 --output my_secret
|
|
128
|
+
|
|
129
|
+
# generate an rs256 keypair, outputs go to
|
|
130
|
+
# separate PEM files named "my_rs256_public.pem"
|
|
131
|
+
# and "my_rs256_private.pem"
|
|
132
|
+
reshut-keygen --type RS256 --output my_rs256
|
|
133
|
+
|
|
134
|
+
# the --output arg may specify a path, the last
|
|
135
|
+
# part of the path is always taken as a filename base.
|
|
136
|
+
|
|
137
|
+
# if you omit --output a default is derived from
|
|
138
|
+
# the --type arg, ie "hs256.b64", "rs256_public.pem",
|
|
139
|
+
# and "rs256_private.pem" for the examples above.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
There is also a `reshut-tokenize` tool you can use to tokenize claims:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# this generates a SHARED TOKEN meant to be provided
|
|
146
|
+
# to a third party such as developers, testers,
|
|
147
|
+
# or business partners/integrators for authorization.
|
|
148
|
+
|
|
149
|
+
# tokenize claims
|
|
150
|
+
reshut-tokenize --type RS256 --key my_secret.b64 --claims '{"foo":"bar"}' --output shared_token.b64
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Lastly, there is a `reshut-validate` tool you can use to validate tokens:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
reshut-tokenize --type RS256 --key my_secret.b64 --claims '{"foo":"bar"}' --output shared_token.b64
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
These tools are written using the `reshut.utils` namespace, you can use the utils namespace within your own code to dynamically allocate keys and tokens as you see fit for your solution (instead of dropping to a shell for the same result).
|
|
160
|
+
|
|
161
|
+
For example, here is a snippet demonstrating the generation of an ed448 keypair and-also an ed448 token valid for that keypair:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
|
|
165
|
+
from reshut.utils import Algorithm, keygen, tokenize, validate
|
|
166
|
+
|
|
167
|
+
# on the server/etc, generate keys
|
|
168
|
+
ed448_prikey, ed448_pubkey = keygen(Algorithm.ED448)
|
|
169
|
+
print(ed448_prikey)
|
|
170
|
+
print(ed448_pubkey)
|
|
171
|
+
|
|
172
|
+
# on the server/etc, issue "secure" claims (claims the recipient can see/verify, but cannot modify)
|
|
173
|
+
token = tokenize(Algorithm.ED448, ed448_prikey, {
|
|
174
|
+
sub: 'Subject',
|
|
175
|
+
iss: 'Issuer',
|
|
176
|
+
exp: datetime.now()+timedelta(days=90)
|
|
177
|
+
})
|
|
178
|
+
print(token)
|
|
179
|
+
|
|
180
|
+
claims = validate(Algorithm.ED448, ed448_pubkey, token)
|
|
181
|
+
print(claims)
|
|
182
|
+
|
|
183
|
+
# individual claims can then be verified. these examples are only really useful
|
|
184
|
+
# if you are automating key issuance, token issuance, are a third-party that
|
|
185
|
+
# needs to generate a complex/symmetric token on-demand, or are implementing
|
|
186
|
+
# a custom token validator.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Contact
|
|
190
|
+
|
|
191
|
+
You can reach me on [Discord](https://discordapp.com/users/307684202080501761) or [open an Issue on Github](https://github.com/wilson0x4d/reshut/issues/new/choose).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
reshut/__init__.py,sha256=H_24D1xLKZtGBOBP2QTTO7ag-qW8tsqGsUEZyMTUoko,222
|
|
2
|
+
reshut/__main__.py,sha256=Avsq2EutIxoiZpTEJbj27ZeooLPN9ZxxPoBdKm2JjUc,4362
|
|
3
|
+
reshut/authorization.py,sha256=O5RyKnXruYiXGGLtIjbV4GQFeb4pDeqO3mhtC9DzUu0,3163
|
|
4
|
+
reshut/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
reshut/utils.py,sha256=P__BWcljtefqyAo_RdwgDK33ydAtFIgB2J348CpkxQA,8019
|
|
6
|
+
reshut/middleware/AsgiAuthorizationMiddleware.py,sha256=4EevcVqY4CfzeaJYMYh98NjmrmRcQwfa59j5qvrRQ70,1690
|
|
7
|
+
reshut/middleware/AuthorizationEvaluator.py,sha256=_mVR03-Ii42FWy8t4-xOvLs4uH12fDGRbKPirlrBhNI,3514
|
|
8
|
+
reshut/middleware/TokenEvaluator.py,sha256=obQWU5wbYkOVGmLTOa9x8gj2ac5n-Rpoc19LrFotx0o,2953
|
|
9
|
+
reshut/middleware/WsgiAuthorizationMiddleware.py,sha256=e8elEomycMSXmyqGY4FUe_lSoM7zft5c3Zcaw1LZnPY,1684
|
|
10
|
+
reshut/middleware/__init__.py,sha256=RR2tHP09wPAmTnOUeLm4TbnDTQmXpCkZPlZIWKFe0uM,456
|
|
11
|
+
reshut-0.0.3.dist-info/licenses/LICENSE,sha256=WDHBjw5FnacUEgi2Zf5fbjqqMMUM6Sw4DOHaIrFoewY,1058
|
|
12
|
+
reshut-0.0.3.dist-info/METADATA,sha256=zzYQCOFYrYjfPj3Xr2dBOfrxC5J1AVlSzTIiLzNpQ34,7294
|
|
13
|
+
reshut-0.0.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
reshut-0.0.3.dist-info/top_level.txt,sha256=RxpGNEH0IRWjL8NF0oLi3S7bEcubozcQoBW5hP3rlMA,7
|
|
15
|
+
reshut-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
© 2026 Shaun Wilson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
reshut
|