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]

Reply via email to