Manual OAuth testing
When an MCP client's OAuth integration goes wrong, exercising the gateway's
endpoints by hand is the fastest way to figure out where. This guide walks every
step of the downstream OAuth flow using curl, openssl, and jq. Each step
shows the request, the shape of the response, and what to look for.
The flow being tested is the standard MCP authorization handshake: discovery → registration → authorize → token → MCP request → refresh. Read the authentication overview for the conceptual model first.
The user-consent step is browser-based — there's no scriptable way to complete it from a terminal. Steps 4 through 6 show the URL to open in a browser and the redirect to inspect; the rest of the flow runs in your terminal.
Prerequisites
curl,jq,openssl, and a Bash-compatible shell.- A deployed MCP Gateway with an
MCP OAuth policy configured (Auth0, Okta,
Entra, Google, or any other supported IdP) and at least one
/mcp/{slug}route. - A browser to complete the user-consent step.
Throughout this guide, replace:
GATEWAYwith your gateway origin (e.g.,https://gateway.example.com).SLUGwith the route slug (e.g.,linear-v1).REDIRECT_URIwith a redirect URL that you can monitor — for testing,http://localhost:8765/callbackworks because the URL only needs to capture thecodequery parameter.
Code
-
Discover the protected resource.
An unauthenticated request to an MCP route should return a
401with aWWW-Authenticateheader that points at the per-route Protected Resource Metadata document.CodeExpected response:
CodeIf you get a 200 instead, the route isn't protected. Check that the MCP OAuth policy is attached to the route in
routes.oas.json.Now fetch the PRM document:
CodeExpected response shape:
CodeThe
authorization_serversarray tells the client where to find the AS metadata. For the gateway, the AS lives under the same origin. -
Discover the authorization server.
Fetch the per-route AS metadata document.
CodeExpected response shape (truncated to the fields you care about):
CodeCapture the URLs you'll need:
CodeIf
code_challenge_methods_supporteddoesn't includeS256, something is wrong with the gateway configuration. The spec requiresS256and the gateway always advertises it. -
Register a client (DCR).
For this test, register a public client with
token_endpoint_auth_method: "none". This is the simplest mode and matches what a CLI client would use.CodeExpected response shape:
CodeThe client ID is opaque. DCR clients expire 90 days after issuance.
-
Build the authorize URL with PKCE.
Generate a PKCE verifier and S256 challenge, plus a state value for CSRF.
CodeBuild the authorize URL. The
resourceparameter is required by the MCP spec on every authorization and token request.CodeOpen the URL in a browser. The flow is:
- The gateway redirects you to your IdP's login page.
- You authenticate at the IdP.
- The IdP redirects back to the gateway's
/oauth/callback. - The gateway renders the consent setup page.
- You click Authorize.
- The gateway redirects to your
redirect_uriwith?code=...&state=....
Capture the
codevalue from the final redirect URL. There's no listener onhttp://localhost:8765, so the browser shows a connection-refused page — that's expected. Copy thecodevalue out of the address bar.The authorization code is single-use and short-lived (typically 30 seconds). Run the next step immediately after copying it.
Code -
Exchange the code for tokens.
POST /oauth/tokenwith the authorization-code grant. Public clients sendclient_idin the form body; confidential clients use HTTP Basic.CodeExpected response shape:
CodeA common failure mode here is
invalid_grantbecause the authorization code expired or was already used. Re-run from step 4.Another common one is
invalid_requestif you forget thecode_verifieror omit theresourceparameter. -
Call the MCP endpoint with the access token.
Now the access token can be presented as a bearer credential on the MCP route.
CodeExpected response is a JSON-RPC result with the upstream's
serverInfoandcapabilities:CodeIf you see a JSON-RPC error with
code: -32042(URLElicitationRequiredError), the upstream MCP server requires OAuth and the user hasn't connected to it yet. Open theauthUrlin the error payload'sdatafield in a browser. See Per-user OAuth to upstream MCP servers for the full flow.If you see a
401, the bearer token is missing, expired, revoked, or bound to a different route — the responseWWW-Authenticateheader includes a reason code viaerror="...".If you see a
403witherror="insufficient_scope", the token has the wrong scope. The gateway only issuesmcp:toolstoday. -
Refresh the access token.
The access token expires in 15 minutes by default. Exchange the refresh token for a new pair using the
refresh_tokengrant.CodeThe refresh token rotates on every use. Presenting the old refresh token again will revoke the entire grant — that's the spec's defense against refresh-token replay. Always use the most recently issued refresh token.
The new access token can be used immediately on subsequent
/mcp/{slug}requests. -
Revoke the tokens (optional cleanup).
When you're done testing, revoke the grant.
CodePer RFC 7009, the gateway responds with
200 OKand an empty body for both successful revocations and unknown tokens. Subsequent MCP requests with the revoked access token return401.
Putting it all together
Here's a single Bash script that runs every step except the browser-based
authorize redirect. Save it as test-oauth.sh and run it after editing the
configuration block at the top.
Code
Make it executable and run it:
Code
Common issues
401on every MCP request after token exchange. Token bound to a different route than the one you're calling. Each token is scoped to one MCP route. Either re-run for the intended route or call the route you authorized for.401witherror="invalid_token"after a token reuse. Refresh tokens rotate on every use — presenting an old one revokes the entire grant. Re-run the full flow.invalid_requestat the token endpoint. Most often a missingresourceparameter or a missingcode_verifier. Both are required.invalid_grantat the token endpoint. The authorization code expired or was already redeemed. Re-run from step 4.invalid_audience. The bearer token is being used at a route whose canonical resource URI doesn't match the token'sresourceclaim. A misconfigured custom domain or proxy can cause this.- The browser shows the gateway's consent page but the Authorize button is disabled. The route has an upstream that hasn't been connected yet. Click the per-upstream Connect button first. See upstream OAuth.
- JSON-RPC error
-32042(URLElicitationRequiredError). The downstream OAuth succeeded but the upstream MCP server requires OAuth and the user hasn't connected. Open theauthUrlin the error payload'sdatafield in a browser.
Related
- Authentication overview — the full identity provider catalog and per-IdP setup links.
- Per-user OAuth to upstream MCP servers
- Test clients — exercise the same OAuth flow through the
MCP Inspector and MCPJam GUIs instead of
curl. - MCP authorization spec, revision 2025-11-25