feat(authbridge): credential placeholder swap (hide real tokens from the agent)#464
Conversation
Design for keeping real tokens out of the agent: jwt-validation mints an opaque placeholder on inbound, token-exchange resolves it on egress before exchange. Covers the reusable shared store, per-listener inbound header propagation, plugin config, end-to-end data flow with failure branches, security properties, and listener/deployment compatibility (reverse+forward sidecar and Envoy mode). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Ten TDD tasks covering the shared store, placeholder convention, SharedStore pipeline seam, jwt-validation mint mode, token-exchange resolve step, per-listener inbound propagation (reverseproxy + extproc), main wiring, and docs. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…proxy Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
… envoy Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…ion tests Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
TTL is a configured placeholder_ttl (default 1h), not the token exp; eviction is lazy-only in v1 (periodic sweep deferred). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a process-scoped TTL SharedStore, a placeholder handle convention, jwt-validation minting of opaque placeholders, token-exchange resolution, listener and main wiring to propagate Authorization mutations, unit/listener/e2e tests, documentation, and an echo demo. ChangesCredential Placeholder Swap Feature Implementation
Sequence Diagram(s) sequenceDiagram
participant Client
participant ReverseProxy
participant JWTValidation
participant SharedStore
participant AgentBackend
participant ForwardProxy
participant TokenExchange
participant Upstream
Client->>ReverseProxy: HTTP request (Authorization: Bearer realJWT)
ReverseProxy->>JWTValidation: run inbound pipeline (pctx.Shared set)
JWTValidation->>SharedStore: Put(placeholderKey, realJWT, ttl) -- mint
JWTValidation-->>ReverseProxy: mutated Authorization: Bearer abph_<handle>
ReverseProxy->>AgentBackend: forward request (Authorization: Bearer abph_<handle>)
AgentBackend->>ForwardProxy: outbound call with Authorization: Bearer abph_<handle>
ForwardProxy->>TokenExchange: run outbound pipeline (pctx.Shared set)
TokenExchange->>SharedStore: Get(placeholderKey) -- resolve
TokenExchange-->>ForwardProxy: replace Authorization with Bearer EXCHANGED-TOKEN
ForwardProxy->>Upstream: forward request with exchanged token
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
authbridge/authlib/plugins/tokenexchange/plugin.go (1)
572-578: 💤 Low valueMinor: extract bearer once to avoid duplicate parsing.
auth.ExtractBearer(authHeader)is called twice—once forIsPlaceholderand again forresolvePlaceholder. Extracting once avoids the redundant string parsing.♻️ Suggested refactor
+ bearer := auth.ExtractBearer(authHeader) - if p.cfg.ResolvePlaceholders && placeholder.IsPlaceholder(auth.ExtractBearer(authHeader)) { - real, ok := resolvePlaceholder(pctx, auth.ExtractBearer(authHeader)) + if p.cfg.ResolvePlaceholders && placeholder.IsPlaceholder(bearer) { + real, ok := resolvePlaceholder(pctx, bearer) if !ok { return pctx.DenyAndRecord("placeholder_unresolved", "auth.unauthorized", "unresolvable credential placeholder") } authHeader = "Bearer " + real }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@authbridge/authlib/plugins/tokenexchange/plugin.go` around lines 572 - 578, Extract the bearer token once into a local variable (e.g., token := auth.ExtractBearer(authHeader)) before the if; then use that token for placeholder.IsPlaceholder and resolvePlaceholder calls, and when resolved set authHeader = "Bearer "+real; update the conditional to check p.cfg.ResolvePlaceholders && placeholder.IsPlaceholder(token) and pass token to resolvePlaceholder, keeping pctx.DenyAndRecord unchanged for the failure path.authbridge/cmd/authbridge-proxy/main.go (1)
255-255: Consider a background eviction sweep for the shared store.The store created here relies on lazy eviction (entries are only purged on a subsequent
Get). Handles minted on the inbound path for passthrough/off-route hosts are never resolved by outbound exchange, so their keys are neverGet-ed and never expire — they accumulate for the process lifetime. Under sustained inbound traffic this is unbounded memory growth, not bounded by TTL.Since this entry point owns the store's lifecycle, consider starting a periodic janitor goroutine (or adding one in
authlib/shared) to sweep expired entries, and stopping it during the shutdown sequence. This applies equally to authbridge-lite and authbridge-envoy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@authbridge/cmd/authbridge-proxy/main.go` at line 255, The shared store created with shared.New() uses lazy eviction and can leak unreferenced keys; add a background janitor goroutine started in main (right after shared.New()) that periodically calls the store's expiry sweep method (e.g., sharedStore.SweepExpired / sharedStore.EvictExpired or a new Sweep method you add in authlib/shared) on a configurable interval, and ensure the goroutine is cleanly stopped during the program shutdown sequence (use a context with cancel or a stop channel tied into your existing shutdown logic in main) so the janitor terminates before sharedStore is closed; if authlib/shared lacks a sweep API, implement a Sweep/Prune method there and call it from the janitor.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@authbridge/authlib/listener/extproc/server.go`:
- Around line 164-167: The mint/placeholder response helpers
replaceTokenResponse and replaceTokenBodyResponse currently set the new
Authorization header but do not remove the x-authbridge-direction header,
allowing that direction header to leak; update both functions
(replaceTokenResponse and replaceTokenBodyResponse) to include RemoveHeaders:
[]string{"x-authbridge-direction"} inside the created extprocv3.HeaderMutation
so the direction header is explicitly removed on the mint/placeholder path.
In `@authbridge/authlib/shared/store.go`:
- Around line 30-57: The store currently only evicts expired entries lazily in
Store.Get, which leads to unbounded growth; add a periodic background sweeper
started when the Store is created (e.g. in New) that scans s.items and deletes
entries whose expires <= s.now(), and/or change the resolution path to ensure
consumed entries are deleted (e.g. make Get or the handle-resolution logic call
Store.Delete after successful resolution); ensure the sweeper uses s.mu (Lock)
when mutating s.items and stops/cleans up on Store shutdown to avoid goroutine
leaks (reference Store.Get, Store.Put, Store.Delete and the Store constructor to
locate changes).
- Around line 38-50: Get currently RLocks, releases, then calls Delete which can
remove a concurrently Put value; to fix, after detecting the entry is expired
under the read lock, acquire the write lock and re-check the current map
entry/expiry before deleting. In practice change Store.Get to: read under
mu.RLock as now, and if e exists and appears expired then mu.RUnlock();
mu.Lock(); re-fetch e, ok := s.items[key]; if !ok or s.now().After(e.expires)
then delete and return nil,false; otherwise return e.val,true; ensure you
reference Store.Get, s.mu (RLock/Lock), s.items, s.now(), Delete/Put to locate
the code paths to update.
In `@authbridge/docs/superpowers/plans/2026-06-02-credential-placeholder-swap.md`:
- Line 13: The markdown line currently uses a machine-specific absolute path
string (the text starting with
"/Users/haihuang/works/go/src/github.com/kagenti/kagenti-extensions/authbridge")—replace
that with a repo-relative working-directory instruction instead (for example:
"Run from the authbridge/ repo root" or "All commands run from authbridge/") in
the file 2026-06-02-credential-placeholder-swap.md so contributors and CI have a
portable, non-personalized instruction.
In
`@authbridge/docs/superpowers/specs/2026-06-02-credential-placeholder-swap-design.md`:
- Around line 5-6: Update the top-level "Scope" statement in the spec to
consistently declare the v1 implementation boundary: explicitly list the
components included (jwt-validation plugin, token-exchange plugin, the new
shared store, and the reverseproxy change) and remove or reconcile any mentions
of extproc/extauthz/waypoint that contradict the v1 deferral language; ensure
the same single v1 scope text is used in all locations currently inconsistent
(including the blocks around lines referencing "extauthz/waypoint" and the other
two ranges) and add the behavior note about defaults: AuthBridge defaults to
passthrough outbound policy, token exchange only occurs for hosts in the
authproxy-routes ConfigMap, and set DEFAULT_OUTBOUND_POLICY: "exchange" to
restore legacy behavior.
---
Nitpick comments:
In `@authbridge/authlib/plugins/tokenexchange/plugin.go`:
- Around line 572-578: Extract the bearer token once into a local variable
(e.g., token := auth.ExtractBearer(authHeader)) before the if; then use that
token for placeholder.IsPlaceholder and resolvePlaceholder calls, and when
resolved set authHeader = "Bearer "+real; update the conditional to check
p.cfg.ResolvePlaceholders && placeholder.IsPlaceholder(token) and pass token to
resolvePlaceholder, keeping pctx.DenyAndRecord unchanged for the failure path.
In `@authbridge/cmd/authbridge-proxy/main.go`:
- Line 255: The shared store created with shared.New() uses lazy eviction and
can leak unreferenced keys; add a background janitor goroutine started in main
(right after shared.New()) that periodically calls the store's expiry sweep
method (e.g., sharedStore.SweepExpired / sharedStore.EvictExpired or a new Sweep
method you add in authlib/shared) on a configurable interval, and ensure the
goroutine is cleanly stopped during the program shutdown sequence (use a context
with cancel or a stop channel tied into your existing shutdown logic in main) so
the janitor terminates before sharedStore is closed; if authlib/shared lacks a
sweep API, implement a Sweep/Prune method there and call it from the janitor.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5c67bbb6-ddec-4563-afc7-3cb44ca027c3
📒 Files selected for processing (21)
authbridge/authlib/listener/extproc/placeholder_test.goauthbridge/authlib/listener/extproc/server.goauthbridge/authlib/listener/forwardproxy/server.goauthbridge/authlib/listener/reverseproxy/placeholder_test.goauthbridge/authlib/listener/reverseproxy/server.goauthbridge/authlib/pipeline/context.goauthbridge/authlib/pipeline/sharedstore_test.goauthbridge/authlib/placeholder/placeholder.goauthbridge/authlib/placeholder/placeholder_test.goauthbridge/authlib/plugins/jwtvalidation/placeholder_test.goauthbridge/authlib/plugins/jwtvalidation/plugin.goauthbridge/authlib/plugins/tokenexchange/placeholder_test.goauthbridge/authlib/plugins/tokenexchange/plugin.goauthbridge/authlib/shared/store.goauthbridge/authlib/shared/store_test.goauthbridge/cmd/authbridge-envoy/main.goauthbridge/cmd/authbridge-lite/main.goauthbridge/cmd/authbridge-proxy/main.goauthbridge/docs/plugin-reference.mdauthbridge/docs/superpowers/plans/2026-06-02-credential-placeholder-swap.mdauthbridge/docs/superpowers/specs/2026-06-02-credential-placeholder-swap-design.md
Wire the inbound reverseproxy (jwt-validation placeholder_mode) and the outbound forwardproxy (token-exchange resolve_placeholders) through a single shared store in one process, and prove via real HTTP round-trips that the agent only ever sees an opaque abph_ handle while the upstream receives the exchanged token. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…er swap Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
authbridge/demos/echo/scripts/echo-merge.py (1)
41-81: ⚡ Quick winAdd type hints to merge functions.
The coding guidelines require Python 3.12+ syntax with type hints. While
main()has a return type hint,merge_inboundandmerge_outboundare missing parameter and return type hints.✏️ Proposed type hints
-def merge_inbound(plugins, jwt_cfg): +def merge_inbound(plugins: list[dict], jwt_cfg: dict) -> None: """Set placeholder_mode / placeholder_ttl on every jwt-validation plugin entry's config. Creates the config map if missing.""" -def merge_outbound(plugins, te_cfg, routes): +def merge_outbound(plugins: list[dict], te_cfg: dict, routes: list[dict]) -> None: """Set resolve_placeholders on every token-exchange plugin entry and append the patch's routes into config.routes.rules (by `host`)."""As per coding guidelines: "Use Python 3.12+ syntax, including type hints with
str | Nonenotation"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@authbridge/demos/echo/scripts/echo-merge.py` around lines 41 - 81, Add Python 3.12+ type hints to both functions: change merge_inbound signature to accept plugins: list[dict[str, object]] and jwt_cfg: dict[str, object] and return None, and change merge_outbound signature to accept plugins: list[dict[str, object]], te_cfg: dict[str, object], routes: list[dict[str, object]] and return None; use built-in generic types (PEP 585) and Python 3.12 union syntax where applicable so callers of merge_inbound and merge_outbound (look for those exact function names) get explicit parameter/return typing.authbridge/demos/echo/agent/main.go (1)
1-398: 💤 Low valueConsider using
log/slogfor consistency.The coding guidelines specify using
log/slogfor logging throughout authbridge binaries. This demo agent uses the standardlogpackage. While functional, switching toslogwould maintain consistency with the rest of the authbridge codebase and provide structured logging benefits.As per coding guidelines: "Use
log/slogfor logging throughout authlib and authbridge binaries; binaries log under their own name (authbridge-proxy, authbridge-envoy, authbridge-lite)"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@authbridge/demos/echo/agent/main.go` around lines 1 - 398, Replace use of the standard log package with log/slog: add import "log/slog", add a package-level logger (e.g., logger := slog.New(slog.NewTextHandler(os.Stderr))) and update all logging calls (log.Printf, log.Fatalf, log.Printf in buildProxiedClient/handleAgentCard/handleA2A/runEcho/callUpstreamEcho/main/writeRPCSuccess/writeRPCError/newUUID) to use logger.Info or logger.Error (passing context.Background() as the first arg) and where log.Fatalf was used, call logger.Error(...) then os.Exit(1) to preserve exit behavior.authbridge/demos/echo/scripts/setup_keycloak.py (1)
100-193: 💤 Low valueAdd type hints to helper functions.
Per coding guidelines, Python code in
authbridge/must use Python 3.12 syntax with type hints. Onlyget_spiffe_idhas annotations; the remaining helpers should follow suit.♻️ Example type annotations
-def get_or_create_realm(keycloak_admin, realm_name): +def get_or_create_realm(keycloak_admin: KeycloakAdmin, realm_name: str) -> None: -def get_or_create_client(keycloak_admin, client_payload): +def get_or_create_client(keycloak_admin: KeycloakAdmin, client_payload: dict) -> str | None: -def get_or_create_client_scope(keycloak_admin, scope_payload): +def get_or_create_client_scope(keycloak_admin: KeycloakAdmin, scope_payload: dict) -> str: -def add_audience_mapper(keycloak_admin, scope_id, mapper_name, audience): +def add_audience_mapper(keycloak_admin: KeycloakAdmin, scope_id: str, mapper_name: str, audience: str) -> None: -def get_or_create_user(keycloak_admin, user_config): +def get_or_create_user(keycloak_admin: KeycloakAdmin, user_config: dict) -> str:As per coding guidelines: "Python code must use Python 3.12 syntax with type hints".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@authbridge/demos/echo/scripts/setup_keycloak.py` around lines 100 - 193, Add Python 3.12 type hints to the helper functions: annotate parameters and return types for get_or_create_realm(keycloak_admin, realm_name) -> None, get_or_create_client(keycloak_admin, client_payload: dict[str, Any]) -> str, get_or_create_client_scope(keycloak_admin, scope_payload: dict[str, Any]) -> str, add_audience_mapper(keycloak_admin, scope_id: str, mapper_name: str, audience: str) -> None, and get_or_create_user(keycloak_admin, user_config: dict[str, Any]) -> str; also add the necessary typing imports (e.g., from typing import Any) and use Python 3.12 union syntax where appropriate if you choose to allow Optional returns. Ensure the function signatures in this file match these annotations and update any docstrings/comments if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@authbridge/demos/echo/agent/main.go`:
- Line 1: The go.work file is missing the demo module, so CodeQL can't analyze
authbridge/demos/echo; open authbridge/go.work and add the demo module to the
use(...) block (for example add a line like use ./demos/echo) so the workspace
includes the demo's go.mod and then re-run CodeQL extraction; target the go.work
file and ensure the new entry covers the module for the authbridge/demos/echo
demo.
---
Nitpick comments:
In `@authbridge/demos/echo/agent/main.go`:
- Around line 1-398: Replace use of the standard log package with log/slog: add
import "log/slog", add a package-level logger (e.g., logger :=
slog.New(slog.NewTextHandler(os.Stderr))) and update all logging calls
(log.Printf, log.Fatalf, log.Printf in
buildProxiedClient/handleAgentCard/handleA2A/runEcho/callUpstreamEcho/main/writeRPCSuccess/writeRPCError/newUUID)
to use logger.Info or logger.Error (passing context.Background() as the first
arg) and where log.Fatalf was used, call logger.Error(...) then os.Exit(1) to
preserve exit behavior.
In `@authbridge/demos/echo/scripts/echo-merge.py`:
- Around line 41-81: Add Python 3.12+ type hints to both functions: change
merge_inbound signature to accept plugins: list[dict[str, object]] and jwt_cfg:
dict[str, object] and return None, and change merge_outbound signature to accept
plugins: list[dict[str, object]], te_cfg: dict[str, object], routes:
list[dict[str, object]] and return None; use built-in generic types (PEP 585)
and Python 3.12 union syntax where applicable so callers of merge_inbound and
merge_outbound (look for those exact function names) get explicit
parameter/return typing.
In `@authbridge/demos/echo/scripts/setup_keycloak.py`:
- Around line 100-193: Add Python 3.12 type hints to the helper functions:
annotate parameters and return types for get_or_create_realm(keycloak_admin,
realm_name) -> None, get_or_create_client(keycloak_admin, client_payload:
dict[str, Any]) -> str, get_or_create_client_scope(keycloak_admin,
scope_payload: dict[str, Any]) -> str, add_audience_mapper(keycloak_admin,
scope_id: str, mapper_name: str, audience: str) -> None, and
get_or_create_user(keycloak_admin, user_config: dict[str, Any]) -> str; also add
the necessary typing imports (e.g., from typing import Any) and use Python 3.12
union syntax where appropriate if you choose to allow Optional returns. Ensure
the function signatures in this file match these annotations and update any
docstrings/comments if needed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0053c2cb-d54e-4497-95c9-bbf5ff90ffa7
📒 Files selected for processing (14)
authbridge/demos/echo/.gitignoreauthbridge/demos/echo/Makefileauthbridge/demos/echo/README.mdauthbridge/demos/echo/agent/Dockerfileauthbridge/demos/echo/agent/main.goauthbridge/demos/echo/go.modauthbridge/demos/echo/k8s/agent.yamlauthbridge/demos/echo/k8s/echo-patch.yamlauthbridge/demos/echo/k8s/upstream.yamlauthbridge/demos/echo/scripts/echo-merge.pyauthbridge/demos/echo/scripts/patch-echo-config.shauthbridge/demos/echo/scripts/setup_keycloak.pyauthbridge/demos/echo/upstream/Dockerfileauthbridge/demos/echo/upstream/main.go
✅ Files skipped from review due to trivial changes (5)
- authbridge/demos/echo/.gitignore
- authbridge/demos/echo/go.mod
- authbridge/demos/echo/k8s/echo-patch.yaml
- authbridge/demos/echo/agent/Dockerfile
- authbridge/demos/echo/k8s/upstream.yaml
…idecar Mirrors load-images so the podman+kind provider leaves the operator-referenced bare tag resolvable; without it the injected sidecar tries to pull and ImagePullBackOffs. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
The router compiles route globs with '.' as a separator, so a bare 'echo-upstream' host never matches the in-cluster FQDN the agent dials; the request passed through unresolved. Use the full echo-upstream.team1.svc.cluster.local so the outbound resolve+exchange fires. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…TL sweeper, fix Get race Addresses PR review: x-authbridge-direction leaked to the agent on the extproc mint path; shared store had unbounded growth (lazy-only eviction) and a Get/Put eviction race. Adds a background janitor with Close wired into the listener mains. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…nalize plan paths Addresses PR review: align the spec's top-level scope with the per-listener and files-touched sections (reverseproxy + extproc shipped, extauthz deferred); replace an absolute developer path in the plan with a repo-relative instruction. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
pdettori
left a comment
There was a problem hiding this comment.
Excellent security-critical feature with strong design principles: fail-closed error handling, real tokens never forwarded to agents, 256-bit CSPRNG placeholders, proper TOCTOU mitigation in the store, and comprehensive E2E + unit test coverage.
All 21 commits signed-off. CI fully green. No secrets in production code. Config fields consistently named across all files.
Areas reviewed: Go (auth libraries, plugins, listeners, main binaries), Tests (unit + E2E), Shell, YAML, Docs, Demo
Assisted-By: Claude Code
| delete(s.items, key) | ||
| } | ||
| s.mu.Unlock() | ||
| return nil, false |
There was a problem hiding this comment.
nit: Consider accepting a context.Context in New() so the janitor goroutine respects context-based cancellation (idiomatic for Go services with graceful shutdown). The current channel-based Close() works correctly but diverges from the context pattern used elsewhere in the pipeline.
| // TestStore_CloseIsIdempotent verifies Close can be called multiple times | ||
| // without panicking. | ||
| func TestStore_CloseIsIdempotent(t *testing.T) { | ||
| s := New() |
There was a problem hiding this comment.
suggestion: The concurrency test uses 50 goroutines writing different keys (mod 26). Adding a focused test that hammers Put+Get on the same key concurrently would further strengthen the TOCTOU protection assertion in Get()'s double-check eviction logic.
Verified the placeholder swap end-to-end under envoy-sidecar mode (Envoy + ext_proc). Fixes found while testing: move echo-upstream off :8080 (proxy-init excludes the agent's own Service port from egress iptables interception, so an upstream on 8080 bypasses ext_proc) to :8888; make patch-echo-config.sh mode-agnostic (detect authbridge-proxy vs envoy-proxy container; fall back to the reloader log line since the envoy image has no wget); de-confuse the agent's HTTP_PROXY-unset log; document envoy-mode testing in the README. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
fix(authbridge): post-#464 follow-ups — abctl pipeline panic + echo demo preflight
Summary
Adds an opt-in credential placeholder swap mode that keeps the real user token out of the agent. On inbound,
jwt-validationvalidates the token, mints an opaqueabph_handle, stores the real token in a process-scoped shared store, and forwards only the handle to the agent. On egress,token-exchangeresolves the handle back to the real token before its normal RFC 8693 exchange. A compromised or prompt-injected agent therefore holds only an opaque, audience-scoped handle that is worthless outside the proxy.This escapes the prior either/or: it preserves user-delegated identity (unlike client-credentials injection) and keeps the secret away from the agent (unlike forwarding the real token).
Design spec:
authbridge/docs/superpowers/specs/2026-06-02-credential-placeholder-swap-design.mdImplementation plan:
authbridge/docs/superpowers/plans/2026-06-02-credential-placeholder-swap.mdWhat changed
authlib/shared— generic, semantics-free, process-scoped TTL key→value store.authlib/placeholder— handle convention:abph_prefix, CSPRNG ≥256-bitNew(), namespacedKey(),IsPlaceholder().pipeline.SharedStoreinterface +Context.Shared— the seam listeners inject; no global state.jwt-validation— newplaceholder_mode/placeholder_ttlconfig (off by default). Mints the handle on the validated allow path; fails closed if no store is wired. Records only a short handle prefix, never the token.token-exchange— newresolve_placeholdersconfig (off by default). Resolves the handle to the real token beforeHandleOutbound; unresolvable handles are denied (fail closed). Non-placeholder bearers are unaffected.reverseproxyandextproc(header + body paths) now propagate an inboundAuthorizationmutation to the agent (previously dropped).forwardproxyexposes the store on the outbound context.authbridge-proxy,authbridge-lite, andauthbridge-envoy(mirrors the existingsessionsinjection).authbridge/docs/plugin-reference.md.Security properties
abph_token, meaningless outside the minting process.token-exchangeonly writes theAuthorizationheader on a matched exchange route (ActionReplaceToken), so resolving before routing cannot send the real token to a passthrough/unmatched host (regression-tested).Behavior notes
authbridge-envoy(extproc). Multi-replica / shared-waypoint deployments need an external store — a documented future enhancement.Scope / deferred
placeholder_ttlis a configured duration (default1h); binding it to the token'sexpis a future enhancement.extauthz/waypoint and the external store are out of scope (not compiled into any shipped binary).Testing
-race), placeholder helpers, mint mode (incl. nil-store fail-closed), resolve (matched-route exchange, miss-denies, non-placeholder passthrough, off-route no-leak), and reverseproxy/extproc inbound propagation (header + body).-race; all five modules build clean.Attribution
Assisted-By: Claude Code
Summary by CodeRabbit
New Features
Tests
Documentation