py2http 0.1.63__tar.gz → 0.1.65__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {py2http-0.1.63 → py2http-0.1.65}/PKG-INFO +1 -1
- py2http-0.1.65/py2http/middleware.py +154 -0
- py2http-0.1.65/py2http/tests/test_jwt_middleware.py +179 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http.egg-info/PKG-INFO +1 -1
- {py2http-0.1.63 → py2http-0.1.65}/py2http.egg-info/SOURCES.txt +1 -0
- {py2http-0.1.63 → py2http-0.1.65}/setup.cfg +1 -1
- py2http-0.1.63/py2http/middleware.py +0 -71
- {py2http-0.1.63 → py2http-0.1.65}/LICENSE +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/README.md +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/__init__.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/bottle_plugins.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/config.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/constants.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/decorators.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/default_configs.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/diagnosis.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/examples/__init__.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/examples/custom_input_mapper.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/examples/custom_input_mappers.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/examples/example_service.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/examples/example_service_bottle.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/examples/example_service_flask.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/examples/object_dispatch.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/openapi_utils.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/route_utils.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/schema_tools.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/service.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/__init__.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/example_service_test.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/objects_for_testing.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/simple_run_process_test.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/test_attr_dispatch.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/test_binary_req.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/test_decorators.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/test_flask.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/test_not_set_defaults.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/test_p2h2p.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/tests/utils_for_testing.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/types.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http/util.py +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http.egg-info/dependency_links.txt +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http.egg-info/not-zip-safe +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http.egg-info/requires.txt +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/py2http.egg-info/top_level.txt +0 -0
- {py2http-0.1.63 → py2http-0.1.65}/setup.py +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
py2http is a Python module that allows you to dispatch Python functions as HTTP
|
|
3
|
+
services.
|
|
4
|
+
By running a basic HTTP service from a list of functions, you can access OpenAPI
|
|
5
|
+
specifications, Swagger documentation, and utilize the HTTP service's routes.
|
|
6
|
+
Additionally, Py2http provides features for method transformation, input mapping,
|
|
7
|
+
output mapping, error handling, and client generation.
|
|
8
|
+
|
|
9
|
+
The code snippet provided below is a part of Py2http module and includes middleware
|
|
10
|
+
functions for handling JWT authentication and superadmin authorization.
|
|
11
|
+
These middleware functions are designed to be used within the Py2http framework to
|
|
12
|
+
ensure secure and authenticated access to HTTP routes.
|
|
13
|
+
|
|
14
|
+
Include the following docstring to describe this module:
|
|
15
|
+
"Middleware functions for handling JWT authentication and superadmin authorization
|
|
16
|
+
within the Py2http framework. These functions ensure secure access to HTTP routes
|
|
17
|
+
by verifying credentials and permissions. Use these middleware functions to enforce
|
|
18
|
+
authentication and control access to web resources.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
from warnings import warn
|
|
24
|
+
|
|
25
|
+
from py2http.constants import JSON_CONTENT_TYPE
|
|
26
|
+
|
|
27
|
+
DFLT_JWT_ALGORITHMS = ("HS256",)
|
|
28
|
+
DFLT_PUBLIC_ROUTES = ("ping", "openapi")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _algorithm_family(algorithm: str) -> str:
|
|
32
|
+
"""The key family an algorithm verifies with: a shared secret for ``HS*``, a
|
|
33
|
+
public key otherwise.
|
|
34
|
+
|
|
35
|
+
>>> _algorithm_family('HS512'), _algorithm_family('RS256'), _algorithm_family('ES256')
|
|
36
|
+
('hmac', 'public-key', 'public-key')
|
|
37
|
+
"""
|
|
38
|
+
return "hmac" if algorithm.upper().startswith("HS") else "public-key"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def mk_jwt_middleware(
|
|
42
|
+
secret,
|
|
43
|
+
verify=True,
|
|
44
|
+
*,
|
|
45
|
+
algorithms: Iterable[str] = DFLT_JWT_ALGORITHMS,
|
|
46
|
+
public_routes: Iterable[str] = DFLT_PUBLIC_ROUTES,
|
|
47
|
+
allow_mixed_algorithm_families: bool = False,
|
|
48
|
+
):
|
|
49
|
+
"""Make an aiohttp middleware that requires a valid ``Authorization: Bearer``
|
|
50
|
+
JWT on every request, and puts the decoded claims on ``request.token``.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
secret: The key tokens are verified with (an HMAC secret for ``HS*``
|
|
54
|
+
algorithms, a public key for ``RS*``/``ES*``/...).
|
|
55
|
+
verify: If False, signatures and claims (``exp``, ...) are NOT checked:
|
|
56
|
+
whatever a token says ends up on ``request.token``, and an
|
|
57
|
+
undecodable token only triggers a warning. For development only.
|
|
58
|
+
(Before, PyJWT 2 ignored the legacy option this passed, so
|
|
59
|
+
signatures were still checked even with ``verify=False``.)
|
|
60
|
+
algorithms: The algorithms a token may be signed with (PyJWT names,
|
|
61
|
+
case-sensitive; ``"none"`` is refused). Defaults to ``("HS256",)``,
|
|
62
|
+
as ``py2http.bottle_plugins.JWTPlugin`` does.
|
|
63
|
+
public_routes: Names of aiohttp routes (``name=`` in ``add_route``) that
|
|
64
|
+
need no token. Matched on the route name, not the handler's
|
|
65
|
+
``__name__``, so a handler that happens to be called ``ping`` is not
|
|
66
|
+
public by accident. Route names are matched in sub-applications
|
|
67
|
+
too, so a sub-app route named ``ping`` is public as well.
|
|
68
|
+
allow_mixed_algorithm_families: Accepting an HMAC algorithm and a
|
|
69
|
+
public-key algorithm with one ``secret`` is the classic
|
|
70
|
+
algorithm-confusion setup (a token "signed" with HS256 using the
|
|
71
|
+
public key as the HMAC secret), so it raises ``ValueError`` unless
|
|
72
|
+
this is True. The middleware used to accept ``["HS256", "RS256"]``;
|
|
73
|
+
pass that list with this flag to restore it.
|
|
74
|
+
"""
|
|
75
|
+
import jwt
|
|
76
|
+
from aiohttp import web
|
|
77
|
+
|
|
78
|
+
for name, value in [("algorithms", algorithms), ("public_routes", public_routes)]:
|
|
79
|
+
if isinstance(value, (str, bytes)):
|
|
80
|
+
raise TypeError(f"{name} must be a list of names, e.g. [{value!r}]")
|
|
81
|
+
algorithms = list(algorithms)
|
|
82
|
+
if not algorithms:
|
|
83
|
+
raise ValueError("algorithms must name at least one JWT algorithm")
|
|
84
|
+
known = set(jwt.algorithms.get_default_algorithms()) - {"none"}
|
|
85
|
+
if unknown := [a for a in algorithms if a not in known]:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Unknown or unsigned JWT algorithm(s) {unknown}; names are "
|
|
88
|
+
f"case-sensitive, choose from {sorted(known)}"
|
|
89
|
+
)
|
|
90
|
+
if verify and not secret:
|
|
91
|
+
raise ValueError("secret must be a non-empty key when verify=True")
|
|
92
|
+
families = {_algorithm_family(a) for a in algorithms}
|
|
93
|
+
if verify and "hmac" in families and not isinstance(secret, (str, bytes)):
|
|
94
|
+
raise TypeError(f"an HMAC secret must be str or bytes, got {type(secret)}")
|
|
95
|
+
if len(families) > 1 and not allow_mixed_algorithm_families:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"algorithms={algorithms} mixes HMAC and public-key algorithms with one "
|
|
98
|
+
"secret, which allows algorithm-confusion forgeries; use one family, or "
|
|
99
|
+
"pass allow_mixed_algorithm_families=True if you really mean it"
|
|
100
|
+
)
|
|
101
|
+
public_routes = frozenset(name for name in public_routes if name is not None)
|
|
102
|
+
|
|
103
|
+
def _unauthorized(message):
|
|
104
|
+
return web.HTTPUnauthorized(
|
|
105
|
+
text=json.dumps({"error": message}),
|
|
106
|
+
content_type=JSON_CONTENT_TYPE,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@web.middleware
|
|
110
|
+
async def middleware(req, handler):
|
|
111
|
+
route = req.match_info.route
|
|
112
|
+
if getattr(route, "name", None) in public_routes:
|
|
113
|
+
return await handler(req)
|
|
114
|
+
auth_header = req.headers.get("Authorization", "")
|
|
115
|
+
token = auth_header[7:] if auth_header[:7].lower() == "bearer " else ""
|
|
116
|
+
try:
|
|
117
|
+
decoded = jwt.decode(
|
|
118
|
+
token,
|
|
119
|
+
secret,
|
|
120
|
+
options={"verify_signature": verify},
|
|
121
|
+
algorithms=algorithms,
|
|
122
|
+
)
|
|
123
|
+
except jwt.PyJWTError as error: # incl. a key unfit for the token's alg
|
|
124
|
+
if verify:
|
|
125
|
+
# InvalidTokenError texts describe the token ("Signature has
|
|
126
|
+
# expired"); other PyJWTErrors can describe the server's key
|
|
127
|
+
reason = (
|
|
128
|
+
str(error)
|
|
129
|
+
if isinstance(error, jwt.InvalidTokenError)
|
|
130
|
+
else "not acceptable"
|
|
131
|
+
)
|
|
132
|
+
raise _unauthorized(f"Invalid authentication token: {reason}")
|
|
133
|
+
warn(f"Invalid JWT: {error}")
|
|
134
|
+
return await handler(req)
|
|
135
|
+
req.token = decoded
|
|
136
|
+
return await handler(req)
|
|
137
|
+
|
|
138
|
+
return middleware
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def mk_superadmin_middleware(secret):
|
|
142
|
+
from aiohttp import web
|
|
143
|
+
|
|
144
|
+
@web.middleware
|
|
145
|
+
async def middleware(req, handler):
|
|
146
|
+
auth_header = req.headers.get("Authorization", "")
|
|
147
|
+
if auth_header == secret:
|
|
148
|
+
return await handler(req)
|
|
149
|
+
return web.HTTPUnauthorized(
|
|
150
|
+
text=json.dumps({"error": "invalid API key"}),
|
|
151
|
+
content_type=JSON_CONTENT_TYPE,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return middleware
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""``mk_jwt_middleware`` verifies signatures, pins algorithms, and only lets named
|
|
2
|
+
public routes through without a token."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import jwt
|
|
12
|
+
import pytest
|
|
13
|
+
from aiohttp import web
|
|
14
|
+
from aiohttp.test_utils import TestClient, TestServer
|
|
15
|
+
|
|
16
|
+
from py2http.middleware import mk_jwt_middleware
|
|
17
|
+
|
|
18
|
+
SECRET = "s3cret-for-tests-only-0123456789abcdef"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _whoami(request):
|
|
22
|
+
return web.json_response(getattr(request, "token", None))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def ping(request): # a handler whose __name__ is "ping", on a private route
|
|
26
|
+
return web.json_response("private")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _app(middleware):
|
|
30
|
+
app = web.Application(middlewares=[middleware])
|
|
31
|
+
app.router.add_get("/ping", _whoami, name="ping")
|
|
32
|
+
app.router.add_get("/me", _whoami, name="me")
|
|
33
|
+
app.router.add_get("/secret", ping)
|
|
34
|
+
return app
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get(middleware, path, token=None):
|
|
38
|
+
"""GET ``path`` on a fresh app (an aiohttp app is bound to one event loop)."""
|
|
39
|
+
app = _app(middleware)
|
|
40
|
+
|
|
41
|
+
async def go():
|
|
42
|
+
headers = {"Authorization": f"Bearer {token}"} if token is not None else {}
|
|
43
|
+
async with TestClient(TestServer(app)) as client:
|
|
44
|
+
resp = await client.get(path, headers=headers)
|
|
45
|
+
return resp.status, await resp.text()
|
|
46
|
+
|
|
47
|
+
return asyncio.run(go())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _b64(data: bytes) -> str:
|
|
51
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _hs256_with_key(claims, key: bytes) -> str:
|
|
55
|
+
"""Sign by hand, so tests can forge what PyJWT itself refuses to produce."""
|
|
56
|
+
signing_input = f"{_b64(json.dumps({'alg': 'HS256', 'typ': 'JWT'}).encode())}.{_b64(json.dumps(claims).encode())}"
|
|
57
|
+
sig = hmac.new(key, signing_input.encode(), hashlib.sha256).digest()
|
|
58
|
+
return f"{signing_input}.{_b64(sig)}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_valid_token_is_decoded_onto_request():
|
|
62
|
+
token = jwt.encode({"sub": "alice"}, SECRET, algorithm="HS256")
|
|
63
|
+
status, body = _get(mk_jwt_middleware(SECRET), "/me", token)
|
|
64
|
+
assert status == 200 and json.loads(body) == {"sub": "alice"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize(
|
|
68
|
+
"token",
|
|
69
|
+
[
|
|
70
|
+
None,
|
|
71
|
+
"",
|
|
72
|
+
"garbage",
|
|
73
|
+
jwt.encode(
|
|
74
|
+
{"sub": "alice"}, "wrong-secret-0123456789abcdef0123", algorithm="HS256"
|
|
75
|
+
),
|
|
76
|
+
_b64(b'{"alg":"none","typ":"JWT"}') + "." + _b64(b'{"sub":"alice"}') + ".",
|
|
77
|
+
jwt.encode(
|
|
78
|
+
{"sub": "alice", "exp": int(time.time()) - 60}, SECRET, algorithm="HS256"
|
|
79
|
+
),
|
|
80
|
+
jwt.encode({"sub": "alice"}, SECRET, algorithm="HS512"), # not an allowed alg
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
def test_bad_tokens_get_401_and_no_echo(token):
|
|
84
|
+
status, body = _get(mk_jwt_middleware(SECRET), "/me", token)
|
|
85
|
+
assert status == 401
|
|
86
|
+
if token:
|
|
87
|
+
assert token not in body
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_public_routes_are_matched_by_route_name_not_handler_name():
|
|
91
|
+
mw = mk_jwt_middleware(SECRET)
|
|
92
|
+
assert _get(mw, "/ping")[0] == 200
|
|
93
|
+
assert _get(mw, "/secret")[0] == 401 # handler is called "ping", route is not
|
|
94
|
+
mw = mk_jwt_middleware(SECRET, public_routes=())
|
|
95
|
+
assert _get(mw, "/ping")[0] == 401
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_mixed_algorithm_families_need_explicit_opt_in():
|
|
99
|
+
with pytest.raises(ValueError, match="algorithm-confusion"):
|
|
100
|
+
mk_jwt_middleware(SECRET, algorithms=["HS256", "RS256"])
|
|
101
|
+
with pytest.raises(ValueError):
|
|
102
|
+
mk_jwt_middleware(SECRET, algorithms=[])
|
|
103
|
+
mk_jwt_middleware(
|
|
104
|
+
SECRET, algorithms=["HS256", "RS256"], allow_mixed_algorithm_families=True
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_algorithm_confusion_forgery_is_rejected_with_public_key_secret():
|
|
109
|
+
from cryptography.hazmat.primitives import serialization
|
|
110
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
111
|
+
|
|
112
|
+
private = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
113
|
+
public_pem = private.public_key().public_bytes(
|
|
114
|
+
serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo
|
|
115
|
+
)
|
|
116
|
+
mw = mk_jwt_middleware(public_pem, algorithms=["RS256"])
|
|
117
|
+
genuine = jwt.encode({"sub": "alice"}, private, algorithm="RS256")
|
|
118
|
+
assert _get(mw, "/me", genuine)[0] == 200
|
|
119
|
+
forged = _hs256_with_key({"sub": "mallory"}, public_pem)
|
|
120
|
+
assert _get(mw, "/me", forged)[0] == 401
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_verify_false_skips_signature_check():
|
|
124
|
+
token = jwt.encode(
|
|
125
|
+
{"sub": "alice"}, "another-secret-0123456789abcdef01", algorithm="HS256"
|
|
126
|
+
)
|
|
127
|
+
with pytest.warns(UserWarning):
|
|
128
|
+
status, _ = _get(mk_jwt_middleware(SECRET, verify=False), "/me", "garbage")
|
|
129
|
+
assert status == 200
|
|
130
|
+
status, body = _get(mk_jwt_middleware(SECRET, verify=False), "/me", token)
|
|
131
|
+
assert status == 200 and json.loads(body) == {"sub": "alice"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.parametrize(
|
|
135
|
+
"kwargs, error",
|
|
136
|
+
[
|
|
137
|
+
({"algorithms": "HS256"}, TypeError),
|
|
138
|
+
({"algorithms": ["hs256"]}, ValueError),
|
|
139
|
+
({"algorithms": ["none"]}, ValueError),
|
|
140
|
+
({"algorithms": ["RS256", "none"]}, ValueError),
|
|
141
|
+
({"public_routes": "ping"}, TypeError),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
def test_bad_algorithm_config_fails_at_construction(kwargs, error):
|
|
145
|
+
with pytest.raises(error):
|
|
146
|
+
mk_jwt_middleware(SECRET, **kwargs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.parametrize("secret", ["", b"", None, 12345])
|
|
150
|
+
def test_empty_or_unusable_secret_fails_at_construction(secret):
|
|
151
|
+
with pytest.raises((ValueError, TypeError)):
|
|
152
|
+
mk_jwt_middleware(secret)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_key_unfit_for_token_algorithm_is_401_not_500():
|
|
156
|
+
from cryptography.hazmat.primitives import serialization
|
|
157
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
158
|
+
|
|
159
|
+
public_pem = (
|
|
160
|
+
rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
161
|
+
.public_key()
|
|
162
|
+
.public_bytes(
|
|
163
|
+
serialization.Encoding.PEM,
|
|
164
|
+
serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
mw = mk_jwt_middleware(
|
|
168
|
+
public_pem,
|
|
169
|
+
algorithms=["HS256", "RS256"],
|
|
170
|
+
allow_mixed_algorithm_families=True,
|
|
171
|
+
)
|
|
172
|
+
forged = _hs256_with_key({"sub": "mallory"}, public_pem)
|
|
173
|
+
status, body = _get(mw, "/me", forged)
|
|
174
|
+
assert status == 401 and "asymmetric" not in body.lower()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_none_in_public_routes_does_not_open_unnamed_routes():
|
|
178
|
+
mw = mk_jwt_middleware(SECRET, public_routes=["ping", None])
|
|
179
|
+
assert _get(mw, "/secret")[0] == 401
|
|
@@ -37,6 +37,7 @@ py2http/tests/test_attr_dispatch.py
|
|
|
37
37
|
py2http/tests/test_binary_req.py
|
|
38
38
|
py2http/tests/test_decorators.py
|
|
39
39
|
py2http/tests/test_flask.py
|
|
40
|
+
py2http/tests/test_jwt_middleware.py
|
|
40
41
|
py2http/tests/test_not_set_defaults.py
|
|
41
42
|
py2http/tests/test_p2h2p.py
|
|
42
43
|
py2http/tests/utils_for_testing.py
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
py2http is a Python module that allows you to dispatch Python functions as HTTP
|
|
3
|
-
services.
|
|
4
|
-
By running a basic HTTP service from a list of functions, you can access OpenAPI
|
|
5
|
-
specifications, Swagger documentation, and utilize the HTTP service's routes.
|
|
6
|
-
Additionally, Py2http provides features for method transformation, input mapping,
|
|
7
|
-
output mapping, error handling, and client generation.
|
|
8
|
-
|
|
9
|
-
The code snippet provided below is a part of Py2http module and includes middleware
|
|
10
|
-
functions for handling JWT authentication and superadmin authorization.
|
|
11
|
-
These middleware functions are designed to be used within the Py2http framework to
|
|
12
|
-
ensure secure and authenticated access to HTTP routes.
|
|
13
|
-
|
|
14
|
-
Include the following docstring to describe this module:
|
|
15
|
-
"Middleware functions for handling JWT authentication and superadmin authorization
|
|
16
|
-
within the Py2http framework. These functions ensure secure access to HTTP routes
|
|
17
|
-
by verifying credentials and permissions. Use these middleware functions to enforce
|
|
18
|
-
authentication and control access to web resources.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
from warnings import warn
|
|
23
|
-
from py2http.constants import JSON_CONTENT_TYPE
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def mk_jwt_middleware(secret, verify=True):
|
|
27
|
-
from aiohttp import web
|
|
28
|
-
import jwt
|
|
29
|
-
|
|
30
|
-
@web.middleware
|
|
31
|
-
async def middleware(req, handler):
|
|
32
|
-
if handler.__name__ == "ping" or handler.__name__ == "openapi":
|
|
33
|
-
return await handler(req)
|
|
34
|
-
auth_header = req.headers.get("Authorization", "")
|
|
35
|
-
token = auth_header[7:]
|
|
36
|
-
try:
|
|
37
|
-
decoded = jwt.decode(
|
|
38
|
-
token, secret, options={"verify": verify}, algorithms=["HS256", "RS256"]
|
|
39
|
-
)
|
|
40
|
-
req.token = decoded
|
|
41
|
-
return await handler(req)
|
|
42
|
-
except jwt.DecodeError as error:
|
|
43
|
-
if verify:
|
|
44
|
-
return web.HTTPUnauthorized(
|
|
45
|
-
text=json.dumps(
|
|
46
|
-
{
|
|
47
|
-
"error": f'Invalid authentication token "{token}", {str(error)}'
|
|
48
|
-
}
|
|
49
|
-
),
|
|
50
|
-
content_type=JSON_CONTENT_TYPE,
|
|
51
|
-
)
|
|
52
|
-
warn(f"Invalid JWT: {token}")
|
|
53
|
-
return await handler(req)
|
|
54
|
-
|
|
55
|
-
return middleware
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def mk_superadmin_middleware(secret):
|
|
59
|
-
from aiohttp import web
|
|
60
|
-
|
|
61
|
-
@web.middleware
|
|
62
|
-
async def middleware(req, handler):
|
|
63
|
-
auth_header = req.headers.get("Authorization", "")
|
|
64
|
-
if auth_header == secret:
|
|
65
|
-
return await handler(req)
|
|
66
|
-
return web.HTTPUnauthorized(
|
|
67
|
-
text=json.dumps({"error": "invalid API key"}),
|
|
68
|
-
content_type=JSON_CONTENT_TYPE,
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
return middleware
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|