Authentication is not authorization
When an MCP client authenticates with OAuth2 and receives a valid access token, that only proves the client completed a recognised auth flow. It says nothing about which resources the caller is permitted to touch inside an API.
Without a control layer between the MCP client and the target API, a valid JWT grants the client access to everything the proxy is willing to forward — regardless of which user or service the token represents. That’s the identity gap many teams are struggling with.
RequestRocket sits between the MCP client and your target API and evaluates every inbound request against JWT claims before forwarding anything upstream. The target API never sees a request that the caller wasn’t explicitly authorised to make.
How the connection is monitored
Every request flowing through a RequestRocket proxy carries an observable identity: in the case of a JWT, this is the claim set decoded from the bearer token the MCP client presents. That includes the sub claim (the principal making the request), any scope values, audience, and any custom claims your identity provider adds.
The gateway observes these as part of the request trace. You can see, for any given target API call:
- which
subinitiated the request - which endpoint path was called
- whether the request was allowed, denied, or modified
- the full claim set that was evaluated against your rules
This makes it auditable — you can answer “did any MCP client access /v1/admin/users while holding a token with sub: user-*?” before the answer becomes a post-incident finding.
Step 1: Point the proxy at your target API
POST /clients/{clientId}/targets
{
"targetName": "my-existing-api",
"targetBaseURL": "https://api.myapp.com",
"targetRegion": "us-east-1"
}Step 2: Create a proxy credential that validates inbound JWTs
The jwtVerify credential type tells RequestRocket to validate the bearer token on every inbound request against your identity provider’s JWKS endpoint. Requests with invalid, expired, or absent tokens are rejected with a 401 before the connection to your target API is even attempted.
POST /clients/{clientId}/credentials
{
"credentialType": "proxy",
"credentialAuthType": "jwtVerify",
"credentialName": "mcp-caller-verify",
"credentialRegion": "us-east-1",
"credentialSecret": {
"jwksUri": "https://your-tenant.auth0.com/.well-known/jwks.json",
"audience": "https://mcp.myapp.com",
"issuer": "https://your-tenant.auth0.com/"
}
}Once the token is verified, the decoded claim set is available for rule evaluation on every subsequent request.
Step 3: Create the proxy with deny-by-default
POST /clients/{clientId}/proxies
{
"proxyName": "mcp-myapp-api",
"proxyRegion": "us-east-1",
"proxyProxyCredentialId": "<mcp-caller-verify-id>",
"proxyTargetId": "<my-existing-api-id>",
"proxyTargetCredentialId": "<myapp-api-internal-id>",
"proxyDefaultRuleEffect": "deny",
"proxyAlias": "mcp-myapp"
}proxyDefaultRuleEffect: "deny" means nothing reaches the target API unless a rule explicitly permits it. Any path not covered by a rule is blocked at the gateway — no forwarding, no error response from your API, no side-effects.
Step 4: Require the sub claim to be present
The sub claim identifies the principal making the request. An MCP client acting on behalf of a user should always present a token with a sub. A token without one is either machine-to-machine traffic misconfigured against a user-scoped endpoint, or something more concerning.
Deny it before it reaches the API:
POST /clients/{clientId}/proxies/{proxyId}/rules
{
"effect": "deny",
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE"],
"token": [
{
"claim": { "pattern": "^sub$" },
"presence": "must_absent"
}
],
"priority": 1,
"notes": "Deny any request where the sub claim is missing from the JWT"
}This rule matches any request where the sub claim is absent. A matched deny rule always wins — the request is rejected with a 403 regardless of any allow rules that also match.
Step 5: Restrict which identities can reach sensitive paths
Not every sub should reach every endpoint. Service accounts often have a predictable sub format (e.g. svc-* or a fixed UUID). If your MCP proxy is intended for human-user-delegated access, you can block service account tokens from reaching paths that should only receive user context:
POST /clients/{clientId}/proxies/{proxyId}/rules
{
"effect": "deny",
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE"],
"path": {
"path": { "pattern": "^/v1/users/" },
"presence": "must_exist"
},
"token": [
{
"claim": { "pattern": "^sub$" },
"value": { "pattern": "^svc-" },
"presence": "must_exist"
}
],
"priority": 2,
"notes": "Block service account subs from user-scoped resource paths"
}Conversely, you can allow only tokens whose sub matches a specific tenant or user pattern:
POST /clients/{clientId}/proxies/{proxyId}/rules
{
"effect": "allow",
"methods": ["GET"],
"path": {
"path": { "pattern": "^/v1/(products|orders)/?" },
"presence": "must_exist"
},
"token": [
{
"claim": { "pattern": "^sub$" },
"value": { "pattern": "^user-" },
"presence": "must_exist"
}
],
"priority": 10,
"notes": "Allow read access to products and orders only for user-scoped tokens"
}An MCP caller whose sub doesn’t match ^user- never reaches those paths — not because your API checked, but because the gateway stopped the request first.
Step 6: Filter the response based on the caller’s identity
Rules control which requests reach the target API. Filters control what data comes back from it. A filter inspects the response payload and strips anything the caller shouldn’t receive — using the same verified JWT claims that rules operate on.
Filters bind JWT claims as named variables and interpolate them into matchValue patterns, which are regex-escaped before compilation. Here, a single filter record strips orders from the response that don’t belong to the calling user, and removes any internal fields from those that survive:
POST /clients/{clientId}/proxies/{proxyId}/filters
{
"variables": [
{
"name": "callerSub",
"source": "jwtClaim",
"key": "sub"
}
],
"defaultFilterOperation": "destroy",
"operations": [
{
"effect": "retain",
"jsonPath": { "pattern": "^orders$" },
"matchField": { "pattern": "^userId$" },
"matchValue": { "pattern": "^{{callerSub}}$" },
"priority": 10,
"notes": "Retain only orders where userId matches the caller's JWT sub"
},
{
"effect": "retain",
"jsonPath": { "pattern": "^orders\\.(orderId|userId|amount|status)$" },
"priority": 5,
"notes": "Retain only these fields on each surviving order"
}
],
"notes": "Strip orders not belonging to the calling user; remove internal fields"
}defaultFilterOperation: "destroy" is allowlist mode — everything is removed unless an operation explicitly retains it. With this mode the engine always runs field operations before row operations, so the two operations execute as follows:
- The field operation (priority 5) runs first across every element in the response, stripping any fields not in the retain list. Everything beyond
orderId,userId,amount, andstatus— internal references, audit timestamps, anything else — is removed from every order object before row filtering begins. - The row operation (priority 10) then prunes the array itself, removing any element where
userIddoes not match the caller’ssub. Orders belonging to other users are discarded entirely.
The {{callerSub}} placeholder is replaced with the verified sub value at evaluation time and is regex-escaped before compilation — a sub value of user.abc+1 becomes user\.abc\+1 in the pattern, so it matches exactly.
What the target API sends and what the MCP client receives
From the target API’s perspective, every request arrives as an ordinary authenticated HTTP request — the internal service credential in the Authorization header, no MCP-specific headers, no trace of the caller’s OAuth2 token. The target API has no knowledge of the filter layer.
From the MCP client’s perspective, the response has passed through two independent enforcement layers:
- Rules determined which requests were allowed to reach the API at all, evaluated against JWT claims before any upstream connection.
- Filters determined what data from the API’s response reached the MCP client, evaluated against JWT claims on the way back.
A caller with sub: user-123 asking for /v1/orders receives only the orders where userId is user-123, with only the fields the filter retained. They cannot observe that other orders exist — the response is shaped as if the API only returned their data.
Enforcement summary
Two layers operate independently — rules on the inbound request, filters on the outbound response.
Stopped before the target API is contacted:
- Requests with no bearer token →
401 - Requests with an invalid or expired token →
401 - Requests with a valid token but missing
sub→403 - Requests where
submatches a blocked pattern for the requested path →403 - Requests to any path not covered by an allow rule → default
deny
None of these generate load or log lines on your API. The connection is never made.
Stripped from the response before the MCP client sees it:
- Array elements where the ownership field doesn’t match the caller’s
sub - Fields not explicitly retained by the filter’s allowlist operations
The caller receives a shaped response. They cannot infer what was removed.
Next steps
The RequestRocket proxy and credential documentation covers the full rule and filter configuration reference, including additional claim operators and filter types. If you’re already using an identity provider that issues JWTs with a sub, you can have this running against an existing API in an afternoon — start for free.