Fetching Connection Tokens

Fetching a connection token is how your code pulls the actual OAuth token or API key out of the vault so it can call a third-party API. Most of the time this happens inside an MCP server, though your agent can also fetch tokens directly if that suits your architecture better.

You fetch tokens with an SDK or the REST API. Before you pick a method, decide how you will authenticate the request. That choice controls whether your policies apply and which vaulted tokens you are allowed to read, so it is worth settling first. The next section covers the two options.

Authentication for Token Fetching

Every fetch request is authenticated as Bearer <PROJECT_ID>:<CREDENTIAL>, where the credential is either an access token or a Management Key:

  • Access token: Bearer <PROJECT_ID>:<ACCESS_TOKEN>
  • Management Key: Bearer <PROJECT_ID>:<MANAGEMENT_KEY>

The one you use determines both which tokens you can read and whether policies are evaluated.

Access Token

Reach for an access token whenever an agent or MCP server is acting as a real identity, whether that is a user who granted consent or an autonomous agent that belongs to a tenant. Descope runs your policies on every fetch, so you can control which clients, agents, roles, and tenants are allowed to read which connection credentials. This is the right choice in most cases.

Which access token you use depends on the agent pattern:

Agent patternCredential to useWhat it can fetch
Delegated user agent (user granted consent)The user access token the agent or MCP client already holdsThat user's connection tokens, whether user-level or user-level associated with a tenant
MCP server toolThe MCP server access token from the incoming requestWhatever connection tokens policy allows for that subject. Usually you exchange the MCP access token for the specific token the tool needs
Autonomous agent (client_credentials)The agent's own access tokenTenant-level connection tokens only, and only for tenants the agent belongs to

For a delegated or user-backed agent, you almost always exchange the user access token, or the MCP server access token derived from that session, for a vaulted connection token rather than using a Management Key. Inside an MCP server, validate the incoming MCP access token first, then use it to fetch the connection token the tool needs. Your policies decide whether that fetch is allowed.

An autonomous agent signed in with client credentials has to be associated with specific tenants, and it can only fetch tenant-level tokens for those tenants. It cannot read user-level tokens, including user-level tokens associated with a tenant. To act on a specific user's vaulted OAuth token, you need that user's access token, or a Management Key.

Both variants follow the same shape. The caller presents its own access token, Descope checks the policies for that identity, and returns only the connection tokens that identity is allowed to read.

This is the on-behalf-of model: the agent acts as itself or as a consenting user, and it can never reach a credential your rules do not permit for that identity. How those identities are defined is covered in Agents and Clients and workloads, the rules themselves live in Policies, and the token exchange an MCP server performs to turn an inbound token into a connection token is detailed in Downstream Credential Access.

Management Key

A Management Key is for cases where a privileged backend needs to read connection tokens outside of any user or agent consent path. A common example is a support agent that has to fetch a user's or tenant admin's vaulted token so it can operate as that user or tenant, similar to impersonation.

A Management Key bypasses policies and grants full administrative access, so use it deliberately:

Access tokenManagement Key
Policies enforcedYesNo, policies are bypassed
Typical useDelegated agents, MCP tools, and autonomous agents fetching tenant-level keysSupport or admin tooling that needs to fetch arbitrary user or tenant tokens
Scope of accessLimited by the token subject and your policiesFull access to connection tokens across all users and tenants

Use an access token wherever you can, and turn to a Management Key only when you specifically need privileged access that skips policy checks.

The difference from the access-token model is who chooses the user. There is no user session and no consent in the request. Your backend decides which user to act as, passes that user's ID to the fetch, and Descope returns that user's vaulted token without checking policies. The agent then operates as that user rather than on behalf of them.

Because this bypasses the policy checks that normally protect vaulted credentials, keep it on trusted backends only. If the agent should instead act with a user present and consenting, use the access-token model above, fetching the token directly from the agent, and see Auth patterns for how these fit together across the Agentic Identity Hub.

Fetching with the SDKs

Which SDK you use depends on whether an MCP server sits in front of the connection. Inside an MCP server, use the MCP Auth SDK for your language; it validates the incoming request and hands your tool a connection token. When an agent talks to services directly with no MCP server in between, use the Agent Auth SDK. Both enforce your policies on the fetch. The management SDK and REST methods below still work and remain the right tool for privileged or backend fetches.

Inside an MCP Server

Protect the MCP server with the MCP Auth SDKs, which validate the incoming access token and expose a helper to fetch a connection token from inside a tool handler. Policies are enforced when you pass the request's access token. The example below fetches a user's Google contacts:

import requests
from descope_mcp import get_connection_token

def get_contacts(user_id, mcp_access_token):
    # Fetch the Google connection token; passing the MCP access token enforces policies
    token = get_connection_token(
        user_id=user_id,
        app_id="google-contacts",
        access_token=mcp_access_token,
    )

    # Call the Google People API with the fresh, scoped token
    response = requests.get(
        "https://people.googleapis.com/v1/people/me/connections",
        headers={"Authorization": f"Bearer {token}"},
        params={"personFields": "names,emailAddresses", "pageSize": 100},
    )
    response.raise_for_status()
    connections = response.json().get("connections", [])
    return {
        "contacts": [
            {
                "name": c.get("names", [{}])[0].get("displayName", ""),
                "email": c.get("emailAddresses", [{}])[0].get("value", ""),
            }
            for c in connections
        ]
    }
import { defineTool } from '@descope/mcp-express';
import { z } from 'zod';

// The SDK validates the incoming token; extra.getOutboundToken fetches from Connections
const getContacts = defineTool({
  name: 'get_contacts',
  description: "Fetch the user's Google contacts",
  input: {},
  handler: async (_args, extra) => {
    const token = await extra.getOutboundToken('google-contacts');

    const res = await fetch(
      'https://people.googleapis.com/v1/people/me/connections?personFields=names,emailAddresses&pageSize=100',
      { headers: { Authorization: `Bearer ${token}` } },
    );
    if (!res.ok) throw new Error(`Google API error: ${res.status}`);
    const { connections = [] } = await res.json();

    const contacts = connections.map((c) => ({
      name: c.names?.[0]?.displayName ?? '',
      email: c.emailAddresses?.[0]?.value ?? '',
    }));
    return { content: [{ type: 'text', text: JSON.stringify({ contacts }) }] };
  },
});

Directly from an Agent

When an agent connects to services directly, with no MCP server in between, use the Agent Auth SDK (Python and TypeScript). It signs the agent in to Descope and fetches the tokens its tools need. You bind it to the user's JWT from your app's login, call connections.get_token, and it returns a fresh, policy-checked token. If the user has not linked the account yet, it raises ConnectionAuthorizationRequired, which carries a connect URL to send them through.

from descope_agent_auth import AgentAuthClient, AccessTokenProvider
from descope_agent_auth.errors import ConnectionAuthorizationRequired

client = AgentAuthClient(
    project_id="YOUR_PROJECT_ID",
    credential=AccessTokenProvider(access_token=user_jwt),  # the JWT from your app's login
)

try:
    google = client.connections.get_token(connection="google-contacts", identifier=user_id)
    use(google.access_token)              # a fresh, scoped Google token
except ConnectionAuthorizationRequired as e:
    redirect_user_to(e.connect_url)       # the user hasn't linked Google yet
import { AgentAuthClient, AccessTokenProvider, ConnectionAuthorizationRequired } from '@descope/agent-auth';

const client = new AgentAuthClient({
  projectId: 'YOUR_PROJECT_ID',
  credential: new AccessTokenProvider({ accessToken: userJwt }), // the JWT from your app's login
});

try {
  const google = await client.connections.getToken({ connection: 'google-contacts', identifier: userId });
  use(google.accessToken);                // a fresh, scoped Google token
} catch (e) {
  if (e instanceof ConnectionAuthorizationRequired) {
    redirectUserTo(e.connectUrl);         // the user hasn't linked Google yet
  } else {
    throw e;
  }
}

Direct access fits when your app already authenticates users and can hand the agent a user JWT, the agent runs in a backend you control, and you want the shortest path from a request to a provider call. Be aware that it hands the agent the provider credential itself, and those tokens and API keys are often long-lived. For most cases it is better to put a Resource, commonly an MCP server, between the agent and the vault: the agent holds only a short-lived Descope token, and the exchange for the Connection token happens inside the Resource, so the provider credential never reaches the agent. See Keep credentials off the agent.

Key Points

  1. Prefer an access token: Passing the validated MCP or user access token enforces policies on every fetch. A Management Key bypasses policies and should be reserved for privileged support-style access.
  2. Subject drives what you can fetch: A delegated user token can fetch that user's Connection tokens. An autonomous (client_credentials) agent can only fetch tenant-level tokens for tenants it is associated with, not user-level tokens.
  3. Handle the unconnected case: When the user has not linked the account, the Agent Auth SDK raises ConnectionAuthorizationRequired with a connect URL. Send the user through it, then retry.
  4. Do not cache tokens: Fetch whenever a tool runs. Descope refreshes vaulted tokens, so a fresh fetch always returns a valid credential.

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

Token Fetching Methods

Note

For Python-based MCP servers, use the Python MCP SDK which provides a simplified interface for fetching connection tokens with automatic token validation and policy enforcement.

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

Do not pass a tenant ID for a plain user-level token

The tenantId parameter on the user token endpoints is only for user-level tokens associated with a tenant. If you pass a tenantId but the user's token has no tenant association, the fetch returns 404 Token not found. To fetch a plain user-level token, omit tenantId entirely.

True tenant-level tokens, such as the ones you add directly in the Console, are shared by a whole tenant and are fetched from a separate endpoint, not these user token endpoints.

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
);
# Fetch latest user token
latest_user_token = descope_client.mgmt.outbound_application.fetch_token(
    app_id='my-app-id',
    user_id='user-id',
    tenant_id='tenant-id',  # optional
    options={
        'forceRefresh': False,  # optional
        'withRefreshToken': False  # optional
    }
)
import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;

// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();

// Fetch latest token
FetchOutboundAppUserTokenRequest request = new FetchOutboundAppUserTokenRequest();
request.setAppId("google-contacts");
request.setUserId("user-123");
request.setTenantId("tenant-id"); // optional

FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchOutboundAppUserTokenLatest(request);
String accessToken = response.getToken().getAccessToken();
// Note: Go SDK only supports fetching tokens by specific scopes, not latest tokens
// See "Fetch Token with Specific Scopes" section below
curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/user/token/latest" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY> or <PROJECT_ID:ACCESS_TOKEN>" \
  -d '{
  "appId": "google-contacts",
  "userId": "xxxxx",
  "tenantId": "optional-tenant-id",
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

Request Parameters

  • appId (required): The ID of the connection.
  • userId (required): The user ID for whom to fetch the token.
  • tenantId (optional): Only for a user-level token that was stored with a tenant association, when a user has separate tokens across different tenants. Passing it for a token that has no tenant association returns 404 Token not found, so omit it for a plain user-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 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
);
# Fetch user token with specific scopes
user_token = descope_client.mgmt.outbound_application.fetch_token_by_scopes(
    app_id='my-app-id',
    user_id='user-id',
    scopes=['read', 'write'],
    options={
        'withRefreshToken': False  # optional
    },
    tenant_id='tenant-id'  # optional
)
import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;

// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();

// Fetch token with specific scopes
FetchOutboundAppUserTokenRequest request = new FetchOutboundAppUserTokenRequest();
request.setAppId("google-contacts");
request.setUserId("user-123");
request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly"));
request.setTenantId("tenant-id"); // optional

FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchOutboundAppUserToken(request);
String accessToken = response.getToken().getAccessToken();
// Fetch user token with specific scopes
request := &descope.FetchOutboundAppUserTokenRequest{
    AppID:    "my-app-id",
    UserID:   "user-id",
    Scopes:   []string{"read", "write"},
    TenantID: "tenant-id", // optional
    Options: &descope.OutboundAppUserTokenOptions{
        WithRefreshToken: false, // optional
    },
}

token, err := descopeClient.Management.OutboundApplication().FetchUserToken(ctx, request)
if err != nil {
    // Handle error
}
accessToken := token.AccessToken
curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/user/token" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY> or <PROJECT_ID:ACCESS_TOKEN>" \
  -d '{
  "appId": "google-contacts",
  "userId": "xxxxx",
  "tenantId": "optional-tenant-id",
  "scopes": [
    "https://www.googleapis.com/auth/contacts.readonly"
  ],
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

Request Parameters

  • appId (required): The ID of the connection.
  • userId (required): The user ID for whom to fetch the token.
  • tenantId (optional): Only for a user-level token that was stored with a tenant association, when a user has separate tokens across different tenants. Passing it for a token that has no tenant association returns 404 Token not found, so omit it for a plain user-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.

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.

These are true tenant-level tokens, shared across the tenant's users, for example the ones you add directly in the Console. They are fetched from the dedicated endpoints below. Do not confuse them with user-level tokens associated with a tenant, which are still fetched from the user token endpoints above.

Autonomous agents

An autonomous agent authenticated with client_credentials can fetch tenant-level tokens only, and only for tenants it is associated with. It cannot fetch user-level Connection tokens. See Authentication for Token Fetching.

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
);
# Fetch latest tenant token
latest_tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token(
    app_id='my-app-id',
    tenant_id='tenant-id',
    options={
        'forceRefresh': False,  # optional
        'withRefreshToken': False  # optional
    }
)
import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;

// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();

// Fetch latest tenant token
FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest();
request.setAppId("google-contacts");
request.setTenantId("tenant-123");

FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchOutboundAppTenantTokenLatest(request);
String accessToken = response.getToken().getAccessToken();
// Note: Go SDK does not currently support tenant-level token fetching
// Use the REST API directly for tenant-level operations
curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/tenant/token/latest" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY> or <PROJECT_ID:ACCESS_TOKEN>" \
  -d '{
  "appId": "google-contacts",
  "tenantId": "tenant-123",
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

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
);
# Fetch tenant token with specific scopes
tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token_by_scopes(
    app_id='my-app-id',
    tenant_id='tenant-id',
    scopes=['read', 'write'],
    options={
        'withRefreshToken': False  # optional
    }
)
import com.descope.DescopeClient;
import com.descope.sdk.mgmt.OutboundAppsService;

// Initialize the Descope client
DescopeClient descopeClient = new DescopeClient("YOUR_PROJECT_ID", "YOUR_MANAGEMENT_KEY");
OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService();

// Fetch tenant token with specific scopes
FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest();
request.setAppId("google-contacts");
request.setTenantId("tenant-123");
request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly"));

FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchOutboundAppTenantToken(request);
String accessToken = response.getToken().getAccessToken();
// Note: Go SDK does not currently support tenant-level token fetching
// Use the REST API directly for tenant-level operations
curl -X POST "https://api.descope.com/v1/mgmt/outbound/app/tenant/token" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROJECT_ID:MANAGEMENT_KEY> or <PROJECT_ID:ACCESS_TOKEN>" \
  -d '{
  "appId": "google-contacts",
  "tenantId": "tenant-123",
  "scopes": [
    "https://www.googleapis.com/auth/contacts.readonly"
  ],
  "options": {
    "withRefreshToken": false,
    "forceRefresh": false
  }
}'

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"
    ]
  }
}

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, access token, or project ID
403ForbiddenPolicy denied the fetch, insufficient permissions, or invalid tenant access for an autonomous agent
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

// Fetch a connection token with proper error handling.
async function fetchConnectionToken(appId, userId, scopes = null) {
  const url = scopes
    ? "https://api.descope.com/v1/mgmt/outbound/app/user/token"          // specific scopes
    : "https://api.descope.com/v1/mgmt/outbound/app/user/token/latest";  // latest token
  const body = scopes ? { appId, userId, scopes } : { appId, userId };

  try {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${PROJECT_ID}:${MANAGEMENT_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      if (response.status === 404) {
        // Token not found: either never existed or was cleared recently
        console.log("Token not found. The user may not have connected to this connection, or the token may have been cleared.");
      } else if (response.status === 500) {
        // Server error: issue with HTTP request method or JSON payload
        console.log("Server error. Check your request method (should be POST) and ensure your JSON payload is properly formatted.");
      } else {
        console.log(`HTTP error occurred: ${response.status}`);
      }
      return null;
    }
    return await response.json();
  } catch (e) {
    console.log(`Request failed: ${e.message}`);
    return null;
  }
}

// Make a request to a third-party API with proper error handling.
async function makeApiRequest(accessToken, url, params = {}) {
  const query = new URLSearchParams(params).toString();

  try {
    const response = await fetch(query ? `${url}?${query}` : url, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    if (!response.ok) {
      if (response.status === 401) console.log("Access token may be invalid or expired");
      else if (response.status === 403) console.log("Insufficient permissions for this request");
      else console.log(`HTTP error occurred: ${response.status}`);
      return null;
    }
    return await response.json();
  } catch (e) {
    console.log(`Request failed: ${e.message}`);
    return null;
  }
}
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
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

var httpClient = &http.Client{Timeout: 30 * time.Second}

// fetchConnectionToken fetches a connection token with proper error handling.
func fetchConnectionToken(appID, userID string, scopes []string) (map[string]interface{}, error) {
	endpoint := "https://api.descope.com/v1/mgmt/outbound/app/user/token/latest" // latest token
	payload := map[string]interface{}{"appId": appID, "userId": userID}
	if len(scopes) > 0 {
		endpoint = "https://api.descope.com/v1/mgmt/outbound/app/user/token" // specific scopes
		payload["scopes"] = scopes
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer(body))
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s:%s", projectID, managementKey))
	req.Header.Set("Content-Type", "application/json")

	resp, err := httpClient.Do(req)
	if err != nil {
		fmt.Println("Request failed:", err)
		return nil, err
	}
	defer resp.Body.Close()

	switch resp.StatusCode {
	case http.StatusOK:
		var result map[string]interface{}
		json.NewDecoder(resp.Body).Decode(&result)
		return result, nil
	case http.StatusNotFound:
		// Token not found: either never existed or was cleared recently
		fmt.Println("Token not found. The user may not have connected to this connection, or the token may have been cleared.")
	case http.StatusInternalServerError:
		// Server error: issue with HTTP request method or JSON payload
		fmt.Println("Server error. Check your request method (should be POST) and ensure your JSON payload is properly formatted.")
	default:
		fmt.Printf("HTTP error occurred: %d\n", resp.StatusCode)
	}
	return nil, nil
}

// makeAPIRequest calls a third-party API with proper error handling.
func makeAPIRequest(accessToken, endpoint string, params map[string]string) (map[string]interface{}, error) {
	q := url.Values{}
	for k, v := range params {
		q.Set(k, v)
	}
	full := endpoint
	if len(q) > 0 {
		full = endpoint + "?" + q.Encode()
	}

	req, _ := http.NewRequest("GET", full, nil)
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := httpClient.Do(req)
	if err != nil {
		fmt.Println("Request failed:", err)
		return nil, err
	}
	defer resp.Body.Close()

	switch resp.StatusCode {
	case http.StatusOK:
		var result map[string]interface{}
		json.NewDecoder(resp.Body).Decode(&result)
		return result, nil
	case http.StatusUnauthorized:
		fmt.Println("Access token may be invalid or expired")
	case http.StatusForbidden:
		fmt.Println("Insufficient permissions for this request")
	default:
		fmt.Printf("HTTP error occurred: %d\n", resp.StatusCode)
	}
	return nil, nil
}

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 programatically 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?

On this page