zuplo 7.8.0 → 7.8.1
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/articles/accounts/enterprise-sso.mdx +5 -0
- package/docs/articles/accounts/renew-sso-credentials.mdx +142 -0
- package/docs/mcp-gateway/auth/configuring-auth0.mdx +8 -0
- package/docs/mcp-gateway/auth/configuring-generic-oidc.mdx +13 -7
- package/docs/mcp-gateway/auth/overview.mdx +4 -4
- package/docs/mcp-gateway/auth/upstream-oauth.mdx +12 -0
- package/docs/mcp-gateway/code-config/local-development.mdx +72 -40
- package/docs/mcp-gateway/code-config/overview.mdx +8 -8
- package/docs/mcp-gateway/how-to/connect-upstream-oauth.mdx +14 -0
- package/docs/mcp-gateway/quickstart-local.mdx +19 -31
- package/docs/mcp-gateway/quickstart.mdx +25 -25
- package/docs/policies/mcp-token-exchange-inbound/doc.md +62 -39
- package/docs/policies/mcp-token-exchange-inbound/schema.json +9 -1
- package/package.json +5 -5
|
@@ -48,6 +48,11 @@ This will open the Auth0 configuration window where you can make changes to your
|
|
|
48
48
|
connection settings. This is useful if you need to rotate certificates or client
|
|
49
49
|
secrets or if you need to add or remove domains.
|
|
50
50
|
|
|
51
|
+
Client secrets and signing certificates expire, and an expired credential blocks
|
|
52
|
+
sign-in for everyone on the connection. See
|
|
53
|
+
[Renew SSO Credentials](./renew-sso-credentials.mdx) for how to rotate a
|
|
54
|
+
credential, monitor expiry dates, and recover if a credential already expired.
|
|
55
|
+
|
|
51
56
|
### Disable Single Sign On
|
|
52
57
|
|
|
53
58
|
If you need to disable SSO completely, contact Zuplo support for assistance.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Renew SSO Credentials
|
|
3
|
+
sidebar_label: Renew SSO Credentials
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<EnterpriseFeature name="Single Sign On" />
|
|
7
|
+
|
|
8
|
+
Your SSO connection authenticates Zuplo to your identity provider with a
|
|
9
|
+
credential that you create and own:
|
|
10
|
+
|
|
11
|
+
- An OIDC or OAuth2 **client secret**, for connections that use OpenID Connect.
|
|
12
|
+
- A **SAML signing certificate**, for connections that use SAML.
|
|
13
|
+
|
|
14
|
+
Both kinds expire. When one expires, the identity provider stops accepting the
|
|
15
|
+
sign-in request, and everyone who signs in to Zuplo through that connection is
|
|
16
|
+
locked out until you replace the credential.
|
|
17
|
+
|
|
18
|
+
Renewing the credential is your responsibility. Zuplo can't create a credential
|
|
19
|
+
in your identity provider or extend the expiry date of an existing one.
|
|
20
|
+
|
|
21
|
+
## Nothing reminds you automatically
|
|
22
|
+
|
|
23
|
+
Most identity providers notify you about a narrower set of credentials than
|
|
24
|
+
administrators expect:
|
|
25
|
+
|
|
26
|
+
- **Microsoft Entra ID** sends automatic expiry email for SAML signing
|
|
27
|
+
certificates on enterprise applications. It doesn't send email about client
|
|
28
|
+
secrets on app registrations.
|
|
29
|
+
- **Auth0**, which Zuplo uses to broker enterprise SSO, sends email about an
|
|
30
|
+
expiring SAML certificate on a connection. It doesn't send email about an
|
|
31
|
+
expiring OIDC client secret.
|
|
32
|
+
|
|
33
|
+
If your connection uses a client secret, assume that no one notifies you. Record
|
|
34
|
+
the expiry date when you create the secret and set your own reminder.
|
|
35
|
+
|
|
36
|
+
## What an expired credential looks like
|
|
37
|
+
|
|
38
|
+
Sign-in fails for every user on the connection, and the failure happens at your
|
|
39
|
+
identity provider before the user returns to Zuplo. With Microsoft Entra ID, the
|
|
40
|
+
error names the app registration:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
The provided client secret keys for app '<app-id>' are expired.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Other providers word it differently. Look for `invalid_client` from an OIDC
|
|
47
|
+
provider, or a message about an expired or untrusted certificate from a SAML
|
|
48
|
+
provider.
|
|
49
|
+
|
|
50
|
+
Two signals point at credential expiry rather than a configuration change:
|
|
51
|
+
sign-in worked until a specific date, and the failure affects every user on the
|
|
52
|
+
connection at once.
|
|
53
|
+
|
|
54
|
+
## Renew a credential before it expires
|
|
55
|
+
|
|
56
|
+
While you can still sign in to Zuplo, rotate the credential yourself:
|
|
57
|
+
|
|
58
|
+
<Stepper>
|
|
59
|
+
|
|
60
|
+
1. In your identity provider, create a new client secret or signing certificate
|
|
61
|
+
for the application that Zuplo connects to. Keep the old credential active
|
|
62
|
+
until the new one works.
|
|
63
|
+
1. Open
|
|
64
|
+
[**Account Settings → Security**](https://portal.zuplo.com/+/account/settings/security)
|
|
65
|
+
in the Zuplo Portal and click "Edit Connection" in the "Single Sign On (SSO)"
|
|
66
|
+
section. See [Edit Single Sign On](./enterprise-sso.mdx#edit-single-sign-on)
|
|
67
|
+
for details.
|
|
68
|
+
1. Enter the new client secret or upload the new certificate, then save the
|
|
69
|
+
connection.
|
|
70
|
+
1. Sign out and sign back in to Zuplo to confirm the connection works.
|
|
71
|
+
1. Remove the old credential from your identity provider.
|
|
72
|
+
|
|
73
|
+
</Stepper>
|
|
74
|
+
|
|
75
|
+
## Recover from a credential that has already expired
|
|
76
|
+
|
|
77
|
+
:::caution
|
|
78
|
+
|
|
79
|
+
If your account requires enterprise SSO, an expired credential locks out every
|
|
80
|
+
user, including administrators. Nobody can reach **Account Settings → Security**
|
|
81
|
+
to fix the connection, so this case isn't self-serve.
|
|
82
|
+
|
|
83
|
+
:::
|
|
84
|
+
|
|
85
|
+
<Stepper>
|
|
86
|
+
|
|
87
|
+
1. In your identity provider, create a new client secret or signing certificate
|
|
88
|
+
for the application that Zuplo connects to.
|
|
89
|
+
1. Email [support@zuplo.com](mailto:support@zuplo.com) and tell them that your
|
|
90
|
+
SSO credential expired. Zuplo can issue a self-service SSO link that lets you
|
|
91
|
+
update the connection without signing in to the Zuplo Portal.
|
|
92
|
+
1. Open the link, enter the new credential, and save the connection. Make sure
|
|
93
|
+
the connection is still enabled.
|
|
94
|
+
1. Sign in to Zuplo to confirm that the connection works.
|
|
95
|
+
1. Remove the old credential from your identity provider.
|
|
96
|
+
|
|
97
|
+
</Stepper>
|
|
98
|
+
|
|
99
|
+
## Stay ahead of the next expiry
|
|
100
|
+
|
|
101
|
+
- **Set a calendar reminder** for 30 days before the expiry date, on a shared
|
|
102
|
+
team calendar rather than one person's. Credential lifetimes outlast staff
|
|
103
|
+
changes.
|
|
104
|
+
- **Choose a shorter lifetime**, such as six or twelve months, instead of the
|
|
105
|
+
longest one your provider offers. Frequent rotation keeps the procedure
|
|
106
|
+
familiar and documented, and a two-year secret usually expires long after
|
|
107
|
+
everyone who set it up has forgotten it exists.
|
|
108
|
+
- **Prefer a certificate credential over a client secret** when your provider
|
|
109
|
+
supports it. Microsoft Entra ID treats certificates as the more secure
|
|
110
|
+
credential type for app registrations.
|
|
111
|
+
- **Rotate with an overlap.** Most providers let you hold two active credentials
|
|
112
|
+
at once, so you can add the new one, switch Zuplo over, verify sign-in, and
|
|
113
|
+
then delete the old one without downtime.
|
|
114
|
+
- **Monitor expiry dates** with the tools your provider gives you. The next
|
|
115
|
+
section links to them.
|
|
116
|
+
|
|
117
|
+
## Provider documentation
|
|
118
|
+
|
|
119
|
+
Each provider manages and monitors its own credentials. Use their documentation
|
|
120
|
+
for the exact steps.
|
|
121
|
+
|
|
122
|
+
**Microsoft Entra ID**
|
|
123
|
+
|
|
124
|
+
- [Add and manage app credentials](https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-credentials)
|
|
125
|
+
— add a client secret or certificate to an app registration.
|
|
126
|
+
- [Renew expiring application credentials recommendation](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/recommendation-renew-expiring-application-credential)
|
|
127
|
+
— flags app-registration credentials that expire within the next 30 days.
|
|
128
|
+
Requires a Microsoft Entra Workload ID license.
|
|
129
|
+
- [Export app registrations with expiring secrets and certificates](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/scripts/powershell-export-apps-with-expiring-secrets)
|
|
130
|
+
— a PowerShell sample that reports expiring credentials across your tenant.
|
|
131
|
+
|
|
132
|
+
**Okta**
|
|
133
|
+
|
|
134
|
+
- [Manage secrets and keys for OIDC app client authentication](https://help.okta.com/oie/en-us/content/topics/apps/oauth-client-cred-mgmt.htm)
|
|
135
|
+
- [Manage signing certificates](https://help.okta.com/en-us/content/topics/apps/manage-signing-certificates.htm)
|
|
136
|
+
|
|
137
|
+
**Google Workspace**
|
|
138
|
+
|
|
139
|
+
- [Maintain SAML certificates](https://support.google.com/a/answer/7394709)
|
|
140
|
+
|
|
141
|
+
For any other identity provider, look for the credential management page of the
|
|
142
|
+
application you created for Zuplo.
|
|
@@ -68,6 +68,11 @@ On the same **Settings** tab:
|
|
|
68
68
|
(plus `http://localhost:9000` for local dev).
|
|
69
69
|
3. Save changes.
|
|
70
70
|
|
|
71
|
+
For local development, use the same hostname in your MCP client URL, **Allowed
|
|
72
|
+
Callback URLs**, and **Allowed Web Origins**. If your client uses `127.0.0.1`,
|
|
73
|
+
replace `localhost` with `127.0.0.1` in both Auth0 settings. See
|
|
74
|
+
[Local development](../code-config/local-development.mdx#use-one-loopback-origin-everywhere).
|
|
75
|
+
|
|
71
76
|
### Optional: Set an audience
|
|
72
77
|
|
|
73
78
|
If you want Auth0 to issue identity-bound API access tokens (for example, to
|
|
@@ -112,6 +117,9 @@ Set the three environment variables in your Zuplo project's environment
|
|
|
112
117
|
configuration. `AUTH0_DOMAIN` is the bare hostname; the secret values belong in
|
|
113
118
|
the project secret store.
|
|
114
119
|
|
|
120
|
+
Use the same variable names in your environment settings and the `$env(...)`
|
|
121
|
+
references in `policies.json`.
|
|
122
|
+
|
|
115
123
|
Attach the policy to each MCP route in `config/routes.oas.json`:
|
|
116
124
|
|
|
117
125
|
```jsonc
|
|
@@ -55,7 +55,9 @@ at the same place:
|
|
|
55
55
|
client", "confidential client" — terminology varies).
|
|
56
56
|
2. Set the **redirect URI** to `https://<gateway-host>/__zuplo/oauth/callback`.
|
|
57
57
|
Add `http://localhost:9000/__zuplo/oauth/callback` for local development with
|
|
58
|
-
`zuplo dev`.
|
|
58
|
+
`zuplo dev`. If your MCP client uses `127.0.0.1`, use that hostname in the
|
|
59
|
+
redirect URI too. See
|
|
60
|
+
[Local development](../code-config/local-development.mdx#use-one-loopback-origin-everywhere).
|
|
59
61
|
3. Note the **client ID** and **client secret**.
|
|
60
62
|
4. Restrict the application to the users or groups who should be able to
|
|
61
63
|
authenticate against the gateway.
|
|
@@ -128,20 +130,20 @@ export function runtimeInit(runtime: RuntimeExtensions) {
|
|
|
128
130
|
}
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
Attach the same MCP OAuth policy to every MCP route. Attaching different OAuth
|
|
134
|
+
policies causes a configuration error.
|
|
133
135
|
|
|
134
136
|
## Local development shortcut
|
|
135
137
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
To test locally without an identity provider, set `browserLogin.url` to the
|
|
139
|
+
dev-login endpoint:
|
|
138
140
|
|
|
139
141
|
```json
|
|
140
142
|
{
|
|
141
143
|
"options": {
|
|
142
144
|
"oidc": {
|
|
143
|
-
"issuer": "http://
|
|
144
|
-
"jwksUrl": "http://
|
|
145
|
+
"issuer": "http://127.0.0.1:9000",
|
|
146
|
+
"jwksUrl": "http://127.0.0.1:9000/jwks"
|
|
145
147
|
},
|
|
146
148
|
"browserLogin": {
|
|
147
149
|
"url": "http://127.0.0.1:9000/__zuplo/oauth/dev-login"
|
|
@@ -154,6 +156,10 @@ When `browserLogin.url` points at `/__zuplo/oauth/dev-login`, you don't need
|
|
|
154
156
|
`tokenUrl`, `clientId`, or `clientSecret`. The endpoint is only served on
|
|
155
157
|
loopback origins; production deployments cannot reach it.
|
|
156
158
|
|
|
159
|
+
The policy requires `oidc.issuer` and `oidc.jwksUrl`, but dev-login doesn't use
|
|
160
|
+
them. These values are placeholders; the gateway doesn't serve `/jwks`. Use
|
|
161
|
+
`127.0.0.1` in your MCP client URL to match this example.
|
|
162
|
+
|
|
157
163
|
See the [local development guide](../code-config/local-development.mdx) for the
|
|
158
164
|
rest of the local setup.
|
|
159
165
|
|
|
@@ -160,10 +160,10 @@ connect-required error is returned — the proxy forwards transparently.
|
|
|
160
160
|
|
|
161
161
|
## Identity providers
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
Use a provider-specific policy from the table, or the generic
|
|
164
|
+
`mcp-oauth-inbound` policy for other OIDC providers. Attach the same policy to
|
|
165
|
+
every MCP route in the project. Attaching different MCP OAuth policies causes a
|
|
166
|
+
configuration error.
|
|
167
167
|
|
|
168
168
|
Each wrapper has the same shape: it takes a small set of provider-specific
|
|
169
169
|
fields (a domain, a tenant ID, a subdomain, and so on) plus `clientId` and
|
|
@@ -89,6 +89,18 @@ can request tokens. The `clientRegistration` option controls how:
|
|
|
89
89
|
manages OAuth client lifecycle centrally, the upstream provider requires an
|
|
90
90
|
approved client, or one OAuth client should be shared across multiple routes.
|
|
91
91
|
|
|
92
|
+
Manual mode needs a redirect URI registered with the upstream provider. Register
|
|
93
|
+
this URI, where `<connection-id>` is the connection's `id`:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
https://<gateway-host>/__zuplo/auth/connections/<connection-id>/callback
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This is different from `/__zuplo/oauth/callback`, which is the callback for the
|
|
100
|
+
identity provider that signs users in to the gateway itself. See
|
|
101
|
+
[Upstream connection endpoints](../reference.mdx#upstream-connection-endpoints)
|
|
102
|
+
in the MCP Gateway reference.
|
|
103
|
+
|
|
92
104
|
Both modes are first-class. CIMD documents are accessible to the upstream
|
|
93
105
|
provider over HTTPS — the upstream fetches them as part of its OAuth
|
|
94
106
|
registration flow. The CIMD URL includes the `authProfileId` query parameter so
|
|
@@ -2,61 +2,53 @@
|
|
|
2
2
|
title: "Local development"
|
|
3
3
|
sidebar_label: "Local development"
|
|
4
4
|
description:
|
|
5
|
-
Run the
|
|
6
|
-
|
|
7
|
-
client against 127.0.0.1, and recover cleanly from the known workerd restart
|
|
8
|
-
quirk.
|
|
5
|
+
Run the MCP Gateway locally, test OAuth without an identity provider, and
|
|
6
|
+
configure local callback URLs and environment variables for your MCP clients.
|
|
9
7
|
---
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
gateway: the gateway prefers `127.0.0.1` over `localhost`, OAuth login can be
|
|
14
|
-
short-circuited entirely in dev, and the local `workerd` worker needs a full
|
|
15
|
-
restart after some MCP client connect attempts.
|
|
9
|
+
Use `zuplo dev` to run the MCP Gateway locally on port `9000` with hot reload.
|
|
10
|
+
For local testing, you can sign in without setting up an identity provider.
|
|
16
11
|
|
|
17
12
|
## Start the gateway
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
Start the development server from the project root:
|
|
20
15
|
|
|
21
16
|
```bash
|
|
22
17
|
zuplo dev
|
|
23
18
|
```
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`http://127.0.0.1:9000/mcp/linear-v1`.
|
|
20
|
+
If the project has the standard `dev` script, `npm run dev` runs the same
|
|
21
|
+
command.
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
The gateway listens on port `9000`. Each MCP route in `routes.oas.json` becomes
|
|
24
|
+
reachable there — for example `http://127.0.0.1:9000/mcp/linear-v1`.
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
request origin. Other loopback aliases (`localhost`, `::1`, `[::1]`) can cause
|
|
33
|
-
subtle OAuth issues in local dev.
|
|
26
|
+
## Use one loopback origin everywhere
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
Use either `http://localhost:9000` or `http://127.0.0.1:9000` consistently for
|
|
29
|
+
local gateway URLs. The gateway treats them as different origins.
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
// Good
|
|
39
|
-
"url": "http://127.0.0.1:9000/mcp/linear-v1"
|
|
31
|
+
Use the same hostname in these settings:
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
- The gateway URL in your MCP client configuration.
|
|
34
|
+
- `browserLogin.url` when using the local dev-login endpoint.
|
|
35
|
+
- The gateway callback and allowed-origin URLs registered with your identity
|
|
36
|
+
provider, when testing with one.
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
provider
|
|
38
|
+
Mixing hostnames can cause `oauth_callback_mismatch` during local login or a
|
|
39
|
+
callback URL error from your identity provider. The examples on this page use
|
|
40
|
+
`127.0.0.1`.
|
|
47
41
|
|
|
48
42
|
## Bypass your IdP with `/__zuplo/oauth/dev-login`
|
|
49
43
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
exposes a loopback-only shortcut that skips the IdP round-trip entirely and
|
|
53
|
-
signs you in as a fixed `dev-browser-user` subject.
|
|
44
|
+
The local `/__zuplo/oauth/dev-login` endpoint signs you in as `dev-browser-user`
|
|
45
|
+
without an identity provider.
|
|
54
46
|
|
|
55
47
|
To use it, set `browserLogin.url` to the dev-login URL when configuring the
|
|
56
48
|
OAuth policy:
|
|
57
49
|
|
|
58
50
|
```jsonc
|
|
59
|
-
// config/policies.json
|
|
51
|
+
// config/policies.json
|
|
60
52
|
{
|
|
61
53
|
"name": "dev-oauth",
|
|
62
54
|
"policyType": "mcp-oauth-inbound",
|
|
@@ -64,9 +56,10 @@ OAuth policy:
|
|
|
64
56
|
"module": "$import(@zuplo/runtime/mcp-gateway)",
|
|
65
57
|
"export": "McpOAuthInboundPolicy",
|
|
66
58
|
"options": {
|
|
59
|
+
// Required placeholders for local dev-login.
|
|
67
60
|
"oidc": {
|
|
68
61
|
"issuer": "http://127.0.0.1:9000",
|
|
69
|
-
"jwksUrl": "http://127.0.0.1:9000
|
|
62
|
+
"jwksUrl": "http://127.0.0.1:9000/jwks",
|
|
70
63
|
},
|
|
71
64
|
"browserLogin": {
|
|
72
65
|
"url": "http://127.0.0.1:9000/__zuplo/oauth/dev-login",
|
|
@@ -81,19 +74,55 @@ When `browserLogin.url` points at `/__zuplo/oauth/dev-login`, the
|
|
|
81
74
|
`browserLogin.clientSecret` options aren't required. The consent page renders
|
|
82
75
|
normally.
|
|
83
76
|
|
|
77
|
+
The policy requires `oidc.issuer` and `oidc.jwksUrl`, but dev-login doesn't use
|
|
78
|
+
them. Keep the placeholder values in this example for local testing. The `/jwks`
|
|
79
|
+
URL is a placeholder, not an endpoint the gateway serves.
|
|
80
|
+
|
|
84
81
|
:::caution
|
|
85
82
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
real OIDC provider through one of the
|
|
90
|
-
[IdP-specific wrappers](../auth/overview.mdx#identity-providers).
|
|
83
|
+
Use an OIDC identity provider in deployed environments. The dev-login endpoint
|
|
84
|
+
only accepts requests from loopback origins and returns `403 Forbidden` for
|
|
85
|
+
other origins.
|
|
91
86
|
|
|
92
87
|
:::
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
### Configure local and deployed environments
|
|
90
|
+
|
|
91
|
+
Attach the same MCP OAuth policy to every MCP route. To use dev-login locally
|
|
92
|
+
and an identity provider when deployed, configure a generic `mcp-oauth-inbound`
|
|
93
|
+
policy with environment variables:
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// config/policies.json
|
|
97
|
+
"options": {
|
|
98
|
+
"oidc": {
|
|
99
|
+
"issuer": "$env(OIDC_ISSUER)",
|
|
100
|
+
"jwksUrl": "$env(OIDC_JWKS_URL)",
|
|
101
|
+
},
|
|
102
|
+
"browserLogin": {
|
|
103
|
+
"url": "$env(BROWSER_LOGIN_URL)",
|
|
104
|
+
"tokenUrl": "$env(BROWSER_LOGIN_TOKEN_URL)",
|
|
105
|
+
"clientId": "$env(BROWSER_LOGIN_CLIENT_ID)",
|
|
106
|
+
"clientSecret": "$env(BROWSER_LOGIN_CLIENT_SECRET)",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For local development, set these values:
|
|
112
|
+
|
|
113
|
+
```bash title=".env"
|
|
114
|
+
OIDC_ISSUER=http://127.0.0.1:9000
|
|
115
|
+
OIDC_JWKS_URL=http://127.0.0.1:9000/jwks
|
|
116
|
+
BROWSER_LOGIN_URL=http://127.0.0.1:9000/__zuplo/oauth/dev-login
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Leave `BROWSER_LOGIN_TOKEN_URL`, `BROWSER_LOGIN_CLIENT_ID`, and
|
|
120
|
+
`BROWSER_LOGIN_CLIENT_SECRET` unset locally. In each deployed environment, set
|
|
121
|
+
all six variables to your identity provider's values.
|
|
122
|
+
|
|
123
|
+
Provider-specific policies such as `mcp-auth0-oauth-inbound` use the provider's
|
|
124
|
+
login page and don't support dev-login. See
|
|
125
|
+
[Define one OAuth policy](./overview.mdx#2-define-one-oauth-policy).
|
|
97
126
|
|
|
98
127
|
## Environment variables
|
|
99
128
|
|
|
@@ -115,6 +144,9 @@ AUTH0_AUDIENCE=https://mcp-gateway.example.com
|
|
|
115
144
|
`.env` is read at `zuplo dev` startup. Restart the dev server after adding or
|
|
116
145
|
changing an environment variable.
|
|
117
146
|
|
|
147
|
+
Match your environment variable names to the `$env(...)` references in
|
|
148
|
+
`policies.json`.
|
|
149
|
+
|
|
118
150
|
Never commit `.env` to source control. Instead, check in a `.env.example` (or
|
|
119
151
|
`env.example`) that documents which variables are required and an
|
|
120
152
|
empty/placeholder value for each.
|
|
@@ -61,6 +61,9 @@ supported IdP. The Auth0 case looks like this:
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
Match your environment variable names to the `$env(...)` references in
|
|
65
|
+
`policies.json`.
|
|
66
|
+
|
|
64
67
|
Each wrapper takes a small set of provider-specific options (a domain, a tenant
|
|
65
68
|
ID, a subdomain, and so on) and derives the OIDC URLs from them. For IdPs
|
|
66
69
|
without a dedicated wrapper — Ory Hydra, Authentik, FusionAuth, PingFederate, a
|
|
@@ -70,10 +73,8 @@ the worked example.
|
|
|
70
73
|
|
|
71
74
|
:::caution
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
every MCP route in the project — every route authenticates against the same
|
|
76
|
-
identity provider.
|
|
76
|
+
Attach the same MCP OAuth policy to every MCP route in the project. Attaching
|
|
77
|
+
different OAuth policies causes a configuration error.
|
|
77
78
|
|
|
78
79
|
:::
|
|
79
80
|
|
|
@@ -157,9 +158,8 @@ gateway returns a configuration error on the first matching request.
|
|
|
157
158
|
|
|
158
159
|
## Verify the gateway is wired up
|
|
159
160
|
|
|
160
|
-
Start the project with `zuplo dev`
|
|
161
|
-
|
|
162
|
-
unauthenticated POST:
|
|
161
|
+
Start the project with `zuplo dev`, or `npm run dev` if the project's `dev`
|
|
162
|
+
script runs that command. Then send an unauthenticated request to the route:
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
165
|
curl -i -X POST http://127.0.0.1:9000/mcp/linear-v1 \
|
|
@@ -176,7 +176,7 @@ skips your IdP during development.
|
|
|
176
176
|
## Add more upstreams
|
|
177
177
|
|
|
178
178
|
The pattern is the same for each additional upstream: one MCP OAuth policy stays
|
|
179
|
-
|
|
179
|
+
attached across the project, and one `mcp-token-exchange-*` policy and one route
|
|
180
180
|
get added per new upstream MCP server. Per-user state is keyed by
|
|
181
181
|
`(subjectId, upstreamServerId)`, so each user maintains independent connections
|
|
182
182
|
to each upstream they consent to.
|
|
@@ -90,6 +90,20 @@ upstream OAuth provider:
|
|
|
90
90
|
|
|
91
91
|
Use `$env(...)` for `clientSecret` so the secret stays out of source control.
|
|
92
92
|
|
|
93
|
+
When you register the OAuth app with the upstream provider, register this
|
|
94
|
+
redirect URI, where `<connection-id>` is the policy's `id` (for the example
|
|
95
|
+
policy above, `linear`, inferred from the `mcp-token-exchange-linear` name):
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
https://<gateway-host>/__zuplo/auth/connections/<connection-id>/callback
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This is different from `/__zuplo/oauth/callback`, which is the callback for the
|
|
102
|
+
identity provider that signs users in to the gateway itself. Registering that
|
|
103
|
+
path with the upstream provider fails with `redirect_uri_mismatch`. Both paths
|
|
104
|
+
are listed in the
|
|
105
|
+
[MCP Gateway reference](../reference.mdx#upstream-connection-endpoints).
|
|
106
|
+
|
|
93
107
|
## Set scopes when the upstream needs them
|
|
94
108
|
|
|
95
109
|
When the upstream requires specific scopes that aren't discoverable from MCP
|
|
@@ -70,10 +70,8 @@ older project and the build complains about the compatibility date, see
|
|
|
70
70
|
|
|
71
71
|
2. **Add an OAuth policy with the dev-login shortcut**
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
exposes a loopback-only shortcut that skips the IdP round-trip entirely and
|
|
76
|
-
signs you in as a fixed `dev-browser-user`.
|
|
73
|
+
The local dev-login endpoint signs you in as `dev-browser-user` without an
|
|
74
|
+
identity provider.
|
|
77
75
|
|
|
78
76
|
Open `config/policies.json` and add the generic OAuth policy pointed at the
|
|
79
77
|
dev-login URL:
|
|
@@ -88,7 +86,7 @@ older project and the build complains about the compatibility date, see
|
|
|
88
86
|
"options": {
|
|
89
87
|
"oidc": {
|
|
90
88
|
"issuer": "http://127.0.0.1:9000",
|
|
91
|
-
"jwksUrl": "http://127.0.0.1:9000
|
|
89
|
+
"jwksUrl": "http://127.0.0.1:9000/jwks"
|
|
92
90
|
},
|
|
93
91
|
"browserLogin": {
|
|
94
92
|
"url": "http://127.0.0.1:9000/__zuplo/oauth/dev-login"
|
|
@@ -98,32 +96,20 @@ older project and the build complains about the compatibility date, see
|
|
|
98
96
|
}
|
|
99
97
|
```
|
|
100
98
|
|
|
99
|
+
The policy requires `oidc.issuer` and `oidc.jwksUrl`, but dev-login doesn't
|
|
100
|
+
use them. Keep these placeholder values for this tutorial; the gateway
|
|
101
|
+
doesn't serve `/jwks`.
|
|
102
|
+
|
|
101
103
|
:::caution
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
dev) and selecting between them in `routes.oas.json` by environment.
|
|
105
|
+
Before deploying, configure an OIDC identity provider. The dev-login endpoint
|
|
106
|
+
only accepts requests from loopback origins and returns `403 Forbidden` for
|
|
107
|
+
other origins. See
|
|
108
|
+
[Configure local and deployed environments](./code-config/local-development.mdx#configure-local-and-deployed-environments)
|
|
109
|
+
to use one policy with different environment variables for each environment.
|
|
109
110
|
|
|
110
111
|
:::
|
|
111
112
|
|
|
112
|
-
When you do switch to a real provider, its policy reads credentials from
|
|
113
|
-
`$env(...)` references. Define those values in a `.env` file at the project
|
|
114
|
-
root:
|
|
115
|
-
|
|
116
|
-
```bash title=".env"
|
|
117
|
-
MCP_AUTH0_DOMAIN=your-tenant.us.auth0.com
|
|
118
|
-
MCP_AUTH0_CLIENT_ID=your-auth0-web-app-client-id
|
|
119
|
-
MCP_AUTH0_CLIENT_SECRET=your-auth0-web-app-client-secret
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
`.env` is read when `npm run dev` starts, so restart the dev server after
|
|
123
|
-
adding or changing a variable. Never commit `.env`. Check in a `.env.example`
|
|
124
|
-
with placeholder values instead. The dev-login shortcut above needs no
|
|
125
|
-
environment variables, so you can skip this until you wire up a provider.
|
|
126
|
-
|
|
127
113
|
3. **Add a token-exchange policy for the upstream**
|
|
128
114
|
|
|
129
115
|
Each OAuth-protected upstream gets its own `mcp-token-exchange-inbound`
|
|
@@ -200,6 +186,8 @@ older project and the build complains about the compatibility date, see
|
|
|
200
186
|
npm run dev
|
|
201
187
|
```
|
|
202
188
|
|
|
189
|
+
The project's `dev` script runs `zuplo dev`.
|
|
190
|
+
|
|
203
191
|
The route is now reachable at `http://127.0.0.1:9000/mcp/linear-v1`.
|
|
204
192
|
|
|
205
193
|
:::tip{title="Checkpoint: confirm the OAuth policy is wired up"}
|
|
@@ -219,12 +207,12 @@ older project and the build complains about the compatibility date, see
|
|
|
219
207
|
|
|
220
208
|
:::
|
|
221
209
|
|
|
222
|
-
:::caution{title="Use
|
|
210
|
+
:::caution{title="Use the same hostname for local URLs"}
|
|
223
211
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
local-
|
|
212
|
+
Use `127.0.0.1` in your MCP client URL to match the policy in this tutorial.
|
|
213
|
+
You can use `localhost` instead, but update both URLs to avoid a callback
|
|
214
|
+
mismatch. See
|
|
215
|
+
[Local development](./code-config/local-development.mdx#use-one-loopback-origin-everywhere).
|
|
228
216
|
|
|
229
217
|
:::
|
|
230
218
|
|
|
@@ -124,15 +124,12 @@ result on your machine.
|
|
|
124
124
|
|
|
125
125
|
5. **Configure upstream authentication**
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- **User OAuth**: exchange each user's gateway grant for an upstream MCP
|
|
130
|
-
grant, so every user connects their own Linear account.
|
|
131
|
-
- **Dynamic** OAuth client registration: the gateway registers itself with
|
|
132
|
-
Linear's OAuth server on demand, so no upstream client credentials are
|
|
133
|
-
needed.
|
|
127
|
+
On **Outbound Auth**, leave **User OAuth** selected so each user connects
|
|
128
|
+
their own Linear account. Click **Next**.
|
|
134
129
|
|
|
135
|
-
|
|
130
|
+
Leave **OAuth client registration** set to **Dynamic**. The gateway registers
|
|
131
|
+
with Linear automatically, so you don't need a Linear client ID or secret.
|
|
132
|
+
Click **Finish**.
|
|
136
133
|
|
|
137
134
|
<ModalScreenshot size="md">
|
|
138
135
|
|
|
@@ -153,29 +150,32 @@ result on your machine.
|
|
|
153
150
|
|
|
154
151
|
6. **Set your identity provider's environment variables**
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
In `config/policies.json`, set the Auth0 policy's environment variable
|
|
154
|
+
references to match these names, then save the file:
|
|
155
|
+
|
|
156
|
+
```json title="Auth0 policy options"
|
|
157
|
+
{
|
|
158
|
+
"auth0Domain": "$env(AUTH0_DOMAIN)",
|
|
159
|
+
"clientId": "$env(AUTH0_CLIENT_ID)",
|
|
160
|
+
"clientSecret": "$env(AUTH0_CLIENT_SECRET)"
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Open your project's **Settings** from the navigation bar, then click
|
|
165
|
+
**Environment Variables** under Project Settings.
|
|
160
166
|
|
|
161
167
|
Add one variable per credential. For the Auth0 example:
|
|
162
168
|
|
|
163
|
-
| Name
|
|
164
|
-
|
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
169
|
+
| Name | Value | Secret? |
|
|
170
|
+
| --------------------- | -------------------------- | ------- |
|
|
171
|
+
| `AUTH0_DOMAIN` | `your-tenant.us.auth0.com` | No |
|
|
172
|
+
| `AUTH0_CLIENT_ID` | your application client ID | Yes |
|
|
173
|
+
| `AUTH0_CLIENT_SECRET` | your application secret | Yes |
|
|
168
174
|
|
|
169
175
|
Check the **Secret** box for the client ID and secret so their values are
|
|
170
|
-
hidden in the encrypted secret store. `
|
|
176
|
+
hidden in the encrypted secret store. `AUTH0_DOMAIN` isn't sensitive, so
|
|
171
177
|
leave it unchecked. Click **Save** for each.
|
|
172
178
|
|
|
173
|
-
<ModalScreenshot size="md">
|
|
174
|
-
|
|
175
|
-

|
|
176
|
-
|
|
177
|
-
</ModalScreenshot>
|
|
178
|
-
|
|
179
179
|
:::note
|
|
180
180
|
|
|
181
181
|
A new deployment is needed for environment variable changes to take effect.
|
|
@@ -184,7 +184,7 @@ result on your machine.
|
|
|
184
184
|
|
|
185
185
|
:::caution
|
|
186
186
|
|
|
187
|
-
`
|
|
187
|
+
`AUTH0_DOMAIN` is a bare hostname (`my-tenant.us.auth0.com`), not a URL.
|
|
188
188
|
|
|
189
189
|
:::
|
|
190
190
|
|
|
@@ -108,51 +108,73 @@ as Entra tenant admin consent. If connections start requiring frequent
|
|
|
108
108
|
reauthorization after setting `prompt: false`, remove the option so the consent
|
|
109
109
|
grant is requested again.
|
|
110
110
|
|
|
111
|
-
##
|
|
111
|
+
## OAuth Metadata Validation
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
its protected-resource metadata names
|
|
117
|
-
`https://<instance>.service-now.com/sncapps/...` while the metadata served there
|
|
118
|
-
reports `https://<instance>.service-now.com`. Atlassian and Okta
|
|
119
|
-
org-versus-custom authorization servers publish the same shape.
|
|
113
|
+
The gateway checks OAuth metadata before connecting to an upstream. If a
|
|
114
|
+
provider publishes metadata that does not match its URLs, you can skip either of
|
|
115
|
+
the checks below for that connection.
|
|
120
116
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
Both options default to `false` and support `user-oauth` and `shared-oauth`.
|
|
118
|
+
They are not available with `id-jag`. Leave them disabled unless you have
|
|
119
|
+
confirmed the mismatch with the provider and trust its metadata and endpoints.
|
|
120
|
+
Enable only the option needed for that mismatch.
|
|
121
|
+
|
|
122
|
+
### Authorization Server Issuer Mismatch
|
|
123
|
+
|
|
124
|
+
`skipIssuerMetadataValidation` allows the authorization server's metadata to
|
|
125
|
+
report an `issuer` that differs from the server URL used to discover it.
|
|
126
|
+
|
|
127
|
+
For example, the upstream might advertise `https://auth.example.com/team` as its
|
|
128
|
+
authorization server, while that server's metadata reports
|
|
129
|
+
`https://auth.example.com` as its `issuer`. By default, the gateway rejects this
|
|
130
|
+
mismatch. To allow it, set the following policy options:
|
|
125
131
|
|
|
126
132
|
```json
|
|
127
133
|
{
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
"displayName": "Acme",
|
|
135
|
+
"authMode": "user-oauth",
|
|
136
|
+
"skipIssuerMetadataValidation": true
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This disables the issuer check required by RFC 8414 section 3.3. The gateway
|
|
141
|
+
accepts the metadata's OAuth endpoints without checking that the document's
|
|
142
|
+
issuer matches the expected authorization server. Only use it after verifying
|
|
143
|
+
that the metadata and endpoints belong to the provider you intend to use.
|
|
144
|
+
Authorization-response issuer validation and the check that the callback uses
|
|
145
|
+
the same authorization server remain enabled.
|
|
146
|
+
|
|
147
|
+
### Resource Indicator Mismatch
|
|
148
|
+
|
|
149
|
+
`skipResourceIndicatorValidation` allows the `resource` in the upstream's
|
|
150
|
+
protected-resource metadata to differ from the upstream URL configured in the
|
|
151
|
+
route handler's `rewritePattern`.
|
|
152
|
+
|
|
153
|
+
By default, the resource URL must have the same origin (scheme, host, and port)
|
|
154
|
+
as the upstream URL. Its path must also match the upstream path or be a parent
|
|
155
|
+
of it. For example, `https://api.example.com` is valid for an upstream at
|
|
156
|
+
`https://api.example.com/mcp`, but `https://api.example.com/other` is not.
|
|
157
|
+
|
|
158
|
+
A provider might expose its MCP server at `https://mcp.example.com/mcp` but
|
|
159
|
+
advertise `https://api.example.com/mcp` as its resource. If the provider
|
|
160
|
+
confirms that both URLs identify the same service, set the following policy
|
|
161
|
+
options:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"displayName": "Acme",
|
|
166
|
+
"authMode": "user-oauth",
|
|
167
|
+
"skipResourceIndicatorValidation": true
|
|
139
168
|
}
|
|
140
169
|
```
|
|
141
170
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
metadata
|
|
148
|
-
|
|
149
|
-
proved it speaks for that issuer.
|
|
150
|
-
|
|
151
|
-
Only enable it for a specific upstream known to publish a mismatched issuer, and
|
|
152
|
-
only when the metadata URL's host is itself trusted. Never enable it as a
|
|
153
|
-
default. It relaxes the discovery-time check only: the RFC 9207
|
|
154
|
-
authorization-response check and the callback-leg authorization-server binding
|
|
155
|
-
still apply.
|
|
171
|
+
The gateway still sends the advertised `resource` on OAuth authorization and
|
|
172
|
+
token requests. It skips only the comparison with the configured upstream URL.
|
|
173
|
+
If the advertised resource identifies another service, the gateway could receive
|
|
174
|
+
a token intended for that service and forward it to this upstream. Only enable
|
|
175
|
+
this option when you trust the upstream to receive the resulting token. Issuer
|
|
176
|
+
metadata validation remains enabled unless you also set
|
|
177
|
+
`skipIssuerMetadataValidation`.
|
|
156
178
|
|
|
157
179
|
## ID-JAG Token Exchange (Cross-App Access)
|
|
158
180
|
|
|
@@ -174,8 +196,9 @@ Use this mode in enterprise deployments where the gateway and the upstream trust
|
|
|
174
196
|
the same identity provider (for example, Okta Cross-App Access), so upstream
|
|
175
197
|
access follows the caller's identity without a per-user upstream consent flow.
|
|
176
198
|
The `user-oauth`/`shared-oauth` options — `scopes`, `scopeDelimiter`, `prompt`,
|
|
177
|
-
`skipIssuerMetadataValidation`,
|
|
178
|
-
`id-jag` mode is configured entirely
|
|
199
|
+
`skipIssuerMetadataValidation`, `skipResourceIndicatorValidation`, and
|
|
200
|
+
`clientRegistration` — do not apply; the `id-jag` mode is configured entirely
|
|
201
|
+
through the `idJag` option.
|
|
179
202
|
|
|
180
203
|
```json
|
|
181
204
|
{
|
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
{
|
|
60
60
|
"required": ["skipIssuerMetadataValidation"]
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
"required": ["skipResourceIndicatorValidation"]
|
|
64
|
+
},
|
|
62
65
|
{
|
|
63
66
|
"required": ["clientRegistration"]
|
|
64
67
|
}
|
|
@@ -126,7 +129,12 @@
|
|
|
126
129
|
"skipIssuerMetadataValidation": {
|
|
127
130
|
"type": "boolean",
|
|
128
131
|
"x-advanced": true,
|
|
129
|
-
"description": "
|
|
132
|
+
"description": "Allows the authorization server metadata's `issuer` to differ from the server URL used for discovery. Defaults to `false`. Enable only for a confirmed provider metadata mismatch after verifying that its metadata and OAuth endpoints are trusted. This disables the RFC 8414 section 3.3 issuer check; authorization-response and callback checks still apply. Available only with `user-oauth` and `shared-oauth`."
|
|
133
|
+
},
|
|
134
|
+
"skipResourceIndicatorValidation": {
|
|
135
|
+
"type": "boolean",
|
|
136
|
+
"x-advanced": true,
|
|
137
|
+
"description": "Allows the upstream metadata's `resource` URL to have a different origin or path from the configured upstream URL. Defaults to `false`. The gateway still sends the advertised `resource` on OAuth requests. Enable only for a confirmed provider metadata mismatch: a token intended for another service could otherwise be sent to this upstream. Available only with `user-oauth` and `shared-oauth`."
|
|
130
138
|
},
|
|
131
139
|
"clientRegistration": {
|
|
132
140
|
"x-advanced": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "7.8.
|
|
3
|
+
"version": "7.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The official Zuplo CLI for local development and platform management",
|
|
6
6
|
"homepage": "https://zuplo.com/docs/cli/overview",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"zuplo": "zuplo.js"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zuplo/cli": "7.8.
|
|
36
|
-
"@zuplo/core": "7.8.
|
|
37
|
-
"@zuplo/runtime": "7.8.
|
|
38
|
-
"@zuplo/test": "7.8.
|
|
35
|
+
"@zuplo/cli": "7.8.1",
|
|
36
|
+
"@zuplo/core": "7.8.1",
|
|
37
|
+
"@zuplo/runtime": "7.8.1",
|
|
38
|
+
"@zuplo/test": "7.8.1"
|
|
39
39
|
}
|
|
40
40
|
}
|