asf-tooling commented on issue #335:
URL:
https://github.com/apache/tooling-trusted-releases/issues/335#issuecomment-4410375933
<!-- gofannon-issue-triage-bot v2 -->
**Automated triage** — analyzed at `main@2da7807a`
**Type:** `discussion` • **Classification:** `no_action` •
**Confidence:** `high`
**Application domain(s):** `authentication_authorization`,
`cryptographic_keys`
### Summary
This is an open-ended design discussion about whether to adopt DPoP (RFC
9449) for API tokens before ATR sees widespread use. @sbp's analysis
(2025-11-25) identified several unresolved blockers: (1) Authentik (planned
OAuth server) doesn't yet support DPoP, (2) no suitable Python DPoP package has
been found that works without full OAuth integration, and (3) client complexity
concerns for non-CLI users. The team agreed DPoP would be for API tokens only
(not OAuth sessions), but no concrete implementation decision was reached. The
current bearer-token JWT system in atr/jwtoken.py remains the active mechanism.
### Where this lives in the code today
#### `atr/jwtoken.py` — `issue` (lines 71-89)
_needs modification_
This is the JWT issuance function that would need DPoP proof binding (cnf
claim with jkt thumbprint) if DPoP were adopted.
```python
def issue(uid: str, *, ttl: int = _ATR_JWT_TTL, pat_hash: str | None = None)
-> str:
# audit_guidance no explicit typ header or token_type claim is added:
the aud claim (_ATR_JWT_AUDIENCE)
# already acts as an explicit token type discriminator, and ATR issues
only one JWT type verified
# by a single internal verifier — the RFC 9068 typ header is relevant to
multi-issuer OAuth2 RS
# deployments, which this is not
now = datetime.datetime.now(tz=datetime.UTC)
payload = {
"sub": uid,
"iss": _ATR_JWT_ISSUER,
"aud": _ATR_JWT_AUDIENCE,
"iat": now,
"nbf": now,
"exp": now + datetime.timedelta(seconds=ttl),
"jti": secrets.token_hex(128 // 8),
}
if pat_hash:
payload["atr_th"] = pat_hash
log.auth_event("jwt_issuance", uid, pat_hash=pat_hash if pat_hash else
None)
return jwt.encode(payload, _signing_key(), algorithm=_ALGORITHM)
```
#### `atr/jwtoken.py` — `_extract_bearer_token` (lines 238-245)
_needs modification_
With DPoP, the Authorization scheme changes from 'Bearer' to 'DPoP', and a
separate DPoP header containing the proof JWT must also be extracted and
verified.
```python
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(
"Authentication required. Please provide a valid Bearer token in
the Authorization header", errorcode=401
)
return token
```
#### `atr/jwtoken.py` — `verify` (lines 125-138)
_needs modification_
Token verification would need to additionally validate the DPoP proof (check
signature, nonce, ath binding, jkt thumbprint matching cnf claim).
```python
async def verify(token: str) -> dict[str, Any]:
jwt_secret_key = _signing_key()
claims_unsafe = jwt.decode(token, options={"verify_signature": False},
algorithms=[_ALGORITHM])
asf_uid = claims_unsafe.get("sub")
log.set_asf_uid(asf_uid)
claims = jwt.decode(
token,
jwt_secret_key,
algorithms=[_ALGORITHM],
issuer=_ATR_JWT_ISSUER,
audience=_ATR_JWT_AUDIENCE,
leeway=_ATR_JWT_LEEWAY_SECONDS,
options={"require": ["sub", "iss", "aud", "iat", "nbf", "exp",
"jti"]},
)
```
#### `atr/storage/writers/tokens.py` — `FoundationCommitter.issue_jwt`
(lines 113-134)
_needs modification_
JWT issuance from PAT would need to accept a DPoP proof and bind the issued
token to the client's public key via a cnf/jkt claim.
```python
async def issue_jwt(self, pat_text: str) -> str:
pat_hash = hashlib.sha3_256(pat_text.encode()).hexdigest()
pat = await self.__data.query_one_or_none(
sqlmodel.select(sql.PersonalAccessToken).where(
sql.PersonalAccessToken.asfuid == self.__asf_uid,
sql.PersonalAccessToken.token_hash == pat_hash,
)
)
if (pat is None) or (pat.expires <
datetime.datetime.now(datetime.UTC)):
log.warning(
"Authentication failed",
extra={
"reason": "invalid_or_expired_pat",
},
)
raise storage.AccessError("Authentication failed", status=401)
# Verify account still exists in LDAP
account_details = await ldap.account_lookup(self.__asf_uid)
if (account_details is None) or ldap.is_banned(account_details):
log.auth_failure("jwt_issuance", "account_deleted_or_banned",
self.__asf_uid)
raise storage.AccessError("Authentication failed", status=401)
```
#### `atr/shared/tokens.py` — `IssueForm` (lines 48-49)
_needs modification_
The token issuance form would need to accept DPoP proof header or public key
registration data instead of (or in addition to) raw PAT text.
```python
class IssueForm(form.Form):
pat: str = form.label("PAT", widget=form.Widget.TEXT)
```
#### `atr/post/tokens.py` — `jwt_post` (lines 37-50)
_needs modification_
The JWT issuance endpoint would need DPoP proof validation and token binding
per RFC 9449 § 4.2.
```python
@post.typed
@rate_limiter.rate_limit(10, datetime.timedelta(hours=1))
async def jwt_post(
session: web.Committer, _tokens_jwt: Literal["tokens/jwt"], form:
shared.tokens.IssueForm
) -> web.QuartResponse:
"""
URL: /tokens/jwt
"""
async with storage.write(session) as write:
wafc = write.as_foundation_committer()
jwt_token = await wafc.tokens.issue_jwt(form.pat)
response = web.TextResponse(jwt_token)
response.headers["Cache-Control"] = "no-store"
return response
```
### Where new code would go
- `atr/dpop.py` — new file
DPoP proof generation, verification, nonce management, and JWK thumbprint
computation would belong in a dedicated module.
- `atr/models/sql.py` — after PersonalAccessToken model
A new table would be needed to store registered DPoP public keys (Ed25519
JWK) associated with users, and to track server-issued nonces.
### Proposed approach
This issue is still in the discussion/evaluation phase with unresolved
blockers. @sbp identified (2025-11-25) that Authentik doesn't support DPoP and
that no suitable standalone Python DPoP library was found. The team agreed DPoP
would only apply to API tokens (RFC 9449 § 7), not web sessions. Before
implementation can proceed, the team needs to: (1) decide whether to implement
DPoP server-side logic internally or wait for Authentik/library support, (2)
resolve the RFC 9449 § 5 ambiguity about whether OAuth issuance is normatively
required vs. the § 6 escape clause, and (3) accept the client complexity
trade-off for non-CLI users.
If implementation proceeds, the changes would involve: adding a DPoP proof
verification module, modifying JWT issuance to bind tokens to client public
keys via cnf/jkt claims, changing the Authorization scheme from Bearer to DPoP,
implementing server nonce rotation (§ 8), and adding public key registration
endpoints. The current PAT system could remain as the authentication method for
token issuance, with DPoP layered on top for proof-of-possession of access
tokens.
### Open questions
- Has Authentik added DPoP support since the last discussion (Dec 2025)?
- Has a suitable Python DPoP library been identified that works without full
OAuth integration?
- Should ATR implement DPoP server-side logic internally, or wait for
upstream support?
- Is RFC 9449 § 5 (OAuth issuance requirement) normative, or does § 6 allow
alternative public key association?
- What is the timeline for 'widespread use of ATR' that would lock in the
token format?
- Would a phased approach work - e.g., support both Bearer and DPoP during a
transition period?
_The agent reviewed this issue and is not proposing patches in this run.
Review the existing-code citations and open questions above before deciding
next steps._
### Files examined
- `atr/jwtoken.py`
- `atr/post/tokens.py`
- `atr/storage/writers/tokens.py`
- `atr/get/tokens.py`
- `atr/storage/readers/tokens.py`
- `atr/shared/tokens.py`
- `atr/principal.py`
- `atr/ssh.py`
---
*Draft from a triage agent. A human reviewer should validate before merging
any change. The agent did not run tests or verify diffs apply.*
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]