feat: reject non-proxied requests on module public + platform surfaces#117
Merged
Conversation
Add an SDK-level proxy guard (auth.RequireProxy) that rejects any request
reaching a module's PUBLIC or PLATFORM surface without the server-injected
X-MS-Platform-Token. A direct caller can spoof X-MS-App-ID / X-MS-User-ID /
X-MS-App-Role, but never the per-session platform token dispatch injects at
the tunnel boundary — enforcing it at the SDK trust boundary makes every
3rd-party module inherit the protection (doc 09, Fix 2).
- auth.RequireProxy: mirrors internalAuth's env gating via platformSecretReader.
inert when no token configured (standalone go test); pass-through in Lambda
(headers stripped + identity injected from the typed payload — the payload is
the trust boundary); enforce on the HTTP dev/tunnel/prod path. Absent/mismatch
-> 403 with stable code "not_proxied".
- Module.Public now runs the guard (was unauthenticated). Module.Platform runs
it in front of PlatformAuth so both surfaces enforce identically.
- Internal surface keeps its existing X-MS-Internal-Secret guard, unchanged.
- httputil.ErrorResponse gains an optional, omitempty Code field so the 403
carries the machine-matchable not_proxied code without changing existing
{"error": ...} responses.
Dev path verified: the CLI writes the platform token to MS_PLATFORM_TOKEN_FILE
per module and dispatch injects the matching X-MS-Platform-Token, so
oauth-core/oauth-google /public/* flows pass behind dispatch; a direct curl
to the tunnel port (no token) -> 403 not_proxied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gate dev CORS on full secret chain Review fixes for the proxy-guard PR: - requireProxy/internalAuth/platformAuth now distinguish 'no secret source configured' from 'configured but unreadable'. A token-file read error (MS_PLATFORM_TOKEN_FILE deleted mid-rotation, bad perms, tmpfs full) previously collapsed to the local-dev bypass and made the guard inert. secretReader now returns a 'configured' bool so guards fail CLOSED (403/503) on read errors instead of passing through. - Cache PlatformAuth() on the Module at New() (m.platformAuth) so every Platform() registration reuses one closure, matching the captured-once contract already honored by internalAuth and proxyGuard. - Gate localDevCORS and requireInternalSecret on the full secret chain (new auth.SecretConfigured) instead of MS_INTERNAL_SECRET alone, so a tunnel module carrying only MS_PLATFORM_TOKEN no longer attaches permissive credentialed CORS or fails Start() spuriously. - Tests: token-file read-error fails closed; SecretConfigured stays true when the file is set-but-unreadable; clarify the auto-mount-prefix test comment that the guard is inert there (enforcement covered elsewhere). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Implements Fix 2 of doc 09 (
docs-temp/next-wave/09-app-scoped-public-proxy.md): the SDK guard that rejects non-proxied requests on a module's public + platform surface.A direct caller can spoof
X-MS-App-ID/X-MS-User-ID/X-MS-App-Role, but notX-MS-Platform-Token— the per-session secret the platform injects at the tunnel boundary (the browser never sees it). This addsauth.RequireProxy, validating that token at the SDK trust boundary so every 3rd-party module inherits the protection.What changed
auth.RequireProxy()— new middleware. MirrorsinternalAuth's env gating exactly viaplatformSecretReader:go teststill runs)X-MS-*and injects identity from the typed payload; the payload is the trust boundary, there's no header to match)403with stable codenot_proxiedModule.Publicnow runs the guard (was unauthenticated — the actual gap).Module.Platformruns it in front ofPlatformAuthso both surfaces enforce identically and auditably.X-MS-Internal-Secretguard, unchanged.httputil.ErrorResponsegains an optionalomitemptyCodefield so the 403 carries the machine-matchablenot_proxiedcode without changing any existing{"error": ...}response.Contract for the dispatch track (Fix 1)
X-MS-Platform-Token(falls back toX-MS-Internal-Secretwhen onlyMS_INTERNAL_SECRETis configured — same precedenceplatformSecretReaderalready uses).403{"error": ..., "code": "not_proxied"}.dev_tunnel.gosetsX-MS-Platform-Token: sess.ServiceToken). The new/v1/apps/{appRef}/{moduleRef}/*proxy route must reuse the same injection tail.Dev-stack impact (traced)
The CLI writes the platform token to
MS_PLATFORM_TOKEN_FILEper module; dispatch injects the matchingX-MS-Platform-Token. So oauth-core/oauth-google/public/*flows pass behind dispatch, while a directcurlto the tunnel port (no token) →403 not_proxied.Verification
go build ./...greengo test ./auth/... ./internal/core/... ./internal/httputil/...greennot_proxied; standalone (no token) → inert; Lambda → pass-through; legacy internal-secret header; token-file rotation; internal surface unaffected.internal/refcache.TestSlowPath_SingleflightCoalescesis a pre-existing concurrency-timing flake (fails identically on cleanorigin/main); untouched by this PR.🤖 Generated with Claude Code