zuplo 6.70.53 → 6.70.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/mcp-gateway/auth/configuring-auth0.mdx +216 -0
- package/docs/mcp-gateway/auth/configuring-clerk.mdx +153 -0
- package/docs/mcp-gateway/auth/configuring-cognito.mdx +128 -0
- package/docs/mcp-gateway/auth/configuring-entra.mdx +134 -0
- package/docs/mcp-gateway/auth/configuring-generic-oidc.mdx +242 -0
- package/docs/mcp-gateway/auth/configuring-google.mdx +117 -0
- package/docs/mcp-gateway/auth/configuring-keycloak.mdx +125 -0
- package/docs/mcp-gateway/auth/configuring-logto.mdx +116 -0
- package/docs/mcp-gateway/auth/configuring-okta.mdx +199 -0
- package/docs/mcp-gateway/auth/configuring-onelogin.mdx +122 -0
- package/docs/mcp-gateway/auth/configuring-ping.mdx +157 -0
- package/docs/mcp-gateway/auth/configuring-workos.mdx +117 -0
- package/docs/mcp-gateway/auth/manual-oauth-testing.mdx +528 -0
- package/docs/mcp-gateway/auth/overview.mdx +314 -0
- package/docs/mcp-gateway/auth/upstream-oauth.mdx +221 -0
- package/docs/mcp-gateway/capability-filtering.mdx +162 -0
- package/docs/mcp-gateway/code-config/compatibility-dates.mdx +33 -0
- package/docs/mcp-gateway/code-config/local-development.mdx +198 -0
- package/docs/mcp-gateway/code-config/mcp-proxy-handler.mdx +186 -0
- package/docs/mcp-gateway/code-config/multi-upstream.mdx +293 -0
- package/docs/mcp-gateway/code-config/overview.mdx +210 -0
- package/docs/mcp-gateway/connect-clients/chatgpt.mdx +127 -0
- package/docs/mcp-gateway/connect-clients/claude-code.mdx +184 -0
- package/docs/mcp-gateway/connect-clients/claude-desktop.mdx +160 -0
- package/docs/mcp-gateway/connect-clients/cursor.mdx +100 -0
- package/docs/mcp-gateway/connect-clients/other-clients.mdx +207 -0
- package/docs/mcp-gateway/connect-clients/overview.mdx +137 -0
- package/docs/mcp-gateway/connect-clients/vs-code.mdx +128 -0
- package/docs/mcp-gateway/how-it-works.mdx +266 -0
- package/docs/mcp-gateway/how-to/connect-upstream-oauth.mdx +268 -0
- package/docs/mcp-gateway/how-to/curate-tools.mdx +278 -0
- package/docs/mcp-gateway/introduction.mdx +151 -0
- package/docs/mcp-gateway/observability/analytics.mdx +191 -0
- package/docs/mcp-gateway/observability/logging.mdx +191 -0
- package/docs/mcp-gateway/quickstart.mdx +266 -0
- package/docs/mcp-gateway/reference.mdx +148 -0
- package/docs/mcp-gateway/test-clients.mdx +130 -0
- package/docs/mcp-gateway/troubleshooting.mdx +228 -0
- package/docs/mcp-server/introduction.mdx +10 -0
- package/docs/mcp-server/openai-apps-sdk.mdx +12 -0
- package/docs/policies/_index.md +14 -0
- package/docs/policies/akamai-ai-firewall/schema.json +1 -0
- package/docs/policies/akamai-firewall-for-ai-inbound/schema.json +1 -0
- package/docs/policies/akamai-firewall-for-ai-outbound/schema.json +1 -0
- package/docs/policies/amberflo-metering-inbound/schema.json +1 -0
- package/docs/policies/api-key-inbound/schema.json +1 -0
- package/docs/policies/audit-log-inbound/schema.json +1 -0
- package/docs/policies/auth0-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/authzen-inbound/schema.json +1 -0
- package/docs/policies/axiomatics-authz-inbound/schema.json +1 -0
- package/docs/policies/basic-auth-inbound/schema.json +1 -0
- package/docs/policies/bot-detection-inbound/schema.json +1 -0
- package/docs/policies/brownout-inbound/schema.json +1 -0
- package/docs/policies/caching-inbound/schema.json +1 -0
- package/docs/policies/change-method-inbound/schema.json +1 -0
- package/docs/policies/clear-headers-inbound/schema.json +1 -0
- package/docs/policies/clear-headers-outbound/schema.json +1 -0
- package/docs/policies/clerk-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/cognito-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/comet-opik-tracing-inbound/schema.json +1 -0
- package/docs/policies/complex-rate-limit-inbound/schema.json +1 -0
- package/docs/policies/composite-inbound/schema.json +1 -0
- package/docs/policies/composite-outbound/schema.json +1 -0
- package/docs/policies/curity-phantom-token-inbound/schema.json +1 -0
- package/docs/policies/firebase-jwt-inbound/schema.json +1 -0
- package/docs/policies/formdata-to-json-inbound/schema.json +1 -0
- package/docs/policies/galileo-tracing-inbound/schema.json +1 -0
- package/docs/policies/geo-filter-inbound/schema.json +1 -0
- package/docs/policies/graphql-complexity-limit-inbound/schema.json +1 -0
- package/docs/policies/graphql-disable-introspection-inbound/schema.json +1 -0
- package/docs/policies/graphql-introspection-filter-outbound/schema.json +1 -0
- package/docs/policies/http-deprecation-outbound/schema.json +1 -0
- package/docs/policies/jwt-scopes-inbound/schema.json +1 -0
- package/docs/policies/ldap-auth-inbound/schema.json +1 -0
- package/docs/policies/mcp-auth0-oauth-inbound/doc.md +54 -0
- package/docs/policies/mcp-auth0-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-auth0-oauth-inbound/schema.json +135 -0
- package/docs/policies/mcp-capability-filter-inbound/doc.md +58 -0
- package/docs/policies/mcp-capability-filter-inbound/intro.md +9 -0
- package/docs/policies/mcp-capability-filter-inbound/schema.json +212 -0
- package/docs/policies/mcp-clerk-oauth-inbound/doc.md +34 -0
- package/docs/policies/mcp-clerk-oauth-inbound/intro.md +1 -0
- package/docs/policies/mcp-clerk-oauth-inbound/schema.json +134 -0
- package/docs/policies/mcp-cognito-oauth-inbound/doc.md +52 -0
- package/docs/policies/mcp-cognito-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-cognito-oauth-inbound/schema.json +152 -0
- package/docs/policies/mcp-entra-oauth-inbound/doc.md +51 -0
- package/docs/policies/mcp-entra-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-entra-oauth-inbound/schema.json +131 -0
- package/docs/policies/mcp-google-oauth-inbound/doc.md +52 -0
- package/docs/policies/mcp-google-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-google-oauth-inbound/schema.json +125 -0
- package/docs/policies/mcp-keycloak-oauth-inbound/doc.md +43 -0
- package/docs/policies/mcp-keycloak-oauth-inbound/intro.md +2 -0
- package/docs/policies/mcp-keycloak-oauth-inbound/schema.json +140 -0
- package/docs/policies/mcp-logto-oauth-inbound/doc.md +52 -0
- package/docs/policies/mcp-logto-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-logto-oauth-inbound/schema.json +131 -0
- package/docs/policies/mcp-oauth-inbound/doc.md +70 -0
- package/docs/policies/mcp-oauth-inbound/intro.md +11 -0
- package/docs/policies/mcp-oauth-inbound/schema.json +177 -0
- package/docs/policies/mcp-okta-oauth-inbound/doc.md +61 -0
- package/docs/policies/mcp-okta-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-okta-oauth-inbound/schema.json +137 -0
- package/docs/policies/mcp-onelogin-oauth-inbound/doc.md +50 -0
- package/docs/policies/mcp-onelogin-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-onelogin-oauth-inbound/schema.json +131 -0
- package/docs/policies/mcp-ping-oauth-inbound/doc.md +80 -0
- package/docs/policies/mcp-ping-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-ping-oauth-inbound/schema.json +151 -0
- package/docs/policies/mcp-token-exchange-inbound/doc.md +135 -0
- package/docs/policies/mcp-token-exchange-inbound/intro.md +6 -0
- package/docs/policies/mcp-token-exchange-inbound/schema.json +134 -0
- package/docs/policies/mcp-workos-oauth-inbound/doc.md +50 -0
- package/docs/policies/mcp-workos-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-workos-oauth-inbound/schema.json +125 -0
- package/docs/policies/mock-api-inbound/schema.json +1 -0
- package/docs/policies/moesif-inbound/schema.json +1 -0
- package/docs/policies/monetization-inbound/schema.json +1 -0
- package/docs/policies/mtls-auth-inbound/schema.json +1 -0
- package/docs/policies/okta-fga-authz-inbound/schema.json +1 -0
- package/docs/policies/okta-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/open-id-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/openfga-authz-inbound/schema.json +1 -0
- package/docs/policies/openmeter-inbound/schema.json +1 -0
- package/docs/policies/prompt-injection-outbound/schema.json +1 -0
- package/docs/policies/propel-auth-jwt-inbound/schema.json +1 -0
- package/docs/policies/query-param-to-header-inbound/schema.json +1 -0
- package/docs/policies/quota-inbound/schema.json +1 -0
- package/docs/policies/rate-limit-inbound/schema.json +1 -0
- package/docs/policies/readme-metrics-inbound/schema.json +1 -0
- package/docs/policies/remove-headers-inbound/schema.json +1 -0
- package/docs/policies/remove-headers-outbound/schema.json +1 -0
- package/docs/policies/remove-query-params-inbound/schema.json +1 -0
- package/docs/policies/replace-string-outbound/schema.json +1 -0
- package/docs/policies/request-size-limit-inbound/schema.json +1 -0
- package/docs/policies/request-validation-inbound/schema.json +1 -0
- package/docs/policies/require-origin-inbound/schema.json +1 -0
- package/docs/policies/secret-masking-outbound/schema.json +1 -0
- package/docs/policies/semantic-cache-inbound/schema.json +1 -0
- package/docs/policies/set-body-inbound/schema.json +1 -0
- package/docs/policies/set-headers-inbound/schema.json +1 -0
- package/docs/policies/set-headers-outbound/schema.json +1 -0
- package/docs/policies/set-query-params-inbound/schema.json +1 -0
- package/docs/policies/set-status-outbound/schema.json +1 -0
- package/docs/policies/set-upstream-api-key-inbound/schema.json +1 -0
- package/docs/policies/sleep-inbound/schema.json +1 -0
- package/docs/policies/stripe-webhook-verification-inbound/schema.json +1 -0
- package/docs/policies/supabase-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-azure-ad-service-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-firebase-admin-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-firebase-user-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-gcp-federated-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-gcp-jwt-inbound/schema.json +1 -0
- package/docs/policies/upstream-gcp-service-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-zuplo-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/validate-json-schema-inbound/schema.json +1 -0
- package/docs/policies/web-bot-auth-inbound/schema.json +1 -0
- package/docs/policies/xml-to-json-outbound/schema.json +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Manual OAuth testing"
|
|
3
|
+
sidebar_label: "Manual OAuth testing"
|
|
4
|
+
description:
|
|
5
|
+
Walk through every step of the MCP Gateway's downstream OAuth flow with curl,
|
|
6
|
+
openssl, and jq. Useful for debugging discovery, registration, authorize,
|
|
7
|
+
token exchange, refresh, and the first authenticated MCP request.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
When an MCP client's OAuth integration goes wrong, exercising the gateway's
|
|
11
|
+
endpoints by hand is the fastest way to figure out where. This guide walks every
|
|
12
|
+
step of the downstream OAuth flow using `curl`, `openssl`, and `jq`. Each step
|
|
13
|
+
shows the request, the shape of the response, and what to look for.
|
|
14
|
+
|
|
15
|
+
The flow being tested is the standard MCP authorization handshake: discovery →
|
|
16
|
+
registration → authorize → token → MCP request → refresh. Read the
|
|
17
|
+
[authentication overview](./overview.mdx) for the conceptual model first.
|
|
18
|
+
|
|
19
|
+
:::note
|
|
20
|
+
|
|
21
|
+
The user-consent step is browser-based — there's no scriptable way to complete
|
|
22
|
+
it from a terminal. Steps 4 through 6 show the URL to open in a browser and the
|
|
23
|
+
redirect to inspect; the rest of the flow runs in your terminal.
|
|
24
|
+
|
|
25
|
+
:::
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
- `curl`, `jq`, `openssl`, and a Bash-compatible shell.
|
|
30
|
+
- A deployed MCP Gateway with an
|
|
31
|
+
[MCP OAuth policy](./overview.mdx#identity-providers) configured (Auth0, Okta,
|
|
32
|
+
Entra, Google, or any other supported IdP) and at least one `/mcp/{slug}`
|
|
33
|
+
route.
|
|
34
|
+
- A browser to complete the user-consent step.
|
|
35
|
+
|
|
36
|
+
Throughout this guide, replace:
|
|
37
|
+
|
|
38
|
+
- `GATEWAY` with your gateway origin (e.g., `https://gateway.example.com`).
|
|
39
|
+
- `SLUG` with the route slug (e.g., `linear-v1`).
|
|
40
|
+
- `REDIRECT_URI` with a redirect URL that you can monitor — for testing,
|
|
41
|
+
`http://localhost:8765/callback` works because the URL only needs to capture
|
|
42
|
+
the `code` query parameter.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
GATEWAY="https://gateway.example.com"
|
|
46
|
+
SLUG="linear-v1"
|
|
47
|
+
REDIRECT_URI="http://localhost:8765/callback"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
<Stepper>
|
|
51
|
+
|
|
52
|
+
1. **Discover the protected resource.**
|
|
53
|
+
|
|
54
|
+
An unauthenticated request to an MCP route should return a `401` with a
|
|
55
|
+
`WWW-Authenticate` header that points at the per-route Protected Resource
|
|
56
|
+
Metadata document.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
curl -i -X POST "${GATEWAY}/mcp/${SLUG}" \
|
|
60
|
+
-H "content-type: application/json" \
|
|
61
|
+
-H "accept: application/json, text/event-stream" \
|
|
62
|
+
-d '{"jsonrpc":"2.0","id":"1","method":"ping"}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Expected response:
|
|
66
|
+
|
|
67
|
+
```http
|
|
68
|
+
HTTP/1.1 401 Unauthorized
|
|
69
|
+
WWW-Authenticate: Bearer realm="OAuth",
|
|
70
|
+
resource_metadata="https://gateway.example.com/.well-known/oauth-protected-resource/mcp/linear-v1"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If you get a 200 instead, the route isn't protected. Check that the MCP OAuth
|
|
74
|
+
policy is attached to the route in `routes.oas.json`.
|
|
75
|
+
|
|
76
|
+
Now fetch the PRM document:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl -s "${GATEWAY}/.well-known/oauth-protected-resource/mcp/${SLUG}" | jq
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Expected response shape:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"resource": "https://gateway.example.com/mcp/linear-v1",
|
|
87
|
+
"resource_name": "Linear MCP Proxy",
|
|
88
|
+
"authorization_servers": ["https://gateway.example.com/mcp/linear-v1"],
|
|
89
|
+
"bearer_methods_supported": ["header"],
|
|
90
|
+
"scopes_supported": ["mcp:tools"],
|
|
91
|
+
"mcp_protocol_version": "2025-11-25"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `authorization_servers` array tells the client where to find the AS
|
|
96
|
+
metadata. For the gateway, the AS lives under the same origin.
|
|
97
|
+
|
|
98
|
+
1. **Discover the authorization server.**
|
|
99
|
+
|
|
100
|
+
Fetch the per-route AS metadata document.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
curl -s "${GATEWAY}/.well-known/oauth-authorization-server/mcp/${SLUG}" | jq
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Expected response shape (truncated to the fields you care about):
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"issuer": "https://gateway.example.com/mcp/linear-v1",
|
|
111
|
+
"authorization_endpoint": "https://gateway.example.com/oauth/authorize/mcp/linear-v1",
|
|
112
|
+
"token_endpoint": "https://gateway.example.com/oauth/token",
|
|
113
|
+
"registration_endpoint": "https://gateway.example.com/oauth/register",
|
|
114
|
+
"revocation_endpoint": "https://gateway.example.com/oauth/revoke",
|
|
115
|
+
"scopes_supported": ["mcp:tools"],
|
|
116
|
+
"response_types_supported": ["code"],
|
|
117
|
+
"grant_types_supported": ["authorization_code", "refresh_token"],
|
|
118
|
+
"code_challenge_methods_supported": ["S256"],
|
|
119
|
+
"token_endpoint_auth_methods_supported": [
|
|
120
|
+
"none",
|
|
121
|
+
"client_secret_basic",
|
|
122
|
+
"client_secret_post",
|
|
123
|
+
"private_key_jwt"
|
|
124
|
+
],
|
|
125
|
+
"client_id_metadata_document_supported": true
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Capture the URLs you'll need:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
AS_METADATA=$(curl -s "${GATEWAY}/.well-known/oauth-authorization-server/mcp/${SLUG}")
|
|
133
|
+
AUTH_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.authorization_endpoint')
|
|
134
|
+
TOKEN_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.token_endpoint')
|
|
135
|
+
REGISTRATION_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.registration_endpoint')
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If `code_challenge_methods_supported` doesn't include `S256`, something is
|
|
139
|
+
wrong with the gateway configuration. The spec requires `S256` and the
|
|
140
|
+
gateway always advertises it.
|
|
141
|
+
|
|
142
|
+
1. **Register a client (DCR).**
|
|
143
|
+
|
|
144
|
+
For this test, register a public client with
|
|
145
|
+
`token_endpoint_auth_method: "none"`. This is the simplest mode and matches
|
|
146
|
+
what a CLI client would use.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
DCR_RESPONSE=$(curl -s -X POST "${REGISTRATION_ENDPOINT}" \
|
|
150
|
+
-H "content-type: application/json" \
|
|
151
|
+
-d "{
|
|
152
|
+
\"client_name\": \"Manual OAuth Test\",
|
|
153
|
+
\"redirect_uris\": [\"${REDIRECT_URI}\"],
|
|
154
|
+
\"grant_types\": [\"authorization_code\", \"refresh_token\"],
|
|
155
|
+
\"response_types\": [\"code\"],
|
|
156
|
+
\"token_endpoint_auth_method\": \"none\",
|
|
157
|
+
\"scope\": \"mcp:tools\"
|
|
158
|
+
}")
|
|
159
|
+
|
|
160
|
+
echo "$DCR_RESPONSE" | jq
|
|
161
|
+
CLIENT_ID=$(echo "$DCR_RESPONSE" | jq -r '.client_id')
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Expected response shape:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"client_id": "dcr:abc123...",
|
|
169
|
+
"client_id_issued_at": 1747958400,
|
|
170
|
+
"client_id_metadata_document_supported": true,
|
|
171
|
+
"client_name": "Manual OAuth Test",
|
|
172
|
+
"redirect_uris": ["http://localhost:8765/callback"],
|
|
173
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
174
|
+
"response_types": ["code"],
|
|
175
|
+
"token_endpoint_auth_method": "none",
|
|
176
|
+
"scope": "mcp:tools"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The client ID is opaque. DCR clients expire 90 days after issuance.
|
|
181
|
+
|
|
182
|
+
1. **Build the authorize URL with PKCE.**
|
|
183
|
+
|
|
184
|
+
Generate a PKCE verifier and S256 challenge, plus a state value for CSRF.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '\n=+/' | cut -c1-128)
|
|
188
|
+
CODE_CHALLENGE=$(printf "%s" "$CODE_VERIFIER" | openssl dgst -sha256 -binary \
|
|
189
|
+
| openssl base64 | tr '/+' '_-' | tr -d '=')
|
|
190
|
+
STATE=$(openssl rand -hex 16)
|
|
191
|
+
RESOURCE=$(echo "$AS_METADATA" | jq -r '.issuer')
|
|
192
|
+
|
|
193
|
+
echo "CODE_VERIFIER: $CODE_VERIFIER"
|
|
194
|
+
echo "CODE_CHALLENGE: $CODE_CHALLENGE"
|
|
195
|
+
echo "STATE: $STATE"
|
|
196
|
+
echo "RESOURCE: $RESOURCE"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Build the authorize URL. The `resource` parameter is **required** by the MCP
|
|
200
|
+
spec on every authorization and token request.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
AUTH_URL="${AUTH_ENDPOINT}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=${STATE}&scope=mcp:tools&resource=$(printf %s "$RESOURCE" | jq -sRr @uri)"
|
|
204
|
+
|
|
205
|
+
echo "Open this URL in a browser:"
|
|
206
|
+
echo "$AUTH_URL"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Open the URL in a browser. The flow is:
|
|
210
|
+
1. The gateway redirects you to your IdP's login page.
|
|
211
|
+
2. You authenticate at the IdP.
|
|
212
|
+
3. The IdP redirects back to the gateway's `/oauth/callback`.
|
|
213
|
+
4. The gateway renders the consent setup page.
|
|
214
|
+
5. You click **Authorize**.
|
|
215
|
+
6. The gateway redirects to your `redirect_uri` with `?code=...&state=...`.
|
|
216
|
+
|
|
217
|
+
Capture the `code` value from the final redirect URL. There's no listener on
|
|
218
|
+
`http://localhost:8765`, so the browser shows a connection-refused page —
|
|
219
|
+
that's expected. Copy the `code` value out of the address bar.
|
|
220
|
+
|
|
221
|
+
:::warning
|
|
222
|
+
|
|
223
|
+
The authorization code is single-use and short-lived (typically 30 seconds).
|
|
224
|
+
Run the next step immediately after copying it.
|
|
225
|
+
|
|
226
|
+
:::
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
read -p "Enter the authorization code from the redirect URL: " AUTH_CODE
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
1. **Exchange the code for tokens.**
|
|
233
|
+
|
|
234
|
+
`POST /oauth/token` with the authorization-code grant. Public clients send
|
|
235
|
+
`client_id` in the form body; confidential clients use HTTP Basic.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
TOKEN_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
|
|
239
|
+
-H "content-type: application/x-www-form-urlencoded" \
|
|
240
|
+
--data-urlencode "grant_type=authorization_code" \
|
|
241
|
+
--data-urlencode "code=${AUTH_CODE}" \
|
|
242
|
+
--data-urlencode "redirect_uri=${REDIRECT_URI}" \
|
|
243
|
+
--data-urlencode "code_verifier=${CODE_VERIFIER}" \
|
|
244
|
+
--data-urlencode "client_id=${CLIENT_ID}" \
|
|
245
|
+
--data-urlencode "resource=${RESOURCE}")
|
|
246
|
+
|
|
247
|
+
echo "$TOKEN_RESPONSE" | jq
|
|
248
|
+
|
|
249
|
+
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
|
|
250
|
+
REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.refresh_token')
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Expected response shape:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"access_token": "at_...",
|
|
258
|
+
"token_type": "Bearer",
|
|
259
|
+
"expires_in": 900,
|
|
260
|
+
"refresh_token": "rt_...",
|
|
261
|
+
"scope": "mcp:tools",
|
|
262
|
+
"resource": "https://gateway.example.com/mcp/linear-v1"
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
A common failure mode here is `invalid_grant` because the authorization code
|
|
267
|
+
expired or was already used. Re-run from step 4.
|
|
268
|
+
|
|
269
|
+
Another common one is `invalid_request` if you forget the `code_verifier` or
|
|
270
|
+
omit the `resource` parameter.
|
|
271
|
+
|
|
272
|
+
1. **Call the MCP endpoint with the access token.**
|
|
273
|
+
|
|
274
|
+
Now the access token can be presented as a bearer credential on the MCP
|
|
275
|
+
route.
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
curl -s -X POST "${GATEWAY}/mcp/${SLUG}" \
|
|
279
|
+
-H "authorization: Bearer ${ACCESS_TOKEN}" \
|
|
280
|
+
-H "content-type: application/json" \
|
|
281
|
+
-H "accept: application/json, text/event-stream" \
|
|
282
|
+
-H "mcp-protocol-version: 2025-11-25" \
|
|
283
|
+
-d '{
|
|
284
|
+
"jsonrpc": "2.0",
|
|
285
|
+
"id": "1",
|
|
286
|
+
"method": "initialize",
|
|
287
|
+
"params": {
|
|
288
|
+
"protocolVersion": "2025-11-25",
|
|
289
|
+
"capabilities": {},
|
|
290
|
+
"clientInfo": {
|
|
291
|
+
"name": "manual-test",
|
|
292
|
+
"version": "0.0.0"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}' | jq
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Expected response is a JSON-RPC result with the upstream's `serverInfo` and
|
|
299
|
+
`capabilities`:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"jsonrpc": "2.0",
|
|
304
|
+
"id": "1",
|
|
305
|
+
"result": {
|
|
306
|
+
"protocolVersion": "2025-11-25",
|
|
307
|
+
"capabilities": {
|
|
308
|
+
"tools": {}
|
|
309
|
+
},
|
|
310
|
+
"serverInfo": {
|
|
311
|
+
"name": "linear",
|
|
312
|
+
"version": "..."
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
If you see a JSON-RPC error with `code: -32042`
|
|
319
|
+
(`URLElicitationRequiredError`), the **upstream** MCP server requires OAuth
|
|
320
|
+
and the user hasn't connected to it yet. Open the `authUrl` in the error
|
|
321
|
+
payload's `data` field in a browser. See
|
|
322
|
+
[Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx) for the full
|
|
323
|
+
flow.
|
|
324
|
+
|
|
325
|
+
If you see a `401`, the bearer token is missing, expired, revoked, or bound
|
|
326
|
+
to a different route — the response `WWW-Authenticate` header includes a
|
|
327
|
+
reason code via `error="..."`.
|
|
328
|
+
|
|
329
|
+
If you see a `403` with `error="insufficient_scope"`, the token has the wrong
|
|
330
|
+
scope. The gateway only issues `mcp:tools` today.
|
|
331
|
+
|
|
332
|
+
1. **Refresh the access token.**
|
|
333
|
+
|
|
334
|
+
The access token expires in 15 minutes by default. Exchange the refresh token
|
|
335
|
+
for a new pair using the `refresh_token` grant.
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
REFRESH_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
|
|
339
|
+
-H "content-type: application/x-www-form-urlencoded" \
|
|
340
|
+
--data-urlencode "grant_type=refresh_token" \
|
|
341
|
+
--data-urlencode "refresh_token=${REFRESH_TOKEN}" \
|
|
342
|
+
--data-urlencode "client_id=${CLIENT_ID}" \
|
|
343
|
+
--data-urlencode "resource=${RESOURCE}")
|
|
344
|
+
|
|
345
|
+
echo "$REFRESH_RESPONSE" | jq
|
|
346
|
+
|
|
347
|
+
ACCESS_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.access_token')
|
|
348
|
+
NEW_REFRESH_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.refresh_token')
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The refresh token rotates on every use. Presenting the old refresh token
|
|
352
|
+
again will **revoke the entire grant** — that's the spec's defense against
|
|
353
|
+
refresh-token replay. Always use the most recently issued refresh token.
|
|
354
|
+
|
|
355
|
+
The new access token can be used immediately on subsequent `/mcp/{slug}`
|
|
356
|
+
requests.
|
|
357
|
+
|
|
358
|
+
1. **Revoke the tokens (optional cleanup).**
|
|
359
|
+
|
|
360
|
+
When you're done testing, revoke the grant.
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
curl -s -i -X POST "${GATEWAY}/oauth/revoke" \
|
|
364
|
+
-H "content-type: application/x-www-form-urlencoded" \
|
|
365
|
+
--data-urlencode "token=${NEW_REFRESH_TOKEN}" \
|
|
366
|
+
--data-urlencode "token_type_hint=refresh_token" \
|
|
367
|
+
--data-urlencode "client_id=${CLIENT_ID}"
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Per RFC 7009, the gateway responds with `200 OK` and an empty body for both
|
|
371
|
+
successful revocations and unknown tokens. Subsequent MCP requests with the
|
|
372
|
+
revoked access token return `401`.
|
|
373
|
+
|
|
374
|
+
</Stepper>
|
|
375
|
+
|
|
376
|
+
## Putting it all together
|
|
377
|
+
|
|
378
|
+
Here's a single Bash script that runs every step except the browser-based
|
|
379
|
+
authorize redirect. Save it as `test-oauth.sh` and run it after editing the
|
|
380
|
+
configuration block at the top.
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
#!/usr/bin/env bash
|
|
384
|
+
# Manual OAuth flow test for the Zuplo MCP Gateway.
|
|
385
|
+
# Walks discovery → DCR → authorize URL → code exchange → MCP request → refresh.
|
|
386
|
+
# The authorize step is browser-based; the script pauses for you to paste the code.
|
|
387
|
+
|
|
388
|
+
set -euo pipefail
|
|
389
|
+
|
|
390
|
+
# ----- Configuration -----
|
|
391
|
+
GATEWAY="https://gateway.example.com"
|
|
392
|
+
SLUG="linear-v1"
|
|
393
|
+
REDIRECT_URI="http://localhost:8765/callback"
|
|
394
|
+
# -------------------------
|
|
395
|
+
|
|
396
|
+
echo "==> Step 1: discover protected resource"
|
|
397
|
+
PRM_URL="${GATEWAY}/.well-known/oauth-protected-resource/mcp/${SLUG}"
|
|
398
|
+
echo "PRM: ${PRM_URL}"
|
|
399
|
+
curl -s "${PRM_URL}" | jq -r '{authorization_servers, scopes_supported, mcp_protocol_version}'
|
|
400
|
+
|
|
401
|
+
echo
|
|
402
|
+
echo "==> Step 2: fetch AS metadata"
|
|
403
|
+
AS_METADATA=$(curl -s "${GATEWAY}/.well-known/oauth-authorization-server/mcp/${SLUG}")
|
|
404
|
+
AUTH_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.authorization_endpoint')
|
|
405
|
+
TOKEN_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.token_endpoint')
|
|
406
|
+
REGISTRATION_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.registration_endpoint')
|
|
407
|
+
RESOURCE=$(echo "$AS_METADATA" | jq -r '.issuer')
|
|
408
|
+
echo "issuer: $RESOURCE"
|
|
409
|
+
echo "authorize: $AUTH_ENDPOINT"
|
|
410
|
+
echo "token: $TOKEN_ENDPOINT"
|
|
411
|
+
|
|
412
|
+
echo
|
|
413
|
+
echo "==> Step 3: register client (DCR)"
|
|
414
|
+
DCR_RESPONSE=$(curl -s -X POST "${REGISTRATION_ENDPOINT}" \
|
|
415
|
+
-H "content-type: application/json" \
|
|
416
|
+
-d "{
|
|
417
|
+
\"client_name\": \"Manual OAuth Test\",
|
|
418
|
+
\"redirect_uris\": [\"${REDIRECT_URI}\"],
|
|
419
|
+
\"grant_types\": [\"authorization_code\", \"refresh_token\"],
|
|
420
|
+
\"response_types\": [\"code\"],
|
|
421
|
+
\"token_endpoint_auth_method\": \"none\",
|
|
422
|
+
\"scope\": \"mcp:tools\"
|
|
423
|
+
}")
|
|
424
|
+
CLIENT_ID=$(echo "$DCR_RESPONSE" | jq -r '.client_id')
|
|
425
|
+
echo "client_id: $CLIENT_ID"
|
|
426
|
+
|
|
427
|
+
echo
|
|
428
|
+
echo "==> Step 4: build authorize URL with PKCE"
|
|
429
|
+
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '\n=+/' | cut -c1-128)
|
|
430
|
+
CODE_CHALLENGE=$(printf "%s" "$CODE_VERIFIER" | openssl dgst -sha256 -binary \
|
|
431
|
+
| openssl base64 | tr '/+' '_-' | tr -d '=')
|
|
432
|
+
STATE=$(openssl rand -hex 16)
|
|
433
|
+
RESOURCE_ENC=$(printf "%s" "$RESOURCE" | jq -sRr @uri)
|
|
434
|
+
AUTH_URL="${AUTH_ENDPOINT}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=${STATE}&scope=mcp:tools&resource=${RESOURCE_ENC}"
|
|
435
|
+
echo
|
|
436
|
+
echo "Open this URL in a browser:"
|
|
437
|
+
echo "$AUTH_URL"
|
|
438
|
+
echo
|
|
439
|
+
echo "After completing login and consent, copy the 'code' query parameter from the redirect URL."
|
|
440
|
+
read -r -p "Enter the authorization code: " AUTH_CODE
|
|
441
|
+
|
|
442
|
+
echo
|
|
443
|
+
echo "==> Step 5: exchange code for tokens"
|
|
444
|
+
TOKEN_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
|
|
445
|
+
-H "content-type: application/x-www-form-urlencoded" \
|
|
446
|
+
--data-urlencode "grant_type=authorization_code" \
|
|
447
|
+
--data-urlencode "code=${AUTH_CODE}" \
|
|
448
|
+
--data-urlencode "redirect_uri=${REDIRECT_URI}" \
|
|
449
|
+
--data-urlencode "code_verifier=${CODE_VERIFIER}" \
|
|
450
|
+
--data-urlencode "client_id=${CLIENT_ID}" \
|
|
451
|
+
--data-urlencode "resource=${RESOURCE}")
|
|
452
|
+
echo "$TOKEN_RESPONSE" | jq
|
|
453
|
+
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
|
|
454
|
+
REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.refresh_token')
|
|
455
|
+
|
|
456
|
+
echo
|
|
457
|
+
echo "==> Step 6: call MCP endpoint with the access token"
|
|
458
|
+
curl -s -X POST "${GATEWAY}/mcp/${SLUG}" \
|
|
459
|
+
-H "authorization: Bearer ${ACCESS_TOKEN}" \
|
|
460
|
+
-H "content-type: application/json" \
|
|
461
|
+
-H "accept: application/json, text/event-stream" \
|
|
462
|
+
-H "mcp-protocol-version: 2025-11-25" \
|
|
463
|
+
-d '{
|
|
464
|
+
"jsonrpc": "2.0",
|
|
465
|
+
"id": "1",
|
|
466
|
+
"method": "initialize",
|
|
467
|
+
"params": {
|
|
468
|
+
"protocolVersion": "2025-11-25",
|
|
469
|
+
"capabilities": {},
|
|
470
|
+
"clientInfo": { "name": "manual-test", "version": "0.0.0" }
|
|
471
|
+
}
|
|
472
|
+
}' | jq
|
|
473
|
+
|
|
474
|
+
echo
|
|
475
|
+
echo "==> Step 7: refresh"
|
|
476
|
+
REFRESH_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
|
|
477
|
+
-H "content-type: application/x-www-form-urlencoded" \
|
|
478
|
+
--data-urlencode "grant_type=refresh_token" \
|
|
479
|
+
--data-urlencode "refresh_token=${REFRESH_TOKEN}" \
|
|
480
|
+
--data-urlencode "client_id=${CLIENT_ID}" \
|
|
481
|
+
--data-urlencode "resource=${RESOURCE}")
|
|
482
|
+
echo "$REFRESH_RESPONSE" | jq
|
|
483
|
+
|
|
484
|
+
echo
|
|
485
|
+
echo "Done."
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Make it executable and run it:
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
chmod +x test-oauth.sh
|
|
492
|
+
./test-oauth.sh
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Common issues
|
|
496
|
+
|
|
497
|
+
- **`401` on every MCP request after token exchange.** Token bound to a
|
|
498
|
+
different route than the one you're calling. Each token is scoped to one MCP
|
|
499
|
+
route. Either re-run for the intended route or call the route you authorized
|
|
500
|
+
for.
|
|
501
|
+
- **`401` with `error="invalid_token"` after a token reuse.** Refresh tokens
|
|
502
|
+
rotate on every use — presenting an old one revokes the entire grant. Re-run
|
|
503
|
+
the full flow.
|
|
504
|
+
- **`invalid_request` at the token endpoint.** Most often a missing `resource`
|
|
505
|
+
parameter or a missing `code_verifier`. Both are required.
|
|
506
|
+
- **`invalid_grant` at the token endpoint.** The authorization code expired or
|
|
507
|
+
was already redeemed. Re-run from step 4.
|
|
508
|
+
- **`invalid_audience`.** The bearer token is being used at a route whose
|
|
509
|
+
canonical resource URI doesn't match the token's `resource` claim. A
|
|
510
|
+
misconfigured custom domain or proxy can cause this.
|
|
511
|
+
- **The browser shows the gateway's consent page but the **Authorize** button is
|
|
512
|
+
disabled.** The route has an upstream that hasn't been connected yet. Click
|
|
513
|
+
the per-upstream **Connect** button first. See
|
|
514
|
+
[upstream OAuth](./upstream-oauth.mdx).
|
|
515
|
+
- **JSON-RPC error `-32042` (`URLElicitationRequiredError`).** The downstream
|
|
516
|
+
OAuth succeeded but the upstream MCP server requires OAuth and the user hasn't
|
|
517
|
+
connected. Open the `authUrl` in the error payload's `data` field in a
|
|
518
|
+
browser.
|
|
519
|
+
|
|
520
|
+
## Related
|
|
521
|
+
|
|
522
|
+
- [Authentication overview](./overview.mdx) — the full
|
|
523
|
+
[identity provider catalog](./overview.mdx#identity-providers) and per-IdP
|
|
524
|
+
setup links.
|
|
525
|
+
- [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx)
|
|
526
|
+
- [Test clients](../test-clients.mdx) — exercise the same OAuth flow through the
|
|
527
|
+
MCP Inspector and MCPJam GUIs instead of `curl`.
|
|
528
|
+
- [MCP authorization spec, revision 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
|