auth: sender-constrained tokens (DPoP / mTLS-bound) — decide what Flowstate accepts before what it presents
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 9
- Forks
- 0
- Avg merge
- 3h 3m
- Merged PRs (30d)
- 509
Description
Problem
#995 proposed DPoP and certificate-bound token modes: 472 lines of ECDSA verification, thumbprinting, canonicalization and replay logic, with no test file, no way to turn it on, and an outbound half that cannot work by construction.
Worth saying first, because it is the good part: the inbound verification core is genuinely fail-closed. Proof absent or malformed, wrong typ/alg, bad JWK, bad signature, htm/htu mismatch, stale or absent iat, empty jti, ath mismatch, no cnf.jkt on the token, replayed jti, cert-bound with no verified chain — every one denies. cfg.Replay == nil denies rather than skipping. That is the right shape and it is worth keeping.
Why it cannot land as one change
The outbound half is broken by construction. dpopExchanger.Exchange obtains a bearer token and then generates a fresh key. A relying party compares that key's thumbprint against the token's cnf.jkt; the key did not exist when the token was minted, so the binding is absent or wrong on every request. This is not a patch — the key must exist before the exchange and its proof be presented to the authorization server, which is a different design and needs the authorization-server leg.
The inbound half is unreachable. Nothing in cmd/flow calls WithTokenMode. No deployment can turn it on.
A bound on the wrong resource, and it is an availability hole. MemoryReplayStore bounds entry count (default 10000); jti is unbounded, and the key is jkt+":"+c.JTI, so the peer controls bytes-per-entry — CLAUDE.md's "bounding one resource does not bound another the peer controls the ratio to". Worse: at capacity CheckAndStore errors and the caller denies, with eviction by expiry only and no LRU. An attacker sends 10,000 distinct jti in one burst and every legitimate DPoP request is refused for maxAge+skew (5m30s by default). Fail-closed into a trivially reachable auth outage.
Configuration validated at request time. WithTokenMode accepts any string; an unknown mode produces ErrInvalidPolicy per request. TokenModeDPoP with a nil DPoPConfig is accepted at construction and denies every request forever. CLAUDE.md: rules compile and type-check when configuration loads, not when a request arrives.
CertificateHeader is never read — an advertised no-op security setting, the same shape as #985's four inert knobs.
The decision, and the split
The two halves are separable and should be separated:
- Inbound acceptance is landable on its own once it is wired to a flag, its replay store bounds bytes as well as entries and evicts rather than refusing, its mode is validated at construction, and it has tests. That is a real, self-contained feature.
- Outbound presentation should not be attempted until the authorization-server leg is designed. Presenting a binding a relying party will reject is worse than presenting none.
Smaller findings to carry
- Scheme comparison is case-sensitive; RFC 9110 auth schemes are case-insensitive. Mirror the existing bearer helper.
- No
DPoP-Nonce/use_dpop_nonceis returned, so nonce enforcement is unusable by a client that wants to comply. - The 401 says "missing bearer token" under a
DPoPchallenge. - DPoP configured against an AWS target validates and then fails per request — the same load-time/request-time defect as above.
Acceptance
The inbound feature lands alone, reachable, bounded on bytes, validated at load, and tested. Outbound waits for its own design.
Filed in place of #995.
Verified at 64234762 (origin/main).
Generated by Claude Code
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the inbound path around WithTokenMode and cmd/flow, then inspect dpopExchanger.Exchange and MemoryReplayStore; read CLAUDE.md for configuration and resource-bounding guidance. Review the existing bearer helper for scheme handling. Done means inbound acceptance is wired, byte-bounded, load-validated, and tested, while outbound presentation remains separated pending authorization-server design.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend-api-design, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100