Summary
When nanobot run acts as the auth-terminating shim in front of a remote MCP backend (mcpServers.<name>.url), it fully validates the caller (JWKS / trusted-issuer / audiences) but forwards a request to the backend with no representation of the validated caller — the backend sees no Authorization and no subject. This blocks the backend from doing per-caller authorization, audit attribution, or rate limiting.
The interesting part: the forwarding machinery is already present and configured — it's gated off by one condition.
Root cause (v0.0.68 / 0ac2a2d)
- RFC-8693 token exchange is configured for the shim:
pkg/cli/serve.go sets TokenExchangeEndpoint = r.Auth.OAuthTokenURL (from --oauth-token-url / NANOBOT_RUN_OAUTH_TOKEN_URL), threaded into the outbound backend client (pkg/mcp/client.go → HTTPClientOptions).
pkg/mcp/httpclient.go newRequest() performs the exchange and sets Authorization: Bearer <exchanged> on the backend request iff TokenFromContext(ctx) != "".
pkg/auth/auth.go setupContext() (on the request path via auth.Wrap) is where the token would enter the context — but it calls mcp.WithToken(ctx, …) only when info.Props["access_token"] is non-empty:
ctx := types.WithNanobotContext(mcp.WithUser(req.Context(), user), nctx)
if token, _ := info.Props["access_token"].(string); token != "" {
ctx = mcp.WithToken(ctx, token)
}
In our deployment the OAuth validator does not populate Props["access_token"], so WithToken is skipped, TokenFromContext(ctx) is empty, the exchange never fires, and the backend receives no caller identity.
Proposed fix (additive, one hunk)
When Props["access_token"] is empty, fall back to the validated inbound bearer from the Authorization header before WithToken. No behavior change when the prop is present:
token, _ := info.Props["access_token"].(string)
if token == "" {
token = strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ")
}
if token != "" {
ctx = mcp.WithToken(ctx, token)
}
strings is already imported in that file.
Question for maintainers
Is the outbound backend client's request built from the per-request context, or from a context captured when the backend session was first established? If per-session, a per-request WithToken wouldn't apply and the exchange would need to be invoked where the per-request context is live — happy to adjust the fix accordingly.
Offer
I have this patch applied against v0.0.68 locally and am glad to open a PR (with a test) if the direction looks right, or to adopt whatever config-first surface you'd prefer (e.g. a documented --forward-identity switch or claim-templated mcpServers.<name>.headers).
Summary
When
nanobot runacts as the auth-terminating shim in front of a remote MCP backend (mcpServers.<name>.url), it fully validates the caller (JWKS / trusted-issuer / audiences) but forwards a request to the backend with no representation of the validated caller — the backend sees noAuthorizationand no subject. This blocks the backend from doing per-caller authorization, audit attribution, or rate limiting.The interesting part: the forwarding machinery is already present and configured — it's gated off by one condition.
Root cause (v0.0.68 /
0ac2a2d)pkg/cli/serve.gosetsTokenExchangeEndpoint = r.Auth.OAuthTokenURL(from--oauth-token-url/NANOBOT_RUN_OAUTH_TOKEN_URL), threaded into the outbound backend client (pkg/mcp/client.go→HTTPClientOptions).pkg/mcp/httpclient.go newRequest()performs the exchange and setsAuthorization: Bearer <exchanged>on the backend request iffTokenFromContext(ctx) != "".pkg/auth/auth.go setupContext()(on the request path viaauth.Wrap) is where the token would enter the context — but it callsmcp.WithToken(ctx, …)only wheninfo.Props["access_token"]is non-empty:In our deployment the OAuth validator does not populate
Props["access_token"], soWithTokenis skipped,TokenFromContext(ctx)is empty, the exchange never fires, and the backend receives no caller identity.Proposed fix (additive, one hunk)
When
Props["access_token"]is empty, fall back to the validated inbound bearer from theAuthorizationheader beforeWithToken. No behavior change when the prop is present:stringsis already imported in that file.Question for maintainers
Is the outbound backend client's request built from the per-request context, or from a context captured when the backend session was first established? If per-session, a per-request
WithTokenwouldn't apply and the exchange would need to be invoked where the per-request context is live — happy to adjust the fix accordingly.Offer
I have this patch applied against
v0.0.68locally and am glad to open a PR (with a test) if the direction looks right, or to adopt whatever config-first surface you'd prefer (e.g. a documented--forward-identityswitch or claim-templatedmcpServers.<name>.headers).