Downstream Credential Access

When a service holds an inbound Descope access token and needs to call a different downstream API, it usually needs a credential scoped to that target, not the token it already has. That applies to MCP servers fetching third-party OAuth tokens, MCP tools calling internal APIs registered as separate Resources, and any middleware that brokers access on an agent's behalf.

This page covers the server side: validate the inbound token, get an outbound credential from Descope, and call the downstream service with it. For where this fits among the ways agents get tokens, see Auth Patterns. For what's specific to MCP tool handlers and the Python SDK helpers, see Calling External APIs from MCP Tools.

Choose a Path

The inbound token is the access token the agent or MCP client sent your service. Its aud is your Resource, and it carries who is calling and the scopes they consented to. What you send downstream depends on the target:

Downstream targetPatternWhat you send downstream
A Descope Resource that accepts the inbound token's audience and scopesPassthroughThe inbound token as-is
An internal API registered as a different Descope ResourceResource exchangeA Descope JWT from a token exchange at the token endpoint, scoped to that Resource's aud and scopes
A third-party OAuth service or API-key-based serviceConnection fetchThe OAuth token or API key from the Connection token endpoints

Passthrough only works when the downstream service is the same Resource, or accepts the same audience and scopes. For most tool handlers, get a new credential instead. Forwarding the inbound token exposes its full scope set downstream, and it skips the per-target policy check and audit event that each exchange or fetch gets.

Setup

1. Register a Client for Your Server

A Resource exchange needs your server to act as an OAuth client toward Descope, so the same server is both a Resource on the inbound side and a Client on the outbound side. A Connection fetch doesn't need this, because it presents the inbound token itself.

In the Descope Console, go to Clients and create a client to represent the server:

  • Enable the Client Credentials grant type, and disable the grant types the server won't use.
  • Copy the generated Client ID and Client Secret, and keep them in the server's environment or secrets manager.

This client is separate from the agents or MCP clients that call your service. Its client ID tells Descope which server is making the exchange, so policies can target your server with client.tags or client.name, and the audit log records the chain from the user, through the calling client, to your server. Without it, Descope can't tell a legitimate server-side exchange from a caller trying to get credentials directly.

2. Register the Downstream Targets

Downstream targetWhere to configure itWhat Descope returns
Internal API that validates Descope JWTsConnect → Resources as an API ResourceA Resource-scoped Descope JWT
Third-party OAuth or API-key serviceAgentic Identity Hub → ConnectionsThe OAuth token or API key stored in the Connections vault

MCP scopes → Connection scopes

MCP Server Resource scopes are what a user consents to when they authorize your MCP server (for example mcp:read_hubspot_contacts). In the MCP server's scopes config, you map each of those scopes to the downstream Connection scopes it needs (for example crm.objects.contacts.read).

Descope uses that mapping at two points, and you never pick Connection scopes by hand at either:

  • During the connect: when the user links the Connection, Descope requests exactly the Connection scopes that correspond to the MCP server scopes the user consented to, so the token stored in the Connections vault carries the right scopes.
  • At fetch time: when your server fetches the token, Descope reads the scopes on the access token (the same MCP server resource scopes) and automatically returns the stored Connection token with the matching Connection scopes.

You can also map an MCP server scope to an API key Connection, such as OpenAI or a custom API key Connection. An API key has no scopes to request, so the MCP scope maps to the Connection as a whole, and the connect step above doesn't apply.

Which stored key a fetch can return depends on what the key is associated with. An API key can belong to a user, to a user within a specific tenant, or to a tenant, the same three models as OAuth tokens. The access token you fetch with has to match that association:

  • User-level keys: The mapping returns keys stored for the same user as the access token. If that user has more than one key on the Connection, set an externalIdentifier on each key when you store it, and pass the same externalIdentifier when you fetch the token so Descope knows which key to return. Keys the same user holds in different tenants don't need one, since tenantId already tells them apart.
  • User-level keys associated with a tenant: The access token must be for that user signed in to that same tenant. An access token for the same user in a different tenant, or with no tenant, can't get the key, and the fetch fails.
  • Tenant-level keys: The fetch succeeds when the access token's user belongs to the tenant and your policies allow it, or when the access token comes from a client credentials flow for a client associated with that tenant.

MCP Server scopes mapped to Connection scopes in the Descope Console

Configure this mapping in your MCP server configuration underneath the Scopes section, not on the Policies page. See MCP Server Resource scopes, for how scopes map to tools.

3. Allow It with a Policy

The scope mapping decides which Connection scopes a token gets. A policy with the Delegated access (token exchange) grant type decides whether your server may get that credential at all, for both Resource exchanges and Connection fetches. Without a matching policy, the request is denied even when the scope mapping is correct. See A Policy on Every Hop, for how these checks chain across services.

Making the Request

At request time your server validates the inbound access token against Descope's JWKS, then asks Descope for the outbound credential:

Exchanging for a Resource Token

Your server calls the token endpoint as its registered client, with the inbound access token as the subject token and the target Resource's URL as resource:

POST /oauth2/v1/apps/token
Authorization: Basic {base64(client_id:client_secret)}
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token={inbound_access_token}
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&resource={resource_url}

Descope validates your server's credentials, evaluates policy against the inbound token's claims, and returns a Descope JWT scoped to that Resource. See Token Exchange for optional scope narrowing, and the Token API for the full parameter reference.

Fetching a Connection Token

Your server calls the Connection token endpoint with the inbound access token in the Authorization header, naming the Connection and the user:

curl -X POST "__BaseURL__/v1/mgmt/outbound/app/user/token/latest" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID>:<INBOUND_ACCESS_TOKEN>" \
  -d '{
  "appId": "<CONNECTION_ID>",
  "userId": "<USER_ID>"
}'

Descope evaluates policies for the token's subject and returns the stored OAuth token or API key that the subject is allowed to read. Tenant-level tokens use a separate endpoint. Fetching Connection Tokens covers every endpoint and SDK method, and the MCP Auth SDKs make this call for you inside an MCP server.

Audit Trail

Every token exchange produces an audit event that records:

  • The original user identity from the inbound token
  • The calling client that initiated the request
  • Your server's client ID that made the exchange
  • The downstream Resource accessed
  • The policy decision applied

Denied Connection fetches are also logged as policy violations.

Was this helpful?

On this page