Skip to content

feat: reject non-proxied requests on module public + platform surfaces#117

Merged
I-am-nothing merged 2 commits into
mainfrom
feat/require-proxy-token
Jun 13, 2026
Merged

feat: reject non-proxied requests on module public + platform surfaces#117
I-am-nothing merged 2 commits into
mainfrom
feat/require-proxy-token

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

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 not X-MS-Platform-Token — the per-session secret the platform injects at the tunnel boundary (the browser never sees it). This adds auth.RequireProxy, validating that token at the SDK trust boundary so every 3rd-party module inherits the protection.

What changed

  • auth.RequireProxy() — new middleware. Mirrors internalAuth's env gating exactly via platformSecretReader:
    • no token configured → inert (pure standalone go test still runs)
    • in Lambda → pass-through (runtime strips X-MS-* and injects identity from the typed payload; the payload is the trust boundary, there's no header to match)
    • token configured on the HTTP dev/tunnel/prod path → enforce: absent/mismatched token → 403 with stable code not_proxied
  • Module.Public now runs the guard (was unauthenticated — the actual gap). Module.Platform runs it in front of PlatformAuth so both surfaces enforce identically and auditably.
  • 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 any existing {"error": ...} response.

Contract for the dispatch track (Fix 1)

  • Header checked: X-MS-Platform-Token (falls back to X-MS-Internal-Secret when only MS_INTERNAL_SECRET is configured — same precedence platformSecretReader already uses).
  • Reject code/status: 403 {"error": ..., "code": "not_proxied"}.
  • Dispatch must inject the matching token (it already does — dev_tunnel.go sets X-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_FILE per module; dispatch injects the matching X-MS-Platform-Token. So oauth-core/oauth-google /public/* flows pass behind dispatch, while a direct curl to the tunnel port (no token) → 403 not_proxied.

Verification

  • go build ./... green
  • go test ./auth/... ./internal/core/... ./internal/httputil/... green
  • New tests: proxied (valid token) passes; direct (no/wrong token) → 403 not_proxied; standalone (no token) → inert; Lambda → pass-through; legacy internal-secret header; token-file rotation; internal surface unaffected.
  • Note: internal/refcache.TestSlowPath_SingleflightCoalesces is a pre-existing concurrency-timing flake (fails identically on clean origin/main); untouched by this PR.

🤖 Generated with Claude Code

I-am-nothing and others added 2 commits June 13, 2026 19:08
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>
@I-am-nothing I-am-nothing merged commit d509bca into main Jun 13, 2026
1 check passed
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