Using Inbound Apps

After configuring an Inbound App in Descope, you can integrate it into third-party applications, OAuth clients (like NextAuth), and your backend services.

Inbound Apps support two main OAuth flows:

  • Authorization Code Flow — Users log in interactively and grant consent.
  • Client Credentials Flow — Services obtain tokens without user interaction.

For a complete working implementation with both Authorization Code and Client Credentials flows, refer to the Descope 3rd-Party Sample App.

Authorization Code Flow

This flow is recommended when a user is present, such as in web apps, mobile apps, or third-party integrations that require explicit user consent and the running of a flow.

How It Works

  • Your app redirects the user to Descope's /authorize endpoint.
  • The user signs in and approves the requested scopes.
  • Descope redirects back to your redirect_uri with an authorization code.
  • Your app exchanges that code for tokens using /token.

Scopes

When including scopes in your /authorize request, the scopes you include must be pre-defined in the Inbound App configuration.

Here is an example of how to include scopes in your /authorize request:

https://api.descope.com/oauth2/v1/apps/authorize? 
  client_id=<CLIENT_ID>
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=full_access
  &state=<RANDOM_STATE_STRING>

In this example, the scope full_access will be included in the consent screen, and the scopes claim of your OAuth token.

consent-screen-with-scopes

1. Initiating Login with Descope

This URL should be automatically constructed if you're using any standard OAuth Client SDK or library.

The third-party application redirects users to Descope's /authorize endpoint, prompting them to authenticate and approve the requested permissions.

https://api.descope.com/oauth2/v1/apps/authorize? 
  client_id=<CLIENT_ID>
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=openid email profile
  &state=<RANDOM_STATE_STRING>

This is the general process that will occur when a user initates login:

  • The user logs in via the Descope Consent Flow.
  • The user approves the requested scopes on the consent screen.
  • Descope redirects the user back to the callback URL with an authorization code.

When using Inbound Apps, if you do not provide user consent to the client requested scopes, the flow will return an error and an OAuth token will not be issued.

Example: NextAuth Application Acting As OAuth Client

Here is an example of how you can configure Auth.js to act as an OAuth client for Descope, instead of manually constructing the /authorize request yourself:

import NextAuth from "next-auth";
import { OIDCConfig } from "next-auth/providers";
 
export const baseUrl = "https://api.descope.com";
export const clientId = process.env.CLIENT_ID ?? "";
export const clientSecret = process.env.CLIENT_SECRET ?? "";
 
const DescopeOAuthApps = (): OIDCConfig => ({
    id: "customapp",
    name: "Custom App",
    type: "oidc",
    authorization: { params: { scope: "openid email profile", prompt: "consent" } },
    client: { token_endpoint_auth_method: "client_secret_post" },
    checks: ["pkce", "state"],
});
 
export const { signIn, signOut, auth } = NextAuth({
    providers: [
        {
            ...DescopeOAuthApps(),
            clientId,
            clientSecret,
        },
    ],
});

2. Handling the OAuth Callback

After successful authentication and consent, Descope redirects the user to the redirect_uri, appending an authorization code.

Example response to the callback URL:

https://yourapp.com/callback?code=<AUTHORIZATION_CODE>&state=<RANDOM_STATE_STRING>

Once the application receives the authorization code, it must be exchanged for an access token by making a POST request to the Descope /token endpoint.

3. Exchanging Authorization Code for an Access Token

When you initially grant, modify, or revoke consent in some way, this will be reflected in the audit trail.

However, additional authorization code exchanges that don't involve consent changes will not be reflected in the audit trail.

There are two ways to exchange the authorization code for an access token:

Using a Client Secret (Confidential Clients - Backend Apps)

Recommended for server-side apps that can securely store secrets.

curl -X POST "https://api.descope.com/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"

Using PKCE (Public Clients - SPAs, Mobile Apps)

Recommended for apps that cannot securely store secrets.

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

4. Using the Token with an API

If successful, Descope responds with an access token, as well as an id token and refresh token:

{
  "access_token": "eyJhbGciOiJIUz...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "abc123",
  "id_token": "eyJhbGciOiJIUz..."
}

With this response, three types of tokens will be returned:

  • access_token → Used to authenticate API requests.
  • id_token → Contains user identity claims (for OpenID Connect).
  • refresh_token → Used to obtain a new access token when it expires.

Once authenticated, the access token is used to authorize API calls.

curl -X GET "https://api.yourservice.com/user/profile" \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

If the user revokes consent or if the user consent expires, once the access token expires, it will no longer be able to be refreshed. It is therefore recommended to keep your session expiry window short.

5. Refreshing Access Tokens

Inbound App token refresh will not be reflected in the audit trail.

Access tokens have a limited lifetime, and will expire after a certain period of time. To refresh an access token, use the /token endpoint with the refresh_token grant type.

curl -X POST "https://api.descope.com/oauth2/v1/apps/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "client_id=<CLIENT_ID>" \
  -d "client_secret=<CLIENT_SECRET>" \
  -d "refresh_token=<REFRESH_TOKEN>"  

Client Credentials Flow

Client credentials exchanges for inbound app tokens will not be reflected in the audit trail.

In Inbound Apps, the client credentials grant lets a backend service obtain a user's JWT without requiring user interaction. Instead of creating a brand-new token, the service can request one directly using its own credentials.

This approach is especially useful in machine-to-machine (M2M) scenarios, where services need to act on behalf of a user or system account without any manual sign-in.

How It Works in Inbound Apps

  1. The backend service authenticates itself with the /token endpoint using its client ID and client secret.
  2. Descope verifies the credentials and issues a user JWT tied to the specified client ID (service account).

Example: Fetching a Token Using Client Credentials

curl -X POST \
  https://<your-descope-base-url>/oauth2/v1/apps/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=<YOUR_CLIENT_ID>" \
  -d "client_secret=<YOUR_CLIENT_SECRET>" \
  -d "scope=openid email profile"

Example: Next.js Backend Route Using Client Credentials

import { auth, baseUrl, clientId, clientSecret } from "@/auth";
import { NextResponse } from "next/server";
 
export const GET = auth(async function GET(req) {
  if (req.auth) {
    const body =
      "grant_type=client_credentials" +
      "&client_id=" + clientId +
      "&client_secret=" + clientSecret +
      "&scope=openid email profile";
 
    const res = await fetch(`${baseUrl}/oauth2/v1/apps/token`, {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body,
    });
 
    return NextResponse.json(await res.json(), { status: res.status });
  }
  return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
});

Getting Outbound App Tokens via Inbound Apps

This currently only works for Inbound App access tokens that are associated with a user (i.e. created with Authorization Code flow). Inbound app tokens created with the Client Credentials flow cannot be used to retrieve Outbound App tokens.

Once the user is authenticated and authorized via an Inbound App, you can use the access token to retrieve an Outbound App token for use with third-party services or internal tools.

To learn how to use an Inbound App access token to retrieve Outbound App tokens, see Using Outbound Apps.

This allows your inbound application to act as a bridge between an authenticated user identity and the tools your MCP server needs to access.

Was this helpful?