feat(socrate): opt-in client attribution for OAuth calls made on a user's behalf - #58
Merged
Merged
Conversation
…er's behalf A BFF calls /oauth/token and /oauth/revoke over loopback, so Socrate (v1.5.0+) audits those events as 127.0.0.1 / Go-http-client/1.1. Add socrate.ClientAttribution, WithClientAttribution, ClientAttributionFrom and ApplyClientAttribution. ExchangeCode, RefreshToken, RevokeToken, VerifyMagicLink, AdminLogin and Logout apply the attribution carried by their context: X-Forwarded-For is replaced with exactly the resolved address (Socrate reads the leftmost entry from a trusted proxy, so appending would let the browser pick its address), X-Real-IP is removed, and the User-Agent is sanitised and capped at 512 bytes. The client_credentials grant, introspection and userinfo never carry it. Without attribution in the context, requests are unchanged. Add bff.WithClientAttribution(r, clientIP) to set it from an incoming request; the gateway's refresh already keeps the request context's values, now covered by a test. IP resolution stays with the caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKRxaeYxyDhmt42cehLsGA
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Since Socrate v1.5.0, audit rows record the client IP and User-Agent that Socrate resolves. A Backend-for-Frontend calls
/oauth/token(code exchange, refresh) and/oauth/revokeserver-to-server over loopback. Those events are therefore logged as127.0.0.1/Go-http-client/1.1: the monitoring console's Sessions view shows loopback "sessions", and per-IP analysis of token events is blind. This lets a BFF tell Socrate which browser a call is made for.New API (all additive; no changed signature, no changed default):
type socrate.ClientAttribution struct{ IP, UserAgent string }: the browser a call is made for.socrate.WithClientAttribution(ctx, a) context.Contextandsocrate.ClientAttributionFrom(ctx) (ClientAttribution, bool), using an unexported context key. The doc requires the IP to come from the caller's own trusted-proxy resolution, never from a raw request header.socrate.ApplyClientAttribution(req *http.Request). With attribution on the context:X-Forwarded-Forwith exactly that one address (canonical form) and removesX-Real-IP. An invalid IP sets nothing.User-Agentis set to the browser's value, stripped of control characters and invalid UTF-8, and capped at 512 bytes without splitting a character.bff.WithClientAttribution(r, clientIP) *http.Request: a middleware helper that fillsUserAgentfromr.UserAgent().Why replace, never append: from a trusted proxy (loopback), Socrate takes the leftmost
X-Forwarded-Forentry (go-oauth2internal/middleware/ratelimit.go,GetClientIPSafe). Appending to a browser-supplied value would let the browser choose the address Socrate logs.Applied automatically in the
socrate.Clientcalls made for a browser:ExchangeCode,RefreshToken,RevokeToken,VerifyMagicLink,AdminLoginandLogout. Not applied to the client-credentials grant,IntrospectToken,GetCurrentUserProfile,Decideor the admin API: there's no browser behind those, and the service token is cached and shared. With no attribution on the context, requests are byte-for-byte as before.bff: the proxy's refresh already runs undercontext.WithoutCancel(ctx), which keeps the request's values, so attribution reaches it. A new test covers this.Side effect, intended: Socrate's per-IP rate limit and IP blocking on
/oauth/tokennow apply per browser, as they do for direct sign-ins, instead of to the whole BFF.Consumers: the admin and monitoring consoles build their own token and revoke requests. Their follow-up PRs call
ApplyClientAttributionthere and bump to the release that contains this.How it was tested
Table test of
ApplyClientAttribution:X-Forwarded-For: 6.6.6.6, 1.2.3.4is replaced, andX-Real-IPremoved;Each of the six calls against an
httptestserver, with and without attribution. Without it, there is noX-Forwarded-Forand the UA is Go's default.Service-account token, introspect and userinfo send no
X-Forwarded-For, even with attribution on the context.A
bffproxy-refresh test with a realsocrate.Client: the refresh carries the attribution.These tests fail with
ApplyClientAttributiondisabled, and thebfftest fails with the refresh detached tocontext.Background().go mod tidy && git diff --exit-code go.sumgo build ./...,go vet ./...go test -race -count=1 -timeout=120s ./...golangci-lint run ./...(v2.14.0): 0 issuesgovulncheck ./...: could not run here, because the network policy blocks vuln.go.dev. No dependency changed.A throwaway module using the new API compiled and ran.
README (package reference, "Client attribution"),
docs/CLIENT-INTEGRATION.md(BFF section, the replace-not-append rule) and a CHANGELOG line under Added.Compatibility
Additive only (v1): new exported identifiers; no removed, renamed or changed ones; default behaviour unchanged. After merging, a v1.15.0 release lets the consoles bump.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GKRxaeYxyDhmt42cehLsGA
Generated by Claude Code