# Configuring PingOne

The MCP Gateway can use PingOne as the identity provider behind its downstream
OAuth flow. The `mcp-ping-oauth-inbound` policy is a PingOne-friendly wrapper
around the generic `mcp-oauth-inbound` policy: provide a PingOne environment ID
(or a custom domain), a client ID, and a client secret, and the policy derives
the OIDC issuer, JWKS URL, and authorize and token URLs for you.

This guide walks through the PingOne admin console setup, then wires the policy
into a gateway project. Read the [authentication overview](./overview.mdx) first
for the two-layer OAuth model.

:::note

This policy is for **PingOne cloud**. For **PingFederate** deployments — which
can customize issuer hosts, issuer paths, and endpoint paths — use the generic
[`mcp-oauth-inbound` policy](./configuring-generic-oidc.mdx) instead.

:::

## Set up PingOne

The MCP Gateway acts as an OAuth 2.1 authorization server in front of PingOne.
PingOne handles browser login; the gateway issues its own access tokens that
bind to MCP routes.

### Create an OIDC application

1. In the PingOne admin console, switch to the environment the gateway should
   use, then open **Applications → Applications**.
2. Click **+ Add Application**, name it (for example, `Zuplo MCP Gateway`),
   choose **OIDC Web App** as the application type, and click **Save**.
3. Open the application's **Configuration** tab.
4. Set **Redirect URIs** to `https://<gateway-host>/oauth/callback`. Add
   `http://localhost:9000/oauth/callback` for local development.
5. Set **Grant Types** to **Authorization Code**.
6. Save.

### Note the credentials

Open the application's **Profile** tab. Copy the **Client ID** and **Client
Secret**.

### Find your environment ID and region

Open **Settings → Environment** in the PingOne admin console. Copy the
**Environment ID** (a UUID like `11111111-1111-4111-8111-111111111111`). Note
the **Geography** of the environment — North America, Canada, Europe, Singapore,
Australia, or Asia-Pacific. You'll pass these to the policy.

### Optional: custom domain

If your PingOne environment uses a custom domain (configured under **Settings →
Domains**), copy the bare host (such as `login.example.com`) and use it instead
of `environmentId` + `region`. The wrapper switches to the custom-domain
endpoint shape when `customDomain` is set.

## Wire the policy into the gateway

Add the policy to `config/policies.json`:

```json
{
  "name": "ping-managed-oauth",
  "policyType": "mcp-ping-oauth-inbound",
  "handler": {
    "module": "$import(@zuplo/runtime/mcp-gateway)",
    "export": "McpPingOAuthInboundPolicy",
    "options": {
      "environmentId": "$env(PING_ENVIRONMENT_ID)",
      "region": "north-america",
      "clientId": "$env(PING_CLIENT_ID)",
      "clientSecret": "$env(PING_CLIENT_SECRET)"
    }
  }
}
```

For a custom domain:

```json
{
  "options": {
    "customDomain": "$env(PING_CUSTOM_DOMAIN)",
    "clientId": "$env(PING_CLIENT_ID)",
    "clientSecret": "$env(PING_CLIENT_SECRET)"
  }
}
```

Attach the policy to each MCP route in `config/routes.oas.json` and register the
gateway plugin in `modules/zuplo.runtime.ts` (see
[Configuring Auth0](./configuring-auth0.mdx#wire-the-policy-into-the-gateway)
for the route and plugin patterns — they're identical across all wrappers).

## Available regions

| `region` value  | PingOne auth host            |
| --------------- | ---------------------------- |
| `north-america` | `auth.pingone.com` (default) |
| `canada`        | `auth.pingone.ca`            |
| `europe`        | `auth.pingone.eu`            |
| `singapore`     | `auth.pingone.sg`            |
| `australia`     | `auth.pingone.com.au`        |
| `asia-pacific`  | `auth.pingone.asia`          |

## What the wrapper derives

For the default region (`north-america`) and environment ID `ENV_ID`:

| Generic field           | Derived value                                    |
| ----------------------- | ------------------------------------------------ |
| `oidc.issuer`           | `https://auth.pingone.com/{ENV_ID}/as`           |
| `oidc.jwksUrl`          | `https://auth.pingone.com/{ENV_ID}/as/jwks`      |
| `browserLogin.url`      | `https://auth.pingone.com/{ENV_ID}/as/authorize` |
| `browserLogin.tokenUrl` | `https://auth.pingone.com/{ENV_ID}/as/token`     |

With `customDomain` set, the host changes to your custom domain and the
`{environmentId}` segment is removed.

## Test the configuration

The fastest sanity check is to connect an MCP client:

1. Open Claude Desktop, Cursor, Claude Code, or another OAuth-aware MCP client.
2. Add a remote MCP server pointing at one of your `/mcp/{slug}` routes.
3. The client should redirect you to the PingOne sign-in page. After login, the
   gateway's consent screen renders. Approve it.
4. The client receives an access token and can call `tools/list`.

If something fails partway through, walk the flow manually using the
[manual OAuth testing guide](./manual-oauth-testing.mdx) — it exercises every
endpoint with `curl` so you can see the raw responses.

## Common issues

- **`environmentId` rejected at boot.** The wrapper expects a UUID. Don't pass
  the issuer URL, the auth domain, or the client ID.
- **Browser login lands on a PingOne error page.** The redirect URI on the
  application doesn't match `https://<gateway-host>/oauth/callback`.
- **`invalid_client`.** The application is set to **Public** instead of
  **Confidential**. Confidential is required so the gateway can authenticate
  with the client secret.

## Related

- [Authentication overview](./overview.mdx)
- [Configuring a generic OIDC provider](./configuring-generic-oidc.mdx) — use
  this for PingFederate.
- [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx)
