Repository navigation
Commit 3e41e73
authored
security(exchange): stop a logged-out subject seeding new delegations (#776)
* security(exchange): stop a logged-out subject seeding new delegations
Token exchange verified a subject_token's signature, issuer and
token_type and nothing else. delegationSessionIsLive then refused the
RESULTING token — but only at Authorizer's own surfaces. A delegated
token is bound to a third-party `resource` and validated by that server,
which has no view of this session store.
So after a logout the user's agent kept minting fresh, externally valid
credentials on their behalf until the subject_token expired, and nothing
downstream could tell. The probe produced a fully-formed delegated token
for https://api.example.com/v1 from an already-logged-out subject.
Checked only when there is something to check. A sid is either
well-formed or absent — DelegationSessionID returns "" rather than a
partial value — and its absence is a documented state meaning "this
subject had no session" (CreateDelegatedAccessToken omits the claim
entirely then, so its presence always means checkable). Rejecting that
case too would delete a supported branch rather than close the gap.
Service-account subjects stay exempt: a client_credentials token has no
browser session, and its liveness comes from the IsActive check above.
Enforcing there would break every agent-to-agent hop.
The rejection reuses the opaque invalid_grant of the invalid-subject
path, so the endpoint cannot be used to probe who is currently signed in.
This changes what a third-party resource server observes, so it needs a
delegation-design decision, not just a code review — see
specs/AGENTIC_DELEGATION_DESIGN.md.
TestTokenExchangeRejectsInvalidResourceIndicator minted its subject via
CreateAuthToken without registering a session, making it revoked-shaped.
Every production path that issues an access token registers it, so the
test was the anomaly; it now registers one.
* feat(observability): audit and meter agent delegation activity
An agent's activity has to be attributable — that is what the RFC 8693
act chain is for. Success was audited and counted; every one of the
fourteen refusal paths on /oauth/token's exchange grant was silent. No
audit row, no metric, only a Debug log. The sibling client_credentials
grant already audits its failures, so machine-identity auth failures were
attributable while delegation failures — carrying a user's authority, so
the more sensitive of the two — were not. An agent probing the
delegation endpoint left no trail.
Every refusal now routes through one helper, which is also what keeps it
true: a rejection path added later cannot be silent without deliberately
bypassing it.
- audit: token.exchange_failed, actor = the calling agent, metadata = a
fixed reason constant naming the rule that refused. Never the subject
id or the token — a refusal record must not be where an unverified
subject's identity gets written.
- metric: authorizer_auth_events_total{event=token_exchange} on both
success and failure. EventTokenIssued is unchanged and still counts all
issuance, but carries no grant label, so delegated issuance was
indistinguishable from any other grant and had no failure count to form
a rate against.
- metric: security event token_exchange_rejected, reason=<constant>.
The session-liveness refusal keeps returning the same opaque
invalid_grant as a malformed token, so the endpoint is not an oracle for
who is signed in — but its audit reason differs, because that row is
written server-side and never reaches the caller.
Also meters the agent scope-ceiling enforcement point, silent on every
transport until now: delegated_insufficient_scope with separate labels
for "not on the delegated allow-list" (a client bug, or probing) and
"reachable but this token lacks the scope" (widen the ceiling, or don't).
Operators had no way to see agents hitting their ceiling, which is the
number needed before deciding whether to widen one. Method names are
deliberately not labels — high cardinality on an internet-facing path.
And meters delegated-token validation refusals, so revocation actually
firing (logout, password reset, admin revoke taking an agent's access
down with the user's session) is externally visible rather than a Debug
line.
AuditTokenExchangeEvent stays reserved and unused: success remains
token.issued, which ROADMAP_V2.md documents, and renaming the action
would silently break any query already filtering on it.1 parent 25a75fb commit 3e41e73
9 files changed
Lines changed: 553 additions & 61 deletions
File tree
- internal
- constants
- grpcsrv/interceptors
- http_handlers
- integration_tests
- metrics
- token
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
299 | 299 | | |
300 | 300 | | |
301 | 301 | | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
302 | 308 | | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
303 | 318 | | |
304 | 319 | | |
305 | 320 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
245 | 253 | | |
246 | 254 | | |
247 | 255 | | |
248 | 256 | | |
| 257 | + | |
249 | 258 | | |
250 | 259 | | |
251 | 260 | | |
| 261 | + | |
252 | 262 | | |
253 | 263 | | |
254 | 264 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
268 | 268 | | |
269 | 269 | | |
270 | 270 | | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
271 | 279 | | |
272 | 280 | | |
273 | 281 | | |
274 | 282 | | |
| 283 | + | |
275 | 284 | | |
276 | 285 | | |
277 | 286 | | |
| 287 | + | |
278 | 288 | | |
279 | 289 | | |
280 | 290 | | |
| |||
0 commit comments