aws-samples / aws-samples/sample-autonomous-cloud-coding-agents

feat(observability): Linear OAuth token-lineage logging for revocation investigation

Open
#807 1 comment 0 reactions 1 assignee Claimed by @isadeks View on GitHub
approved enhancement P1 security
Dominant language
TypeScript
Stars
143
Forks
46
Avg merge
3d 9h
Merged PRs (30d)
20

Description

## Summary

Add resolver-local **diagnostic logging** to the Linear OAuth refresh path
(`cdk/src/handlers/shared/linear-oauth-resolver.ts`) so we can investigate a recurring
per-workspace grant revocation. Additive observability only — **no behavior change** to the refresh
or persist logic.

## Motivation

One Linear workspace's OAuth grant is being revoked ~25h after each (re-)onboard: the platform's
refresh returns `invalid_grant / "Refresh token revoked"`. A second workspace on the identical code
path is durable, so the refresh code itself is sound (verified: the rotated `refresh_token` is
persisted on success). The cause is upstream/app-side, but today's logs can't reconstruct the
token's lifecycle to prove *what* Linear is rejecting — we can't distinguish "a spent/rotated token
was replayed" from "a live grant was killed server-side," nor correlate a failure to the token we
last persisted, nor see the token's age at death.

## Proposed logging (resolver-local, one file)

Threaded through `refreshLinearToken` / `tryRefreshOnce` / the persist path:

1. **Refresh-token fingerprint** — `sha256(refresh_token).slice(0,12)`, logged on every refresh
outcome. Identifies *which* token across events; the raw token is **never** logged.
2. **Token age** — `token_age_h` (now − `installed_at`) and `since_last_refresh_h`
(now − `updated_at`) on each outcome. Both fields already exist on the stored bundle; no schema
change.
3. **Rotation trail** — on successful persist, log `rotated → `, so the next
failure can be correlated to the token we just wrote (server-side kill vs stale/race).
4. **Revocation forensics line** — at "permanently rejected," one structured line:
`{workspace_id, refresh_token_fp, token_age_h, since_last_refresh_h, installed_at, updated_at}`.

## Non-goals

- No change to refresh/persist behavior, cache, or the `invalid_grant` re-read/race handling.
- No trigger/caller threading into other handlers (kept resolver-local to bound blast radius on a
security-sensitive credential file; can follow up if the fingerprint+age data proves insufficient).
- Never log token material — fingerprints (truncated sha256) only.

## Acceptance criteria

- [ ] Every refresh outcome (success, `invalid_grant`, failure, permanently-rejected) logs the
refresh-token fingerprint + `token_age_h` + `since_last_refresh_h`.
- [ ] Successful persist logs the old→new fingerprint rotation.
- [ ] No raw token/secret value appears in any log (verified against the masking gate).
- [ ] No behavior change: existing resolver tests pass unchanged; new tests assert the fingerprint
is a truncated hash and never the raw token.

## Security / priority

Diagnostic-only, additive; touches a credential path so reviewed accordingly (masking gate). Suggest
**P1** — it unblocks an active, recurring auth outage investigation.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.