Upstream token read cannot verify ownership: ErrInvalidBinding documents a subject check the signature cannot make
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
Problem
storage.ErrInvalidBinding is documented as a caller-identity check, is returned by no
implementation, and the read it guards accepts nothing to check against.
The declaration (pkg/authserver/storage/types.go:47-49):
// ErrInvalidBinding is returned when token binding validation fails
// (e.g., subject or client ID mismatch).
ErrInvalidBinding = errors.New("storage: token binding validation failed")
The read it is documented on (types.go:565-566):
// Returns ErrInvalidBinding if binding validation fails.
GetUpstreamTokens(ctx context.Context, sessionID, providerName string) (*UpstreamTokens, error)
That signature takes a session id and a provider name. No subject, no client id. So a
subject-or-client mismatch is not detectable at this seam regardless of implementation — there
is nothing to compare a stored value against.
And no implementation returns it. RedisStorage.GetUpstreamTokens (redis.go:913) and the
in-memory equivalent both return only ErrNotFound / ErrExpired paths. The service layer
carries a mapping branch for it anyway (pkg/auth/upstreamtoken/service.go:62-63), so there is
live handling for an error nobody produces.
Why it matters
The read is keyed on a token-session id. Whoever presents a token carrying that claim gets the
credentials stored under it, and the seam performs no check that the presenting subject is the
subject those credentials were stored for. The documented error is exactly the check that is
missing, which makes the gap easy to mistake for covered — a reader following the interface
contract would reasonably conclude ownership is verified.
This becomes load-bearing as soon as tokens carrying that claim can be minted for a caller other
than the one who authorized the upstream connection.
Suggested shape
Either:
- Make the check possible. Add the caller's subject to the read, and compare it against the
stored record before returning. Then the documented error becomes reachable and means what it
says. - Or remove the claim. Delete the error and the doc comment, and record explicitly that the
session id is treated as a bearer capability with no ownership verification at this layer — so
the absence is a stated decision rather than an apparent oversight.
(1) is the safer default. (2) is honest if the check genuinely belongs at a different layer, in
which case it is worth naming which one.
Notes
Credit: the gap was described in a design doc under review in stacklok/mecatl#321 before we
confirmed it in code. Related: #6053 (the identity does not expose the upstream subject, so the
value a check would need is not currently available).
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 GetUpstreamTokens declaration in pkg/authserver/storage/types.go and compare RedisStorage.GetUpstreamTokens in redis.go with the in-memory implementation. Trace the ErrInvalidBinding handling in pkg/auth/upstreamtoken/service.go and review related issue #6053. Done means the ownership contract, storage implementations, and service-layer handling agree on whether and where verification occurs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100