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]

Reply via email to