Skip to content

feat(auth): forwardedClientId strategy and opt-in apikey/path secret locations - #3

Draft
snowkide wants to merge 3 commits into
mainfrom
feat/forwarded-client-id-auth
Draft

snowkide wants to merge 3 commits into
mainfrom
feat/forwarded-client-id-auth

Conversation

@snowkide

Copy link
Copy Markdown

Two opt-in auth additions for deployments where eRPC sits behind an API gateway or is used by clients that only take a single RPC URL, plus a tracing fix found along the way. Nothing changes for existing configurations.

Commits (reviewable separately):

  1. feat(auth): forwardedClientId strategy. When a gateway or reverse proxy already authenticates callers and forwards the client id in a header, eRPC trusts that header as the user id, so per-client rate-limit budgets, method scoping and the user metric label work without duplicating the gateway's key store. Only active when configured.
    • header (default X-Client-Id)
    • allowedProxies: only trust the header from these direct TCP peers (socket address, not X-Forwarded-For)
    • allowedIds: optional allowlist, also bounds user label cardinality
    • rateLimitBudget
    • Multiple header values are rejected, as are empty ids, ids over 256 bytes, or ids with whitespace, commas or non-ASCII.
  2. fix(tracing): redact ?secret=, ?token= and ?jwt= values from the http.url span attribute. Today they are exported verbatim to the tracing backend.
  3. feat(auth): opt-in secret locations for the secret and database strategies, via additionalLocations (empty by default). Consulted only when none of the existing locations supplied a secret:
    • apikeyQuery: ?apikey=
    • apikeyHeader: X-Api-Key / apikey header
    • path: the URL path as a single segment (https://host/<secret>), only when a domain alias preselects project, architecture and chain; /admin, /healthcheck and /metrics are never treated as secrets.
    • A secret found in a location a strategy does not list is ignored by it. These locations never change the credential type, so requests without one of the existing credentials still reach a network strategy as before. Unknown location names fail validation.
    • Path secrets are redacted to /REDACTED in tracing and in the CORS metrics path label, and ?apikey= like ?secret=.
auth:
  strategies:
    - type: forwardedClientId
      forwardedClientId:
        header: X-Client-Id
        allowedProxies: ["10.0.0.0/8"]
        rateLimitBudget: per-client
    - type: secret
      secret:
        id: wallet-app
        value: ${WALLET_APP_SECRET}
        additionalLocations: [apikeyHeader]

Security note: forwardedClientId trusts a header. Enable it only behind a proxy that strips or overwrites that header from clients, and restrict allowedProxies to that proxy.

Verification

Based on erpc/erpc main @ 094b912.

  • go build ./..., go vet ./... and gofmt -l on touched files are clean.
  • make test-fast (what CI runs): passes, exit 0, no failing test.
  • Tests for the opt-in: TestAuthRegistry_SecondarySecretLocations (each location accepted only when listed; standard locations unaffected), TestSecretStrategyConfig_ValidateAdditionalLocations, TestPathSecretAuth_FullyAliasedDomain; plus the forwardedClientId tests (TestAuthRegistry_ForwardedClientId*, TestAuthStrategyConfig_Validate_ForwardedClientIdRequiresBlock).
  • TypeScript config types regenerated with tygo generate.

Notes for review:

  • forwardedClientId is inert unless configured, and is only offered requests that carry its header. It authenticates (budgets, method scoping and allowClientDirectives apply), unlike trustUserIdHeader, which only labels.
  • Not addressed here (pre-existing): the database strategy logs apiKey at debug/error level, and the secret strategy's comparison is not constant-time.
  • Could be split into two PRs (forwardedClientId; secret locations with the tracing redaction fix) if reviewers prefer.

Docs: config/auth, operation/tracing.

AI assistance

  • Model: anthropic/claude-opus-5.5
  • Harness / skills: Claude Code
  • Human-reviewed: no, pending

🤖 Generated with Claude Code

snowkide and others added 3 commits October 15, 2026 09:44
…callers

When eRPC runs behind an API gateway or reverse proxy that already
authenticates callers, the gateway typically forwards the authenticated
client's id in a request header. The new forwardedClientId strategy
trusts that header as the user id, so per-client rate-limit budgets,
method scoping and the user metric label work without duplicating the
gateway's key store in eRPC.

The strategy is only active when configured and is offered every HTTP or
WebSocket request that carries its header, in declaration order:

- header: header name, default X-Client-Id
- allowedProxies: only trust the header from these direct TCP peers
  (the socket address, not the X-Forwarded-For client IP)
- allowedIds: optional allowlist, also bounds user label cardinality
- rateLimitBudget: budget attached to the authenticated user

A header with several values (a proxy that appends instead of
overwriting) is rejected, as are ids that are empty, longer than 256
bytes, or contain whitespace, commas or non-ASCII characters.

AuthPayload now carries the request headers and remote address so that
strategies can read their own transport inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Http.ReceivedRequest span recorded r.URL.String() verbatim, so a
secret, token or JWT passed as ?secret=, ?token= or ?jwt= was exported
to the tracing backend. Replace those values with REDACTED; other query
parameters and the path are kept as is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Many clients and gateways use the common "API key" conventions, and some
wallets and SDKs only take a single RPC URL. Let the secret and database
strategies also read the secret from three additional locations, consulted
only when none of the existing ones (?token=, ?secret=,
X-ERPC-Secret-Token, Authorization: Basic) supplied one:

- apikeyQuery: the ?apikey= query parameter
- apikeyHeader: the X-Api-Key or apikey header
- path: the URL path as a single segment, https://host/<secret>, but only
  when a domain alias preselects project, architecture and chain so the
  path is not used for routing; /admin, /healthcheck and /metrics are
  never treated as secrets

Each strategy opts in with additionalLocations (default empty, so existing
setups accept exactly what they did before). A secret found in a location
the strategy does not list is ignored by it. These locations never change
the payload type, so requests that carry none of the existing credentials
still reach a network strategy exactly as before.

NewPayloadFromHttp takes the credential path ("" when the path is
routed). The HTTP handler, WebSocket upgrade check and per-message auth,
and the healthcheck pass it through. A path that carries a secret is
replaced with /REDACTED in the http.url span attribute and in the CORS
metrics' path label, and ?apikey= is redacted like ?secret=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant