Authorization Server Endpoints

When you configure an Inbound App, Descope acts as the OAuth 2.0 / OpenID Connect authorization server for that client.

Third-party applications, AI agents, and MCP clients call a shared set of HTTP endpoints under /oauth2/v1/apps/ to start user login, exchange codes for tokens, refresh sessions, and read token claims.

Each Inbound App also exposes a Discovery URL and Issuer in the Descope Console (see Creating Inbound Apps). Standard OAuth libraries use those values to resolve the same endpoints listed below automatically.

If you use a custom domain, replace https://api.descope.com with your configured API hostname in every URL on this page.

Base URL

EnvironmentBase URL
Descope (default)https://api.descope.com
Custom domainhttps://<your-api-host>

All authorization server routes are rooted at:

{baseUrl}/oauth2/v1/apps/...

Endpoint Reference

EndpointMethodPurposeAPI reference
/oauth2/v1/apps/authorizeGETStart the authorization code flow (browser redirect)Get authorization
/oauth2/v1/apps/authorizePOSTStart authorization with a JSON body (non-browser clients)Post authorization
/oauth2/v1/apps/parPOSTPush authorization parameters from your server before redirecting (PAR)Inbound App PAR
/oauth2/v1/apps/tokenPOSTIssue and refresh tokens; client credentials; JWT bearer; token exchangeToken endpoint
/oauth2/v1/apps/revokePOSTRevoke access or refresh tokensRevoke token
/oauth2/v1/apps/userinfoGETReturn claims for the bearer access tokenGet UserInfo
/oauth2/v1/apps/userinfoPOSTReturn claims (POST variant for clients that require it)Post UserInfo
/oauth2/v1/apps/introspectPOSTCheck whether a token is active and read current claims (RFC 7662)Token introspection

Discovery and JWKs

Inbound Apps are OpenID Connect-compatible. For each app, the Console provides:

  • Discovery URL: OpenID Provider metadata, including authorization_endpoint, token_endpoint, jwks_uri, supported scopes, and grant types
  • Issuer: The value to check iss against on ID tokens and access tokens

The project-level issuer uses the same base URL as the endpoints, so it changes with your region or custom domain:

__BaseURL__/v1/apps/{projectId}

Imported client IDs

If you've imported apps with custom client IDs, the issuer is __BaseURL__/v1/apps/customized/{projectId} instead. See Issuer After Importing Client IDs for more details.

Project-level well-known documents are also available when you need metadata scoped to the whole project:

__BaseURL__/v1/apps/{projectId}/.well-known/openid-configuration
__BaseURL__/v1/apps/{projectId}/.well-known/oauth-authorization-server

MCP Server Resources

MCP servers are defined as MCP Server Resources: the same OAuth resource model as API Resources. Descope uses one authorization server for all Inbound Apps; to authenticate against a specific Resource (including an MCP server), include the resource parameter (RFC 8707) on the authorize or token request. Use the Resource identifier from the console, typically your MCP server URL, which also appears in the token aud claim.

MCP clients discover that Resource through OAuth Protected Resource Metadata on your server; see MCP discovery URL for the client-side discovery flow.

Token Endpoint

The token endpoint is the central exchange point for Inbound Apps. Send application/x-www-form-urlencoded (or JSON, per the API schema) with a grant_type and the parameters required for that grant.

Grant typeTypical useGuide
authorization_codeUser-delegated access after consentAuthorization code
refresh_tokenRenew an access token without re-consentRefresh token
client_credentialsMachine-to-machine Inbound App tokensClient credentials
urn:ietf:params:oauth:grant-type:jwt-bearerExchange a trusted external JWT for Descope tokensJWT bearer
urn:ietf:params:oauth:grant-type:token-exchangeTrade a subject token the caller already holds for one scoped to a specific downstream Resource, governed by Policies (RFC 8693)Token exchange

Common optional parameters on the token endpoint include:

  • scope: Requested OAuth scopes (must be allowed on the Inbound App and, for user flows, approved in consent).
  • resource: RFC 8707 resource indicator (single Resource URI per request). Required when issuing tokens for a specific API or MCP Server Resource; pass the same value on authorize and token requests.
  • audience: Target audience for issued tokens or exchanges.

Example (authorization code exchange):

curl -X POST "__BaseURL__/oauth2/v1/apps/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "client_id=<CLIENT_ID>" \
  -d "client_secret=<CLIENT_SECRET>" \
  -d "code=<AUTHORIZATION_CODE>" \
  -d "redirect_uri=https://yourapp.com/callback"

See the Token endpoint API reference for the full request and response schema.

A client can also send a DPoP header with any token request to get an access token bound to a key it holds, so a stolen token can't be used without that key. See DPoP Sender-Constrained Tokens.

Client authentication

Confidential clients have to prove who they are on every token request. Descope supports three methods, which you choose under Authentication Method on the app.

Client secret

The client sends client_id and client_secret, as in the example above. Client secret is the default method and needs no extra setup. An app can have more than one active secret, which lets you rotate a secret without downtime.

Private key JWT

With private key JWT (RFC 7523), the client signs a short-lived JWT with its own private key and sends it as a client_assertion, instead of a shared secret. Descope validates the signature against the app's JWKs URL or uploaded public key, so no shared secret is ever transmitted or stored on the Descope side. Private key JWT is available on every project. You turn it on per app under Authentication Method.

Prefer private key JWT when you would rather not distribute a long-lived shared secret, or when a partner or compliance requirement rules one out. Profiles such as SMART Backend Services require it. Private key JWT is also the stronger option for Cross-App Access, where the client handles assertions that can be redeemed for access tokens elsewhere.

Client authentication happens before Descope handles the grant, so the assertion works with every grant at the token endpoint. A backend service with no user, for example, sends it with client_credentials:

curl -X POST "__BaseURL__/oauth2/v1/apps/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=<CLIENT_ID>" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=<SIGNED_JWT>" \
  -d "scope=patient/*.read"

For the authorization code grant, the client sends the same two client_assertion parameters in place of client_secret, together with code and redirect_uri.

Once an app uses private key JWT, it accepts only signed assertions. A request that sends the client secret instead fails with invalid_client.

Assertion claims

Descope rejects the assertion unless its claims meet these rules:

  • iss and sub: Both claims must equal the app's client ID, because the assertion speaks only for the client itself.
  • aud: The audience must name your project. Descope accepts the project's issuer, __BaseURL__/v1/apps/__ProjectID__, or the project's token endpoint, __BaseURL__/oauth2/v1/apps/__ProjectID__/token. The PAR endpoint is also accepted. The shorter __BaseURL__/oauth2/v1/apps/token URL is not a valid audience, even though the client can send the request to it, because it doesn't name a project.
  • exp: The assertion must carry an exp no more than five minutes in the future.
  • jti: Every assertion needs a unique jti. Descope rejects a jti that it has already accepted, so a captured assertion can't be replayed.

The assertion must be signed with one of RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, or EdDSA. When a key on the app has a Signing Algorithm set, Descope accepts only that algorithm for that key. When the field is empty, Descope accepts any algorithm on the list that fits the key.

Keys and rotation

You can add more than one key to an app. Descope checks the assertion against each key in turn and accepts it when one of them verifies the signature. To rotate an uploaded public key, you can add the new key, move your client to it, and then remove the old one.

When a key comes from a JWKs URL, Descope picks the signing key by the assertion's kid header. It falls back to the x5t#S256 or x5t thumbprint, and then to the only key in a single-key set. When your JWKS holds more than one key, every assertion must name its key with kid.

Descope caches each JWKS and fetches it again every five minutes. The interval is fixed, so Descope ignores Cache-Control headers on the JWKS response. When an assertion names a kid that isn't in the cached set, Descope fetches the JWKS again right away, at most once every ten seconds. You can rotate keys without downtime by publishing the new key in your JWKS before you start signing with it.

Configuring with the Management API

You can set private key JWT in the Console, or with the Management API. In the API, set clientAuthMethod to privateKeyJwt and list the keys in clientAssertions:

{
  "clientAuthMethod": "privateKeyJwt",
  "clientAssertions": [
    {
      "keySource": "jwks",
      "jwksUri": "https://partner.example.com/.well-known/jwks.json",
      "signAlgorithm": "RS384"
    }
  ]
}

Each entry uses keySource: "jwks" with a jwksUri, or keySource: "publicKey" with a PEM public key or certificate in publicKey. The signAlgorithm field is optional. In Terraform, you set them with client_auth_method and client_assertions on the descope_inbound_app resource.

private_key_jwt in the discovery document

For backward compatibility, the discovery document doesn't list private_key_jwt in token_endpoint_auth_methods_supported by default. Private key JWT still works for every app that uses it. If your clients choose their authentication method from discovery metadata, contact Descope Support to have private_key_jwt advertised for your project.

Workload Identity Federation

The client sends a token that its platform issued to it, such as a SPIFFE JWT-SVID or an OIDC token from a CI provider or Kubernetes, in client_assertion. Descope validates the token against the federation configured on the app and checks its subject against the app's Allowed Subjects. The workload has no Descope credential to store or rotate.

The client_assertion_type tells Descope which kind of federation to match:

  • SPIFFE: Send urn:ietf:params:oauth:client-assertion-type:jwt-spiffe (draft-ietf-oauth-spiffe-client-auth). Descope matches the trust domain in the SPIFFE ID to a SPIFFE federation on the app. The jwt-bearer type is also accepted for a JWT-SVID.
  • OIDC: Send urn:ietf:params:oauth:client-assertion-type:jwt-bearer. Descope matches the token's iss claim to an OIDC federation on the app.
curl -X POST "__BaseURL__/oauth2/v1/apps/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=<CLIENT_ID>" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe" \
  -d "client_assertion=<JWT_SVID>"

When your platform issues the workload token, request the project's issuer URL as its only audience. Descope rejects a workload token with any other aud, so a token minted for another service can't be replayed here. The token must also carry an exp no more than one hour in the future. When the token has a jti, Descope accepts that jti only once.

Prefer Workload Identity Federation when your workload already has a platform identity, because the platform issues and rotates that identity for you.

Public clients

Public (non-confidential) clients do not authenticate this way. They send client_id with no secret and rely on PKCE to protect the exchange.

Note

Descope authenticating to an external provider with a signed JWT is a separate feature. See Private Key JWT for custom OAuth providers and OIDC SSO for that direction.

Token exchange

Token exchange (RFC 8693) trades a subject token the caller already holds for a new token scoped to a specific downstream Resource. Credentials stored in Connections come from the Connection token endpoints instead. Token exchange is always available at this endpoint for every project; there is no per-app toggle. Policies with grant type Delegated access (token exchange) decide what each client may exchange.

Pass the token being exchanged as subject_token, its type as subject_token_type, and the target Resource as resource (RFC 8707):

curl -X POST "__BaseURL__/oauth2/v1/apps/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "client_id=<CLIENT_ID>" \
  -d "client_secret=<CLIENT_SECRET>" \
  -d "subject_token=<SUBJECT_TOKEN>" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "resource=https://your-api.example.com" \
  -d "scope=orders.read"

See Token exchange for policy setup, Resource targets, how Connection credentials differ, diagrams, and troubleshooting.

Audience

The aud (audience) claim names who the access token is intended for. Your API or gateway should reject tokens whose aud does not match the service receiving the request.

See Enforce scopes and Resource claims, for more details.

resource vs audience on the token endpoint

ParameterRole
resourceRFC 8707 resource indicator: the Resource URI to issue (or exchange) a token for. Preferred for API and MCP Server Resources.
audienceTarget audience for issued tokens or exchanges when the client or grant uses an audience value directly (including some exchange flows).

Prefer resource when calling a Descope-defined Resource. Use audience when your integration specifically needs that parameter; see the token endpoint schema.

Resource Audience

When a client targets a Resource (API or MCP server), Descope issues a token whose aud includes that Resource's identifier (for MCP Server Resources, typically the MCP server URL).

Clients select the Resource with the resource parameter (RFC 8707) on authorize and token requests. Pass the same Resource URI on both when the flow uses both endpoints.

{
  "aud": ["https://api.example.com"],
  "scope": "orders.read",
  "iss": "https://api.descope.com/<projectId>"
}

Resource Access and Policies

Which clients may receive tokens for which Resources (and scopes) is controlled by Policies, not by listing every Resource only on the Inbound App.

Inbound App audience settings

Each Inbound App also has Console settings under Audience:

  • Default Audience: Descope always includes these values on tokens from the app, whichever Resource the client requested. You can choose the client ID and project ID together (the default), the client ID only, or the project ID only. Choose the client ID only for OIDC clients, such as Shopify, that fail when aud holds more than one value.
  • Audience Whitelist: This legacy allowlist names the audiences the app may request. Prefer Policies to control Resource access.

A token can therefore include both the Resource identifier and the inbound app's default audience values in aud.

Checking the audience in your API

With the default setting, every Inbound App token carries your project ID in aud. Federated App tokens and Descope session tokens carry the project ID too. An API that accepts any token whose aud holds the project ID therefore accepts tokens that Descope issued to every other app in the project, including tokens that were never meant for that API.

Your API should check aud for its own Resource identifier rather than the project ID. When the client needs the Resource in aud, it requests it with the resource parameter.

When only some clients may call the API, the API can also check azp. On an Inbound App token, azp holds the client ID of the app that received the token. For a client that registered through a Client ID Metadata Document, azp holds the metadata URL instead.

Authorize Endpoint

The authorize endpoint starts interactive login and consent. Browser-based apps redirect users with query parameters such as client_id, redirect_uri, response_type=code, scope, state, PKCE fields (code_challenge, code_challenge_method), and optionally resource to target a specific Resource (required when authenticating to an MCP Server Resource).

Example redirect:

__BaseURL__/oauth2/v1/apps/authorize?\
client_id=<CLIENT_ID>&\
redirect_uri=https://yourapp.com/callback&\
response_type=code&\
scope=openid%20email%20profile&\
state=<RANDOM_STATE>

When targeting an MCP Server Resource, add resource (URL-encoded Resource URI):

__BaseURL__/oauth2/v1/apps/authorize?\
client_id=<CLIENT_ID>&\
redirect_uri=https://yourapp.com/callback&\
response_type=code&\
scope=mcp:tools.read&\
resource=https%3A%2F%2Fyour-mcp.example.com%2Fmcp&\
state=<RANDOM_STATE>

After the user completes the consent flow, Descope redirects back to redirect_uri with an authorization code. Exchange that code at the token endpoint, including the same resource value if you specified one at authorize time.

To keep these parameters out of the browser, push them to /oauth2/v1/apps/par from your server first, then redirect with only client_id and the returned request_uri. See Pushed Authorization Requests for the request format.

Revoke Endpoint

The revoke endpoint invalidates an access or refresh token (RFC 7009). Call it when a user disconnects an integration, you rotate credentials, or you need to end a session without waiting for natural expiry.

Send application/x-www-form-urlencoded (or JSON, per the API schema) with:

  • token: The access or refresh token to revoke.
  • token_type_hint: Optional hint: access_token or refresh_token.
  • client_id / client_secret: Inbound App credentials (required for confidential clients).

Example (revoke a refresh token):

curl -X POST "__BaseURL__/oauth2/v1/apps/revoke" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=<REFRESH_TOKEN>" \
  -d "token_type_hint=refresh_token" \
  -d "client_id=<CLIENT_ID>" \
  -d "client_secret=<CLIENT_SECRET>"

Revoking a refresh token prevents future renewals at the token endpoint. Revoking an access token invalidates it for subsequent API calls.

See the Revoke token API reference for the full request and response schema.

Revoking every token for a user

The revoke endpoint works on one token at a time. After a breach, you usually want to cut off a user everywhere at once. The Logout All User Sessions management call does that. Descope records the logout time on the user, which invalidates every access and refresh token issued to that user before it, including tokens issued to Inbound Apps and Agentic Clients.

Back-channel logout

An app that keeps its own session for a user needs to know when the user signs out of Descope. With back-channel logout (OpenID Connect Back-Channel Logout), Descope tells the app directly, server to server, so the app can end its session even when the user's browser never returns to it.

No Console field

The Console doesn't have a back-channel logout field for Inbound Apps. You can set backChannelLogoutUrl when you create or update the app with the Management API.

Descope sends an Inbound App a logout token only when one of its tokens is revoked at the revoke endpoint. Other logout events, such as an admin logging the user out, notify only Federated OIDC Apps.

The logout token follows the spec, and Descope signs it with your project's JWKs. A few details are specific to Descope:

  • iss: The issuer is __BaseURL__/__ProjectID__, your project's OIDC issuer, not the Inbound App issuer from Discovery and JWKs.
  • aud: The audience has your project ID and the app's client ID. Every app in your project shares the signing keys and the project ID, so your endpoint should reject a token without the app's client ID.
  • Session claims: The token also carries the user's session claims, such as tenants and roles, so your endpoint can find the right session without another lookup.

Logout waits for your endpoint by default

By default, Descope waits for your endpoint's response. When the endpoint is down, times out, or returns an error, the user's logout fails. Descope can switch your project to non-blocking delivery, where a failing endpoint never delays or fails the logout. Contact Descope to enable it.

User Info Endpoint

The UserInfo endpoint returns OpenID Connect claims for the subject of a valid access token. Use it when your app needs profile data (sub, email, name) or custom claims mapped in the Inbound App beyond what is embedded in the JWT. For when to introspect vs validate locally, see Token introspection.

Send the access token as a Bearer credential on the GET endpoint: the standard OIDC variant. A POST variant is available for clients that require it.

Example (GET):

curl "__BaseURL__/oauth2/v1/apps/userinfo" \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Example response (shape varies by scopes and Inbound App configuration):

{
  "sub": "U2lz...",
  "email": "user@example.com",
  "email_verified": true,
  "name": "Jane Doe"
}

The access token must include the openid scope, and any scopes required for specific claims (such as email or profile). Claims returned depend on what the user approved during consent and how the Inbound App maps user attributes.

See the Get UserInfo and Post UserInfo API references for the full request and response schema.

Configure clients and scopes

Was this helpful?

On this page