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]