Fetching Connection Tokens

This section covers how to fetch connection tokens for your users and tenants. This is typically done within an MCP server, but can also be done directly by your agent as well, depending on your architecture.

To retrieve a token for a Connection, you can use either one of our SDKs or our REST API.

To see how you make token requests within your MCP server, see the Authentication for Token Fetching section below.

Authentication for Token Fetching

You can authenticate requests to fetch connection tokens using either:

  • MCP Server access token: Formatted as Bearer <PROJECT_ID>:<ACCESS_TOKEN> (recommended)
  • Management Key: Formatted as Bearer <PROJECT_ID>:<MANAGEMENT_KEY>

Your MCP Server access token must include the outbound.token.fetch scope to be able to fetch connection tokens. This scope must be originally requested by the OAuth client and consented to by the user.

When using an MCP Server access token, Descope enforces your custom access control policies on the token request. This allows you to control which MCP clients/agents can retrieve connection tokens based on roles, permissions, or other conditions defined in your policies.

Management keys, on the other hand, provide full administrative access to all connection tokens across all users and tenants, bypassing access control policies.

Token Fetching Methods

These are all of the methods you can use to fetch connection tokens.

Fetch Latest User Token

This method is recommended when you don't know the exact scopes or want the most recent valid token for the user, regardless of scopes.

// Fetch latest user token
const latestUserToken = await descopeClient.management.outboundApplication.fetchToken(
  'my-app-id',
  'user-id',
  'tenant-id', // optional
  { forceRefresh: false, withRefreshToken: false } // optional
);

Request Parameters

  • appId (required): The ID of the connection.
  • userId (required): The user ID for whom to fetch the token.
  • tenantId (optional): The tenant ID of the user, if a user has multiple tokens associated with different tenants.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Fetch Token with Specific Scopes

Use this method when you need a token with specific scopes. Important: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a 404 Token not found error.

// Fetch user token with specific scopes
const userToken = await descopeClient.management.outboundApplication.fetchTokenByScopes(
  'my-app-id',
  'user-id',
  ['read', 'write'],
  { withRefreshToken: false }, // optional
  'tenant-id' // optional
);

Request Parameters

  • appId (required): The ID of the connection.
  • userId (required): The user ID for whom to fetch the token.
  • tenantId (optional): The tenant ID of the user, if a user has multiple tokens associated with different tenants.
  • scopes (required): An array of exact scopes that match the original token.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Fetching Tenant-Level Tokens

In addition to user-specific tokens, you can also fetch tenant-level tokens for connections. These are useful when you need to access APIs on behalf of a tenant rather than a specific user.

Descope provides the same two methods for tenant-level connection tokens as user-level tokens. Depending on whether you know the exact scopes of the token you need, you can use the following methods:

Fetch Latest Tenant Token

This method is recommended when you don't know the exact scopes or want the most recent valid token for the tenant, regardless of scopes.

// Fetch latest tenant token
const latestTenantToken = await descopeClient.management.outboundApplication.fetchTenantToken(
  'my-app-id',
  'tenant-id',
  { forceRefresh: false } // optional
);

Request Parameters

  • appId (required): The ID of the connection.
  • tenantId (required): The tenant ID if you're fetching a tenant-level token.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Fetch Tenant Token with Specific Scopes

Use this method when you need a tenant token with specific scopes. Important: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a 404 Token not found error.

// Fetch tenant token with specific scopes
const tenantToken = await descopeClient.management.outboundApplication.fetchTenantTokenByScopes(
  'my-app-id',
  'tenant-id',
  ['read', 'write'],
  { withRefreshToken: false } // optional
);

Request Parameters

  • appId (required): The ID of the connection.
  • tenantId (required): The tenant ID if you're fetching a tenant-level token.
  • scopes (required): An array of exact scopes that match the original token.
  • options (optional): Additional options for token fetching.
    • withRefreshToken: Defaults to false. Set this to true to include the refresh token in the response.
    • forceRefresh: Defaults to false. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf.

Connection Token Response

The refresh_token will not be returned unless withRefreshToken is set to true in the request.

The response will include the user/tenant token details, similar to the example below:

{
  "token": {
    "id": "xxxx",
    "appId": "google-contacts",
    "userId": "xxxx",
    "tokenSub": "",
    "accessToken": "ya29.xxxx",
    "accessTokenType": "Bearer",
    "accessTokenExpiry": "1741107113",
    "hasRefreshToken": true,
    "refreshToken": "xxxx",
    "lastRefreshTime": "1741103514",
    "lastRefreshError": "",
    "scopes": [
      "https://www.googleapis.com/auth/contacts.readonly"
    ]
  }
}

Using Tokens within your MCP Tools

Once you have the access token from a connection, you can use it to make authenticated requests to the third-party provider's API within your MCP tools.

Example: Google Contacts MCP Tool

The following example shows how an MCP tool would fetch a user's contacts using the Google Contacts API with a token obtained from a connection. This assumes you've already validated the MCP client's access token and extracted the user ID.

import json
import requests
from descope import DescopeClient
 
# Initialize Descope client (using MCP Server access token for policy enforcement)
descope_client = DescopeClient(project_id="YOUR_PROJECT_ID")
 
# In your MCP tool handler, after validating the MCP client's access token:
# 1. Extract user_id from the validated MCP access token
user_id = validated_token.get("sub")  # User ID from the MCP access token
 
# 2. Fetch the connection token for Google Contacts
# This will enforce your access control policies
connection_token_response = descope_client.mgmt.outbound_application.fetch_token(
    app_id="google-contacts",  # Your Connection ID
    user_id=user_id,
    options={
        "withRefreshToken": False
    }
)
 
access_token = connection_token_response["token"]["token"]
 
# 3. Use the connection token to call the Google Contacts API
request_url = "https://people.googleapis.com/v1/people/me/connections"
params = {
    'personFields': 'names,emailAddresses',  # Specify valid fields
    'pageSize': 100  # Adjust as needed
}
headers = {"Authorization": f"Bearer {access_token}"}
 
response = requests.get(request_url, headers=headers, params=params)
 
# Check the response status code
if response.status_code == 200:
    try:
        # Parse the JSON response
        response_data = response.json()
        if 'connections' in response_data and response_data['connections']:
            # Return contacts to the MCP client
            contacts = response_data['connections']
            return {
                "contacts": [
                    {
                        "name": contact.get("names", [{}])[0].get("displayName", ""),
                        "email": contact.get("emailAddresses", [{}])[0].get("value", "")
                    }
                    for contact in contacts
                ]
            }
        else:
            return {"contacts": []}
    except requests.exceptions.JSONDecodeError:
        raise Exception(f"Error decoding JSON: {response.text}")
else:
    raise Exception(f"Google API error: {response.status_code} - {response.text}")

Key Points

  1. Token Fetching: The MCP tool fetches the connection token using the user ID from the validated MCP access token. This ensures that access control policies are enforced.

  2. Policy Enforcement: When using an MCP Server access token (instead of a Management Key), Descope evaluates your access control policies before returning the connection token. This means only authorized users / clients can retrieve tokens for specific connections.

  3. Token Usage: The connection token is then used to authenticate requests to the third-party API (Google Contacts in this example).

  4. Error Handling: Always handle API errors appropriately and return meaningful responses to the MCP client.

For more detailed examples and AI agent implementations, see our Examples Guide.

Error Handling

When working with connection tokens, you may encounter different types of errors. Here's what each error code means and how to handle them:

Common Error Codes

Status CodeMeaningCommon Causes
401UnauthorizedInvalid management key or project ID
403ForbiddenInsufficient permissions or invalid tenant access
404Token not foundUser never connected to the connection, token was cleared, or wrong scopes provided
500Server errorInvalid HTTP method (not POST) or malformed JSON payload

Error Handling Example

import requests
from requests.exceptions import RequestException
 
def fetch_connection_token(app_id, user_id, scopes=None):
    """Fetch a connection token with proper error handling."""
    headers = {"Authorization": f"Bearer {PROJECT_ID}:{MANAGEMENT_KEY}"}
    
    try:
        if scopes:
            # Use specific scopes endpoint
            url = "https://api.descope.com/v1/mgmt/outbound/app/user/token"
            data = {"appId": app_id, "userId": user_id, "scopes": scopes}
        else:
            # Use latest token endpoint
            url = "https://api.descope.com/v1/mgmt/outbound/app/user/token/latest"
            data = {"appId": app_id, "userId": user_id}
        
        response = requests.post(url, headers=headers, json=data, timeout=30)
        response.raise_for_status()
        return response.json()
        
    except requests.exceptions.HTTPError as e:
        if response.status_code == 404:
            # Token not found - either never existed or was cleared recently
            print("Token not found. The user may not have connected to this connection, "
                  "or the token may have been cleared.")
            return None
        elif response.status_code == 500:
            # Server error - issue with HTTP request method or JSON payload
            print("Server error. Check your request method (should be POST) "
                  "and ensure your JSON payload is properly formatted.")
            return None
        else:
            print(f"HTTP error occurred: {e}")
            return None
    except requests.exceptions.Timeout:
        print("Request timed out")
        return None
    except RequestException as e:
        print(f"Request failed: {e}")
        return None
 
def make_api_request(access_token, url, params=None):
    """Make a request to a third-party API with proper error handling."""
    headers = {"Authorization": f"Bearer {access_token}"}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as e:
        if response.status_code == 401:
            print("Access token may be invalid or expired")
        elif response.status_code == 403:
            print("Insufficient permissions for this request")
        else:
            print(f"HTTP error occurred: {e}")
        return None
    except requests.exceptions.Timeout:
        print("Request timed out")
        return None
    except RequestException as e:
        print(f"Request failed: {e}")
        return None

Viewing and Managing Tokens in the Console

After connecting users to a connection, you can view and manage their tokens directly in the Descope Console under the Token Management tab for your connection.

Token Management dashboard in the Descope Console

For each user or tenant-level token, you can:

  • View the access token (and refresh token, if applicable)
  • Manually refresh the access token
  • Delete the token

Note

You can delete your pre-existing tokens programtically as well with these functions.

This provides a convenient way to audit, troubleshoot, or revoke access for specific users or tenants without writing any code.

Was this helpful?