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
| Environment | Base URL |
|---|---|
| Descope (default) | https://api.descope.com |
| Custom domain | https://<your-api-host> |
All authorization server routes are rooted at:
{baseUrl}/oauth2/v1/apps/...Endpoint Reference
| Endpoint | Method | Purpose | API reference |
|---|---|---|---|
/oauth2/v1/apps/authorize | GET | Start the authorization code flow (browser redirect) | Get authorization |
/oauth2/v1/apps/authorize | POST | Start authorization with a JSON body (non-browser clients) | Post authorization |
/oauth2/v1/apps/par | POST | Push authorization parameters from your server before redirecting (PAR) | Inbound App PAR |
/oauth2/v1/apps/token | POST | Issue and refresh tokens; client credentials; JWT bearer; token exchange | Token endpoint |
/oauth2/v1/apps/revoke | POST | Revoke access or refresh tokens | Revoke token |
/oauth2/v1/apps/userinfo | GET | Return claims for the bearer access token | Get UserInfo |
/oauth2/v1/apps/userinfo | POST | Return claims (POST variant for clients that require it) | Post UserInfo |
/oauth2/v1/apps/introspect | POST | Check 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
issagainst 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-serverMCP 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 type | Typical use | Guide |
|---|---|---|
authorization_code | User-delegated access after consent | Authorization code |
refresh_token | Renew an access token without re-consent | Refresh token |
client_credentials | Machine-to-machine Inbound App tokens | Client credentials |
urn:ietf:params:oauth:grant-type:jwt-bearer | Exchange a trusted external JWT for Descope tokens | JWT bearer |
urn:ietf:params:oauth:grant-type:token-exchange | Trade 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:
issandsub: 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/tokenURL 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 anexpno more than five minutes in the future.jti: Every assertion needs a uniquejti. Descope rejects ajtithat 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. Thejwt-bearertype is also accepted for a JWT-SVID. - OIDC: Send
urn:ietf:params:oauth:client-assertion-type:jwt-bearer. Descope matches the token'sissclaim 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
| Parameter | Role |
|---|---|
resource | RFC 8707 resource indicator: the Resource URI to issue (or exchange) a token for. Preferred for API and MCP Server Resources. |
audience | Target 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
audholds 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_tokenorrefresh_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.
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
- Create and manage Inbound Apps in the Console or via Management API.
- Map API permissions to OAuth scopes on Resources.
- Validate Descope JWTs and enforce Resource scopes in Session validation. See also Developing APIs with OAuth.
Related
- Using Inbound Apps: step-by-step flows for each grant type
- Inbound Apps API reference: interactive OpenAPI docs for every authorization server route
- Resources: define scopes and audiences your tokens target
- Creating Inbound Apps → Audience: Default Audience and Audience Whitelist
- Agentic Identity Hub: agents, MCP servers, Connections, and STS token exchange
Building Consent Flows
Build Descope flows for Inbound App user consent and CIBA approval, including screen components and flow actions.
Pushed Authorization Requests
Send OAuth authorization parameters to Descope over a back-channel call with PAR (RFC 9126), so the browser redirect carries only a short request_uri.