This is an automated email from the ASF dual-hosted git repository.

arm pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/arm by this push:
     new 5fb88cfc Use PAT hash as part of issued JWT. Closes #828.
5fb88cfc is described below

commit 5fb88cfc79cc9c04411f2d1f8856a8f9bf168d33
Author: Alastair McFarlane <[email protected]>
AuthorDate: Mon Mar 9 15:30:11 2026 +0000

    Use PAT hash as part of issued JWT. Closes #828.
---
 atr/db/__init__.py            |  5 +++++
 atr/jwtoken.py                | 17 ++++++++++++++++-
 atr/storage/writers/tokens.py |  2 +-
 3 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index a5514511..3236e117 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -372,6 +372,11 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         if commit is True:
             await self.commit()
 
+    def personal_access_token(self, token_hash: str) -> 
Query[sql.PersonalAccessToken]:
+        query = sqlmodel.select(sql.PersonalAccessToken)
+        query = query.where(sql.PersonalAccessToken.token_hash == token_hash)
+        return Query(self, query)
+
     def project(
         self,
         name: Opt[str] = NOT_SET,
diff --git a/atr/jwtoken.py b/atr/jwtoken.py
index be56b438..2bf9dee4 100644
--- a/atr/jwtoken.py
+++ b/atr/jwtoken.py
@@ -31,6 +31,7 @@ import jwt
 import quart
 
 import atr.config as config
+import atr.db as db
 import atr.ldap as ldap
 import atr.log as log
 import atr.models.github as github
@@ -64,7 +65,7 @@ def activate_signing_key(key: str) -> None:
     app.extensions[_JWT_KEY_APP_EXTENSION] = key
 
 
-def issue(uid: str, *, ttl: int = _ATR_JWT_TTL) -> str:
+def issue(uid: str, *, ttl: int = _ATR_JWT_TTL, pat_hash: str | None = None) 
-> str:
     now = datetime.datetime.now(tz=datetime.UTC)
     payload = {
         "sub": uid,
@@ -75,6 +76,8 @@ def issue(uid: str, *, ttl: int = _ATR_JWT_TTL) -> str:
         "exp": now + datetime.timedelta(seconds=ttl),
         "jti": secrets.token_hex(128 // 8),
     }
+    if pat_hash:
+        payload["atr_th"] = pat_hash
     return jwt.encode(payload, _signing_key(), algorithm=_ALGORITHM)
 
 
@@ -131,6 +134,18 @@ async def verify(token: str) -> dict[str, Any]:
     if not await ldap.is_active(asf_uid):
         log.failed_authentication("account_deleted_or_banned")
         raise base.ASFQuartException("Account is disabled", errorcode=401)
+
+    pat_hash = claims.get("atr_th")
+    # We don't fail on missing hash because not all JWTs come from PATs
+    if pat_hash:
+        async with db.session() as data:
+            pat = await data.personal_access_token(pat_hash).get()
+            if not pat:
+                log.failed_authentication("pat_hash_invalid")
+                raise base.ASFQuartException("Personal Access Token invalid")
+            if pat.expires < datetime.datetime.now(datetime.UTC):
+                log.failed_authentication("pat_expired")
+                raise base.ASFQuartException("Personal Access Token expired")
     return claims
 
 
diff --git a/atr/storage/writers/tokens.py b/atr/storage/writers/tokens.py
index 73d8e109..37c499a9 100644
--- a/atr/storage/writers/tokens.py
+++ b/atr/storage/writers/tokens.py
@@ -133,7 +133,7 @@ class FoundationCommitter(GeneralPublic):
             log.failed_authentication("account_deleted_or_banned")
             raise storage.AccessError("Authentication failed")
 
-        issued_jwt = jwtoken.issue(self.__asf_uid)
+        issued_jwt = jwtoken.issue(self.__asf_uid, pat_hash=pat_hash)
         pat.last_used = datetime.datetime.now(datetime.UTC)
         await self.__data.commit()
         self.__write_as.append_to_audit_log(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to