---
title: Client Credentials Flow
description: Configure OAuth2 client credentials flow for microservices authentication and access token verification.
sidebar:
  order: 2
---

## Overview

In the **Client Credentials Flow** the authentication sequence works in the following way:

1. **Service A uses credentials to get an OAuth2 Access Token**

2. **Authorization Service(/authentication/unified-login/oauth2-basics#authorization-server) returns the OAuth2 Access Token**

3. **Service A uses the OAuth2 Access Token to communicate with Service B**

4. **Service B validates the OAuth2 Access Token**

5. **If the token is valid Service B returns the requested resource**

<img class="docs-image-content-width" src="/docs-assets/img/oauth/machine-to-machine.png" alt="Machine to Machine Authentication"/>

Before going into the actual instructions, start by imagining a real life example that you can reference along the way.
This makes it easier to understand what is happening.

We are going to configure authentication for the following setup:
- A **Calendar Service** that exposes these actions: `event.view`, `event.create`, `event.update` and `event.delete`
- A **File Service** that exposes these actions: `file.view`, `file.create`, `file.update` and `file.delete`
- A **Task Service** that interacts with the **Calendar Service** and the **File Service** in the process of scheduling a task

The aim is to allow the **Task Service** to perform an authenticated action on the **Calendar Service**.
Proceed to the actual steps.

## Before you start

<PaidFeatureCallout managedOnly />


## Steps

### 1. Enable the OAuth2 features from the Dashboard

You first have to enable **M2M Authentication** from the [**SuperTokens.com Dashboard**](https://supertokens.com/dashboard). Select the relevant **Managed** deployment, open **Features**, and enable **M2M Authentication**. Changes are saved automatically.

You should be able to use the OAuth2 recipes in your applications.

### 2. Create the OAuth2 Clients

For each of your **`microservices`** you need to create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client).
This can occur by directly calling the **SuperTokens Core** API.

For manual curl testing, provision a config through your secret-management or deployment system, restrict it to the
service account with mode `0600`, and do not commit it:

```text
header = "api-key: <YOUR_API_KEY>"
```

The cURL example refers to this file as `<CORE_API_PROTECTED_CURL_CONFIG>`. This keeps the API key out of shell history and
process arguments. Disable shell tracing and curl verbose or trace output, and ensure HTTP, process, and error logs do not
record request headers, config contents, or the API key.

See the [Create OAuth2 client API reference](/references/cdi/oauth2provider-recipe/createoauth2client) for the complete
request schema and response details.

<ApiRequestSnippet
  operationId="createOAuth2Client"
  source="cdi"
  path={{ appId: "public" }}
  curlConfig="<CORE_API_PROTECTED_CURL_CONFIG>"
  body={{
    clientId: "<STABLE_CLIENT_ID>",
    clientName: "<YOUR_CLIENT_NAME>",
    grantTypes: ["client_credentials"],
    scope: "<custom_scope_1> <custom_scope_2>",
    audience: ["<AUDIENCE_NAME>"],
  }}
/>

:::info[Custom Example]

To create a client for the **Task Service**, use the following attributes:

```json
{
  "clientId": "task-service",
  "clientName": "Task Service",
  "grantTypes": ["client_credentials"],
  "scope": "event.view event.create event.update event.delete file.view file.create file.update file.delete",
  "audience": ["event", "file"]
}
```

This allows the **Task Service** to perform all types of actions against both of the other services as long as it has a valid **OAuth2 Access Token**.

:::

:::note[Retry client provisioning safely]
Client creation has no documented duplicate-request key. Use a stable `clientId`, serialize provisioning for that client, and
after a timeout query the client by ID before retrying. Do not blindly retry an uncertain `POST` response.
:::

:::warning[Protect the client credentials]
Store the client ID and secret in a secret manager. The Core persists the secret encrypted at rest, and callers with the
Core API key can retrieve it. Treat both the API key and client secret as sensitive credentials.
:::


### 3. Set Up your Authorization Service

The Node.js and Python SDKs automatically initialize the **OAuth2Provider** recipe when it is absent. Add it explicitly to
your [**Authorization Server**](/authentication/unified-login/oauth2-basics#authorization-server) configuration when you
need recipe overrides or want to make the dependency visible.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import supertokens from "supertokens-node";
import OAuth2Provider from "supertokens-node/recipe/oauth2provider";

supertokens.init({
  supertokens: {
    connectionURI: "...",
    apiKey: "...",
  },
  appInfo: {
    appName: "...",
    apiDomain: "...",
    websiteDomain: "...",
  },
  recipeList: [OAuth2Provider.init()],
});
```
</Tab>
<Tab title="Python" value="python">
```python
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import oauth2provider

init(
    app_info=InputAppInfo(
        app_name="...",
        api_domain="...",
        website_domain="...",
    ),
    framework="fastapi",
    supertokens_config=SupertokensConfig(
        connection_uri="...",
        api_key="..."
    ),
    recipe_list=[
        oauth2provider.init()
    ],
)
```
</Tab>
</CodeGroup>

### 4. Generate access tokens


You can directly call the [**Authorization Server**](/authentication/unified-login/oauth2-basics#authorization-server) to generate Access Tokens.
See the [Exchange OAuth grant API reference](/references/fdi/oauth2provider-recipe/oauthtokenpost) for response schemas and
error details. The cURL example remains authoritative for this request because the current FDI specification does not model
the form-encoded request body or HTTP Basic client authentication.
Keep the client secret out of command arguments and shell history. For manual testing, provision a curl config through
your secret-management or deployment system, restrict it to the service account with mode `0600`, and do not commit it:

```text
user = "<CLIENT_ID>:<CLIENT_SECRET>"
```

Then reference the protected config by path:

```bash
curl -X POST '<YOUR_API_DOMAIN>/auth/oauth/token' \
  --config '<PROTECTED_CURL_CONFIG>' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=<RESOURCE_SCOPE>' \
  --data-urlencode 'audience=<AUDIENCE>'
```

For production, load the secret from a secret manager in your application client. Disable shell tracing and ensure HTTP,
process, and error logs do not record authorization headers, curl configuration contents, or client secrets.

You should limit the scopes that you are requesting to the ones necessary to perform the desired action.

:::info[Custom Example]

If the **Task Service** wants to create an event on the **Calendar Service**, a token with the following attributes needs generation:

```bash
curl -X POST '<YOUR_API_DOMAIN>/auth/oauth/token' \
  --config '<TASK_SERVICE_PROTECTED_CURL_CONFIG>' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=event.create' \
  --data-urlencode 'audience=event'
```

:::

The **Authorization Server** returns a response that looks like this:

```json
{
  "access_token": "<TOKEN_VALUE>",
  "expires_in": 3600,
  "token_type": "bearer",
  "scope": "event.create"
}
```

Save the `access_token` in memory for use in the next step.
The `expires_in` field indicates how long the token is valid for.

Each service that you communicate with needs its own token.

With an **OAuth2 Access Token**, it can facilitate communication with the other services.
Keep in mind to generate a new one when it expires.

### 5. Verify an OAuth2 Access Token

Use the released SuperTokens backend SDK validator instead of implementing JWT validation yourself. It validates the
signature, expiration, and `stt=1` token type. Pass requirements for the intended audience, client, and every scope needed
by the operation. Also compare the token issuer with your Authorization Server's issuer.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import OAuth2Provider from "supertokens-node/recipe/oauth2provider";

async function validateClientCredentialsToken(token: string): Promise<boolean> {
  try {
    const result = await OAuth2Provider.validateOAuth2AccessToken(token, {
      audience: "<AUDIENCE>",
      clientId: "<CLIENT_ID>",
      scopes: ["<YOUR_REQUIRED_SCOPE>"],
    });

    return result.payload.iss === "<YOUR_API_DOMAIN>/auth";
  } catch {
    return false;
  }
}
```
</Tab>
<Tab title="Python" value="python">
```python
from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements
from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token


def validate_client_credentials_token(token: str) -> bool:
    try:
        result = validate_oauth2_access_token(
            token=token,
            requirements=OAuth2TokenValidationRequirements(
                audience="<AUDIENCE>",
                client_id="<CLIENT_ID>",
                scopes=["<YOUR_REQUIRED_SCOPE>"],
            ),
        )
        return result.payload.get("iss") == "<YOUR_API_DOMAIN>/auth"
    except Exception:
        return False
```
</Tab>
</CodeGroup>

:::warning[Bearer tokens do not prevent request replay]
Token validation authenticates and authorizes a request; it does not make a state-changing operation replay-safe. For
create or update APIs, require an application-level unique request key, atomically bind it to the authenticated client,
operation, and request-body digest, and return the stored result for an exact retry. Reject reuse with a different payload
and use business uniqueness or conditional updates where appropriate.
:::

:::info[Custom Example]

If the **Task Service** uses the previously generated token to create a calendar event, the **Calendar Service** must
require `stt=1`, the `event.create` scope, the `event` audience, the expected Task Service client ID, and the expected
Authorization Server issuer.

:::

#### Handle both SuperTokens session tokens and OAuth2 access tokens

If your Authorization Server is also a Resource Server, a protected route may accept either a SuperTokens session or an
OAuth2 access token. Parse the `Authorization` header strictly. Never accept a malformed bearer value, and never ignore a
validator's failure or false result.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import express, { type NextFunction, type Request, type Response } from "express";
import OAuth2Provider from "supertokens-node/recipe/oauth2provider";
import Session from "supertokens-node/recipe/session";

async function verifySessionOrOAuthToken(req: Request, res: Response, next: NextFunction) {
  const authorization = req.headers.authorization;

  if (authorization !== undefined) {
    try {
      const result = await OAuth2Provider.validateOAuth2AccessToken(match[1], {
        audience: "<AUDIENCE>",
        clientId: "<CLIENT_ID>",
        scopes: ["<REQUIRED_SCOPE>"],
      });
      if (result.payload.iss === "<YOUR_API_DOMAIN>/auth") {
        return next();
      }
    } catch {
      // The bearer token may be a SuperTokens session access token.
    }
  }

  try {
    await Session.getSession(req, res);
    return next();
  } catch {
    return res.status(401).json({ message: "Unauthorized" });
  }
}

const app = express();
app.get("/protected", verifySessionOrOAuthToken, async (_req, res) => {
  res.json({ message: "Authorized" });
});
```
</Tab>
<Tab title="Python" value="python">
```python
from fastapi import HTTPException
from fastapi.requests import Request
from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements
from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token
from supertokens_python.recipe.session.syncio import get_session


def verify_session_or_oauth_token(request: Request) -> bool:
    authorization = request.headers.get("authorization")

    if authorization is not None:
        try:
            result = validate_oauth2_access_token(
                token=match.group(1),
                requirements=OAuth2TokenValidationRequirements(
                    audience="<AUDIENCE>",
                    client_id="<CLIENT_ID>",
                    scopes=["<REQUIRED_SCOPE>"],
                ),
            )
            if result.payload.get("iss") == "<YOUR_API_DOMAIN>/auth":
                return True
        except Exception:
            # The bearer token may be a SuperTokens session access token.
            pass

    try:
        get_session(request)
        return True
    except Exception as error:
        raise HTTPException(status_code=401, detail="Unauthorized") from error
```
</Tab>
</CodeGroup>
