This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new fbd5cb6 Add a JWT module for use with private API endpoints
fbd5cb6 is described below
commit fbd5cb61bc64b247534f3da080565d2e171fde65
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jul 2 19:58:23 2025 +0100
Add a JWT module for use with private API endpoints
---
atr/jwtoken.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
poetry.lock | 20 +++++++++++++++-
pyproject.toml | 1 +
3 files changed, 93 insertions(+), 1 deletion(-)
diff --git a/atr/jwtoken.py b/atr/jwtoken.py
new file mode 100644
index 0000000..db734a2
--- /dev/null
+++ b/atr/jwtoken.py
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import datetime as datetime
+import functools
+import secrets as secrets
+from typing import TYPE_CHECKING, Any, Final
+
+import asfquart.base as base
+import jwt
+import quart
+
+import atr.config as config
+
+_SECRET_KEY: Final[str] = config.get().SECRET_KEY
+_ALGORITHM: Final[str] = "HS256"
+
+if TYPE_CHECKING:
+ from collections.abc import Awaitable, Callable, Coroutine
+
+
+def issue(uid: str, *, ttl: int = 24 * 3600) -> str:
+ now = datetime.datetime.now(tz=datetime.UTC)
+ payload = {
+ "sub": uid,
+ "iat": now,
+ "exp": now + datetime.timedelta(seconds=ttl),
+ "jti": secrets.token_hex(8),
+ }
+ return jwt.encode(payload, _SECRET_KEY, algorithm=_ALGORITHM)
+
+
+def require[**P, R](func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P,
Awaitable[R]]:
+ @functools.wraps(func)
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
+ token = _extract_bearer_token(quart.request)
+ try:
+ claims = verify(token)
+ except jwt.PyJWTError as exc:
+ raise base.ASFQuartException(f"Invalid token: {exc}",
errorcode=401) from exc
+
+ quart.g.jwt_claims = claims
+ return await func(*args, **kwargs)
+
+ return wrapper
+
+
+def verify(token: str) -> dict[str, Any]:
+ return jwt.decode(token, _SECRET_KEY, algorithms=[_ALGORITHM])
+
+
+def _extract_bearer_token(request: quart.Request) -> str:
+ header = request.headers.get("Authorization", "")
+ scheme, _, token = header.partition(" ")
+ if scheme.lower() != "bearer" or not token:
+ raise base.ASFQuartException("Bearer token missing", errorcode=401)
+ return token
diff --git a/poetry.lock b/poetry.lock
index 3588a71..43ececc 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2251,6 +2251,24 @@ examples = ["django", "litestar", "numpy"]
test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest",
"pytest-asyncio (==0.23.8)", "trio"]
types = ["typing_extensions"]
+[[package]]
+name = "pyjwt"
+version = "2.10.1"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "PyJWT-2.10.1-py3-none-any.whl", hash =
"sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
+ {file = "pyjwt-2.10.1.tar.gz", hash =
"sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
+]
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit",
"pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
[[package]]
name = "pyright"
version = "1.1.402"
@@ -3148,4 +3166,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = "~=3.13"
-content-hash =
"3cf10d16cd21f3d39e21f7cb26cfbede7ecb0da0a0b893b9a49fb7b73e2aab3a"
+content-hash =
"cf3130d7e028316482b7e62383c05fe183f64284f99b788b5aea94be162934d8"
diff --git a/pyproject.toml b/pyproject.toml
index 2b1e1d8..54ad11b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,6 +35,7 @@ dependencies = [
"quart-wtforms~=1.0.3",
"rich~=14.0.0",
"sqlmodel~=0.0.24",
+ "pyjwt (>=2.10.1,<3.0.0)",
]
[dependency-groups]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]