feat(api): platform API keys for headless webhook management (#376)#579
feat(api): platform API keys for headless webhook management (#376)#579ClintEastman02 wants to merge 5 commits into
Conversation
…ples#376) Add long-lived, scoped platform API keys so CI/automation can manage webhooks without a Cognito user. A unified REQUEST authorizer accepts either a Cognito JWT or a `webhooks:manage` API key on /v1/webhooks*; new Cognito-gated /v1/api-keys endpoints mint/list/revoke keys. Keys are `bgak_<key_id>_<secret>` with only the secret hash stored in DynamoDB (direct GetItem by key_id, no GSI staleness on revoke). CLI gains `bgagent api-key` commands and `--api-key`/BGAGENT_API_KEY injection.
…pi-keys # Conflicts: # cdk/package.json # cli/src/format.ts
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #579 +/- ##
=======================================
Coverage ? 89.52%
=======================================
Files ? 231
Lines ? 55517
Branches ? 5025
=======================================
Hits ? 49700
Misses ? 5817
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
isadeks
left a comment
There was a problem hiding this comment.
Review — Platform API keys for headless webhook management (#376)
Reviewed by reading + running the code at the merge head (9f2a5e9): the 7 new security suites pass (80 tests), and I verified the auth-critical paths empirically. The security core is sound — this is a careful design. Findings are minor + process, no blocking security defect. (Draft, so treating this as a pre-merge pass.)
Security core — checked, sound
- Key hashing — a 256-bit
crypto.randomBytessecret, stored as a plain SHA-256 hex digest. Correct for a high-entropy random token: there's nothing to brute-force, so bcrypt/argon aren't needed (and would only slow the hot auth path). Plaintext is returned once; only the hash is persisted (test asserts the secret never lands in the Put item). - Constant-time compare —
timingSafeHashEquallength-guards thencrypto.timingSafeEqual; both operands are fixed-length 64-hex, so the length branch isn't a timing signal. Good. - Custom authorizer doesn't downgrade the native one — it uses
aws-jwt-verifywithclientId+tokenUse: 'id', which validates signature/exp/iss/aud/token_use per request (the CLI sendsid_token, so the token type matches). The nativeCognitoUserPoolsAuthorizerit replaces on/webhooks*had no scope checks to lose. The HMAC invoke route (/webhooks/tasks) is untouched (webhookAuthOptions), andrequestValidatoris body-only. - Revocation is immediate — strongly-consistent
GetItembykey_id(no GSI window), andstatus !== 'active'→ Deny. Ownership on delete returns 404 not 403 (no existence oracle), matching the webhook pattern. parseApiKey3-part split is safe —key_idis a ULID (Crockford base32, no_) and the secret is hex, sobgak_<id>_<secret>always splits cleanly; malformed input denies without a lookup.X-API-Keypreferred overAuthorization; unexpected errors fail closed to Deny (tested).- TTL/expiry — active keys carry no
ttl(expire only viaexpires_at); revoked keys get a 30dttland are denied by status before deletion, and by absence after. No auth gap in the window. - Verbose-log redaction —
key+x-api-keyadded toSENSITIVE_FIELDS. Good catch by the author.
Minor
- 1.
list-api-keysdefault page can return short/empty results with anext_token(pagination UX) —list-api-keys.ts:72sendsLimitandFilterExpressiontogether. DynamoDB appliesLimitto scanned items before the filter, so a user with revoked keys can get a page of <20 (or 0) active keys whilehas_moreis true — a confusing "empty but paginated" result. Not a security or correctness bug (the filter is still applied; nothing leaks). Consider over-fetching then trimming tolimit, or documenting that callers must follownext_tokenuntil it's null. - 2.
key_hashis read then stripped, not projected out —list-api-keysqueries withProjectionType.ALL(GSI) and relies ontoApiKeyDetailto dropkey_hash/user_id. Correct today, but a future response-shape change could leak the hash. AProjectionExpression/GSI projection excludingkey_hashwould be defense-in-depth. Low priority (the hash isn't the secret, but least-exposure is cheap here).
Process (author already flagged both — confirming)
- 3. Governance gate not met. #376 has no
approvedlabel (labels: enhancement/orchestration/cli/security/P0) and no "Starting implementation" comment — the author flagged this per ADR-003. An admin should confirm approval before this leaves draft. This is a P0 security surface (a new long-lived credential type), so the governance step matters more than usual. - 4. Rebase. The head is now a merge commit (
9f2a5e9) and GitHub showsMERGEABLE, so thecdk/package.json/cli/src/format.tsconflicts the description warns about appear resolved — worth confirming the description's "Draft because…" note is now stale and flipping to ready if an approval lands.
Design notes (non-blocking)
- Scopes vocabulary ships all four values but only
webhooks:manageis wired;tasks:read/tasks:cancelvalidate but gate nothing yet (documented as Phase 2). Fine — minting keys now against a stable vocabulary is the right call. api-key create/list/revokeare Cognito-only (no--api-keyflag), correctly — minting a credential shouldn't be possible with a credential. Consistent with the endpoints being Cognito-gated.
Recommendation: no blocking security issue — the crypto, authorizer, and revocation model are all correct and well-tested. Land the governance approval (#3) first; fold in #1 (or document the pagination contract) and optionally #2; confirm the rebase note is stale. Nicely scoped Phase 1.
Summary
Closes #376.
Adds platform API keys as a first-class auth mechanism so CI/automation can create and manage webhooks without a Cognito user. Webhook invocation was already Cognito-free (HMAC), but webhook setup still required a Cognito JWT — this closes that gap for headless flows (GitHub Actions, workshops, service-to-service).
What changed
/v1/webhooks*accepts either a Cognito JWT or awebhooks:manageAPI key. Existing Cognito flows are unchanged./v1/api-keysendpoints (Cognito-gated) to mint / list / revoke keys.bgak_<key_id>_<secret>. Only the secret hash is stored in DynamoDB (ApiKeyTable); lookup is a directGetItembykey_id, so revocation has no GSI staleness window.bgagent api-key create|list|revoke, plus--api-keyflag /BGAGENT_API_KEYenv injection so headless callers skipbgagent login.Scope
Phase 1 only (webhook management via keys). Broader
tasks:read/tasks:cancelscoping and replacing Cognito for interactive CLI are explicitly out of scope for v1, per the issue.Testing
mise //cdk:test— 2261 pass (incl. newapi-key-table,api-key-authorizer,create/list/delete-api-key,shared/api-key,gatewaytests)mise //cli:test— 575 pass (incl.api-keycommand +api-clienttests)mise //docs:sync— no driftscripts/check-types-sync.ts— CDK ↔ CLI type sync OKNotes for reviewers
mainhas advanced ~40 commits since this branched; two files conflict on rebase and need resolution before this is review-ready:cdk/package.json(dependency block)cli/src/format.tsapprovedlabel and has no "Starting implementation" comment (ADR-003). Flagging so an admin can confirm approval before this leaves draft.🤖 Generated with Claude Code