> **Building with AI coding agents?** If you're using an AI coding agent, install the official Scalekit plugin. It gives your agent full awareness of the Scalekit API — reducing hallucinations and enabling faster, more accurate code generation.
>
> - **Claude Code**: `/plugin marketplace add scalekit-inc/claude-code-authstack` then `/plugin install <auth-type>@scalekit-auth-stack`
> - **GitHub Copilot CLI**: `copilot plugin marketplace add scalekit-inc/github-copilot-authstack` then `copilot plugin install <auth-type>@scalekit-auth-stack`
> - **Codex**: run the bash installer, restart, then open Plugin Directory and enable `<auth-type>`
> - **Skills CLI** (Windsurf, Cline, 40+ agents): `npx skills add scalekit-inc/skills --list` then `--skill <skill-name>`
>
> `<auth-type>` / `<skill-name>`: `agent-auth`, `full-stack-auth`, `mcp-auth`, `modular-sso`, `modular-scim` — [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Add OAuth 2.0 to your APIs

APIs let your customers, partners, and external systems interact with your application and its data. You need authentication to ensure only authorized clients can consume your APIs. Scalekit helps you add OAuth 2.0-based client-credentials authentication to your API endpoints.

Here's how it works:

```d2
shape: sequence_diagram

API Client
User

User -> Your App: 1. Request to register \n an API client \n with necessary scopes
Your App -> Scalekit: 2. Creates an API client \n in your Scalekit environment
Scalekit -> Your App: 3. Gives you a client_id \n and client_secret
Your App -> User: Show the client credentials
User -> API Client: 4. Saves the API credetials
API Client -> Your App: 5. Requests access to \n protected resource
Your App <> Scalekit: 6. Validates the access tokens
Your App -> API Client: 7. Returns the protected resource

```

1. ## Installation

   Scalekit becomes the authorization server for your APIs. Using Scalekit provides necessary methods to register and authenticate API clients.

   ```sh showLineNumbers=false
   pip install scalekit-sdk-python
   ```
   Alternatively, you can use the [REST APIs directly](/apis/#tag/api-auth).
**Note:** Scalekit provides Node.js, Python, Go, and Java SDKs. <a href="/support/contact-us">Contact us</a> if you need support for another language.

2. ## Enable API client registration for your customers

   Allow your customers to register their applications as API clients. This process generates unique credentials that they can use to authenticate their application when interacting with your API.

   Scalekit will return a client ID and secret that you can show to your customers to integrate their application with your API.
   - An Organization ID identifies your customer, and multiple API clients can be registered for the same organization.
   - The `POST /organizations/{organization_id}/clients` endpoint creates a new API client for the organization. See [Scalekit API Authentication](/apis/#description/quickstart) to get the `<SCALEKIT_ACCESS_TOKEN>` in case of HTTP requests.

   ```bash title="POST /organizations/{organization_id}/clients" showLineNumbers=false
   # For authentication details, see: http://docs.scalekit.com/apis#description/authentication
   curl -L '<SCALEKIT_ENVIRONMENT_URL>/api/v1/organizations/<ORGANIZATION_ID>/clients' \
   -H 'Content-Type: application/json' \
   -H 'Authorization: Bearer <SCALEKIT_ACCESS_TOKEN>' \
   -d '{
       "name": "GitHub Actions Deployment Service",  # A descriptive name for the API client
       "description": "Service account for GitHub Actions to deploy applications to production",  # A detailed explanation of the clients purpose and usage
       "custom_claims": [  # Key-value pairs that provide additional context about the client. Each claim must have a `key` and `value` field
           {
               "key": "github_repository",
               "value": "acmecorp/inventory-service"
           },
           {
               "key": "environment",
               "value": "production_us"
           }
       ],
       "scopes": [  # List of permissions the client needs (e.g., ["deploy:applications", "read:deployments"])
           "deploy:applications",
           "read:deployments"
       ],
       "audience": [  # List of API endpoints this client will access (e.g., ["deployment-api.acmecorp.com"])
           "deployment-api.acmecorp.com"
       ],
       "expiry": 3600  # Token expiration time in seconds. Defaults to 3600 (1 hour)
   }'
   ```

   <details>
   <summary>Sample response</summary>

   ```json title="Sample response" showLineNumbers=false
   {
       "client": {
           "client_id": "m2morg_68315758685323697",
           "secrets": [
               {
                   "id": "sks_68315758802764209",
                   "create_time": "2025-04-16T06:56:05.360Z",
                   "update_time": "2025-04-16T06:56:05.367190455Z",
                   "secret_suffix": "UZ0X",
                   "status": "ACTIVE",
                   "last_used_time": "2025-04-16T06:56:05.360Z"
               }
           ],
           "name": "GitHub Actions Deployment Service",
           "description": "Service account for GitHub Actions to deploy applications to production",
           "organization_id": "org_59615193906282635",
           "create_time": "2025-04-16T06:56:05.290Z",
           "update_time": "2025-04-16T06:56:05.292145150Z",
           "scopes": [
               "deploy:applications",
               "read:deployments"
           ],
           "audience": [
               "deployment-api.acmecorp.com"
           ],
           "custom_claims": [
               {
                   "key": "github_repository",
                   "value": "acmecorp/inventory-service"
               },
               {
                   "key": "environment",
                   "value": "production_us"
               }
           ]
       },
       "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoa..."
   }
   ```
   </details>
   ```python
   from scalekit.v1.clients.clients_pb2 import OrganizationClient

   org_id = "<SCALEKIT_ORGANIZATION_ID>"

   api_client = OrganizationClient(
       name="GitHub Actions Deployment Service",  # A descriptive name for the API client
       description="Service account for GitHub Actions to deploy applications to production",  # A detailed explanation of the client's purpose and usage
       custom_claims=[  # Key-value pairs that provide additional context about the client. Each claim must have a `key` and `value` field
           {
               "key": "github_repository",
               "value": "acmecorp/inventory-service"
           },
           {
               "key": "environment",
               "value": "production_us"
           }
       ],
       scopes=["deploy:applications", "read:deployments"],  # List of permissions the client needs
       audience=["deployment-api.acmecorp.com"],  # List of API endpoints this client will access
       expiry=3600  # Token expiration time in seconds. Defaults to 3600 (1 hour)
   )

   response = scalekit_client.m2m_client.create_organization_client(
       organization_id=org_id,
       m2m_client=api_client
   )

   # Persist the generated credentials securely in your application
   client_id = response.client.client_id
   plain_secret = response.plain_secret
   ```

   <details>
   <summary>Sample response</summary>

   ```json title="Sample response" showLineNumbers=false
   {
       "client": {
           "client_id": "m2morg_68315758685323697",
           "secrets": [
               {
                   "id": "sks_68315758802764209",
                   "create_time": "2025-04-16T06:56:05.360Z",
                   "update_time": "2025-04-16T06:56:05.367190455Z",
                   "secret_suffix": "UZ0X",
                   "status": "ACTIVE",
                   "last_used_time": "2025-04-16T06:56:05.360Z"
               }
           ],
           "name": "GitHub Actions Deployment Service",
           "description": "Service account for GitHub Actions to deploy applications to production",
           "organization_id": "org_59615193906282635",
           "create_time": "2025-04-16T06:56:05.290Z",
           "update_time": "2025-04-16T06:56:05.292145150Z",
           "scopes": [
               "deploy:applications",
               "read:deployments"
           ],
           "audience": [
               "deployment-api.acmecorp.com"
           ],
           "custom_claims": [
               {
                   "key": "github_repository",
                   "value": "acmecorp/inventory-service"
               },
               {
                   "key": "environment",
                   "value": "production_us"
               }
           ]
       },
       "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcf.."
   }
   ```

   </details>
**Tip:** Scalekit only returns the `plain_secret` once during client creation and does not store it. Instruct your API client developers to store the `plain_secret` securely.

3. ## API client requests Bearer access token for API authentication

    API clients use the `client_id` and `client_secret` issued in the previous step to reach your Scalekit environment and get the access token. No action is needed by you in your API server. This section only demonstrates how API clients get the `access_token`.

   The client sends a POST request to the `/oauth/token` endpoint:

   ```sh title="POST /oauth/token"
   curl -X POST \
     "https://<SCALEKIT_ENVIRONMENT_URL>/oauth/token" \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "grant_type=client_credentials" \
     -d "client_id=<API_CLIENT_ID>" \
     -d "client_secret=<API_CLIENT_SECRET>" \
   ```

   ```python showLineNumbers
   client_id = "API_CLIENT_ID"
   client_secret = "API_CLIENT_SECRET"

   token_response = scalekit_client.generate_client_token(
       client_id=client_id,
       client_secret=client_secret
   )
   ```

   Upon successful authentication, your Scalekit environment issues a JWT access token to the API client.

   ```json title="Access token response"
   {
     "access_token":"<API_CLIENT_JWT_ACCESS_TOKEN>",
     "token_type":"Bearer",
     "expires_in":86399,
     // Same scopes that were granted during client registration
     "scope":"deploy:applications read:deployments"
   }
   ```

   The client includes this access token in the `Authorization` header of subsequent requests to your API server. Your API server validates these tokens before granting access to resources.

4. ## Validate and authenticate API client's access tokens

   Your API server must validate the incoming JWT access token to ensure the request originates from a trusted API client and that the token is legitimate.

   Validate the token in two steps:

1. **Retrieve the public key:** Fetch the appropriate public key from your Scalekit environment's JSON Web Key Set (JWKS) endpoint. Use the `kid` (Key ID) from the JWT header to identify the correct key. Cache the key according to standard JWKS practices.

      ```js showLineNumbers=false
      import jwksClient from 'jwks-rsa';

      const client = jwksClient({
        jwksUri: 'YOUR_JWKS_URI',
        cache: true
      });

      async function getPublicKey(header: any): Promise<string> {
        return new Promise((resolve, reject) => {
          client.getSigningKey(header.kid, (err, key) => {
            if (err) reject(err);
            else resolve(key.getPublicKey());
          });
        });
      }
      ```

      ```py showLineNumbers=false
      # This is automatically handled by Scalekit SDK
      ```

2. **Verify the token signature:** Use the retrieved public key and a JWT library to verify the token's signature and claims (like issuer, audience, and expiration).

      ```js showLineNumbers=false
      import jwt from 'jsonwebtoken';

      async function verifyToken(token: string, publicKey: string) {
        try {
          const decoded = jwt.decode(token, { complete: true });
          const verified = jwt.verify(token, publicKey, {
            algorithms: ['RS256'],
            complete: true
          });
          return verified.payload;
        } catch (error) {
          throw new Error('Token verification failed');
        }
      }
      ```

      ```py showLineNumbers=false
      # Token from the incoming API request's authorization header
      token = token_response["<API_CLIENT_JWT_ACCESS_TOKEN>"]

      claims = scalekit_client.validate_access_token_and_get_claims(
          token=token
      )
      ```

      Upon successful token verification, your API server gains confidence in the request's legitimacy and can proceed to process the request, leveraging the authorization scopes embedded within the token.

5. ## Register API client's scopes Optional

   Scopes are embedded in the access token and validated server-side using the Scalekit SDK. This ensures that API clients only access resources they're authorized for, adding an extra layer of security.

   For example, you might create an API client for a customer's deployment service with scopes like `deploy:applications` and `read:deployments`.

   ```bash title="Register an API client with specific scopes" wrap
   curl -L 'https://<SCALEKIT_ENVIRONMENT_URL>/api/v1/organizations/<ORGANIZATION_ID>/clients' \
   -H 'Content-Type: application/json' \
   -H 'Authorization: Bearer <SCALEKIT_ACCESS_TOKEN>' \
   -d '{
       "name": "GitHub Actions Deployment Service",
       "description": "Service account for GitHub Actions to deploy applications to production",
       "scopes": [
           "deploy:applications",
           "read:deployments"
       ],
       "expiry": 3600
   }'
   ```
   <details>
   <summary>Sample response</summary>

   ```json title="Sample response" showLineNumbers=false
   {
       "client": {
           "client_id": "m2morg_68315758685323697",
           "secrets": [
               {
                   "id": "sks_68315758802764209",
                   "create_time": "2025-04-16T06:56:05.360Z",
                   "update_time": "2025-04-16T06:56:05.367190455Z",
                   "secret_suffix": "UZ0X",
                   "status": "ACTIVE",
                   "last_used_time": "2025-04-16T06:56:05.360Z"
               }
           ],
           "name": "GitHub Actions Deployment Service",
           "description": "Service account for GitHub Actions to deploy applications to production",
           "organization_id": "org_59615193906282635",
           "create_time": "2025-04-16T06:56:05.290Z",
           "update_time": "2025-04-16T06:56:05.292145150Z",
           "scopes": [
               "deploy:applications",
               "read:deployments"
           ]
       },
        "plain_secret": "<REDACTED_SECRET>"
   }
   ```
   </details>
   ```javascript title="Register an API client with specific scopes"
   // Use case: Your customer requests API access for their deployment automation.
   // You register an API client app with the appropriate scopes.
   import { ScalekitClient } from '@scalekit-sdk/node';

   // Initialize Scalekit client (see installation guide for setup)
   const scalekit = new ScalekitClient(
     process.env.SCALEKIT_ENVIRONMENT_URL,
     process.env.SCALEKIT_CLIENT_ID,
     process.env.SCALEKIT_CLIENT_SECRET
   );

   async function createAPIClient() {
     try {
       // Define API client details with scopes your customer's app needs
       const clientDetails = {
         name: 'GitHub Actions Deployment Service',
         description: 'Service account for GitHub Actions to deploy applications to production',
         scopes: ['deploy:applications', 'read:deployments'],
         expiry: 3600, // Token expiry in seconds
       };

       // API call to register the client
       const response = await scalekit.m2m.createClient({
         organizationId: process.env.SCALEKIT_ORGANIZATION_ID,
         client: clientDetails,
       });

       // Response contains client details and the plain_secret (only returned once)
       const clientId = response.client.client_id;
       const plainSecret = response.plain_secret;

       // Provide these credentials to your customer securely
       console.log('Created API client:', clientId);
     } catch (error) {
       console.error('Error creating API client:', error);
     }
   }

   createAPIClient();
   ```

   ```python title="Register an API client with specific scopes"
   # Use case: Your customer requests API access for their deployment automation.
   # You register an API client app with the appropriate scopes.
   import os
   from scalekit import ScalekitClient

   # Initialize Scalekit client (see installation guide for setup)
   scalekit_client = ScalekitClient(
       env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
       client_id=os.getenv("SCALEKIT_CLIENT_ID"),
       client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
   )

   try:
       # Define API client details with scopes your customer's app needs
       from scalekit.v1.clients.clients_pb2 import OrganizationClient

       client_details = OrganizationClient(
           name="GitHub Actions Deployment Service",
           description="Service account for GitHub Actions to deploy applications to production",
           scopes=["deploy:applications", "read:deployments"],
           expiry=3600  # Token expiry in seconds
       )

       # API call to register the client
       response = scalekit_client.m2m_client.create_organization_client(
           organization_id=os.getenv("SCALEKIT_ORGANIZATION_ID"),
           m2m_client=client_details
       )

       # Response contains client details and the plain_secret (only returned once)
       client_id = response.client.client_id
       plain_secret = response.plain_secret

       # Provide these credentials to your customer securely
       print("Created API client:", client_id)

   except Exception as e:
       print("Error creating API client:", e)
   ```

   The API returns a JSON object with two key parts:
   - `client.client_id` - The client identifier
   - `plain_secret` - The client secret (only returned once, never stored by Scalekit)

   Provide both values to your customer securely. Your customer will use these credentials in their application to authenticate with your API. The `plain_secret` is never shown again after creation.
**Additional parameters:** You can also include `custom_claims` (key-value metadata) and `audience` (target API endpoints) when registering API clients. See the [API keys guide](/authenticate/m2m/api-keys) for examples.

6. ## Verify API client's scopes

    When your API server receives a request from an API client app, you must validate the scopes present in the access token provided in the `Authorization` header. The access token is a JSON Web Token (JWT).

    First, let's look at the claims inside a decoded JWT payload. Scalekit encodes the granted scopes in the `scopes` field.

    ```json title="Example decoded access token" showLineNumbers=false {9-12}
    {
      "client_id": "m2morg_69038819013296423",
      "exp": 1745305340,
      "iat": 1745218940,
      "iss": "<SCALEKIT_ENVIRONMENT_URL>",
      "jti": "tkn_69041163914445100",
      "nbf": 1745218940,
      "oid": "org_59615193906282635",
      "scopes": [
        "deploy:applications",
        "read:deployments"
      ],
      "sub": "m2morg_69038819013296423"
    }
    ```
**Scope Naming Conventions:** Structure your scopes using the `resource:action` pattern, for example `deployments:read` or `applications:create`. This makes permissions clear and manageable for your customers.

    Your API server should inspect the `scopes` array in the token payload to authorize the requested operation. Here's how you validate the token and check for a specific scope in your API server.

    ```javascript title="Example Express.js middleware for scope validation" collapse={1-27}
    // Security: ALWAYS validate the access token on your server before trusting its claims.
    // This prevents token forgery and ensures the token has not expired.
    import { ScalekitClient } from '@scalekit-sdk/node';
    import jwt from 'jsonwebtoken';
    import jwksClient from 'jwks-rsa';

    const scalekit = new ScalekitClient(
      process.env.SCALEKIT_ENVIRONMENT_URL,
      process.env.SCALEKIT_CLIENT_ID,
      process.env.SCALEKIT_CLIENT_SECRET
    );

    // Setup JWKS client for token verification
    const client = jwksClient({
      jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/.well-known/jwks.json`,
      cache: true
    });

    async function getPublicKey(header) {
      return new Promise((resolve, reject) => {
        client.getSigningKey(header.kid, (err, key) => {
          if (err) reject(err);
          else resolve(key.getPublicKey());
        });
      });
    }

    async function checkPermissions(req, res, next) {
      const authHeader = req.headers.authorization;
      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).send('Unauthorized: Missing token');
      }
      const token = authHeader.split(' ')[1];

      try {
        // Decode to get the header with kid
        const decoded = jwt.decode(token, { complete: true });
        const publicKey = await getPublicKey(decoded.header);

        // Verify the token signature and claims
        const verified = jwt.verify(token, publicKey, {
          algorithms: ['RS256'],
          complete: true
        });

        const decodedToken = verified.payload;

        // Check if the API client app has the required scope
        const requiredScope = 'deploy:applications';
        if (decodedToken.scopes && decodedToken.scopes.includes(requiredScope)) {
          // API client app has the required scope, proceed with the request
          next();
        } else {
          // API client app does not have the required scope
          res.status(403).send('Forbidden: Insufficient permissions');
        }
      } catch (error) {
        // Token is invalid or expired
        res.status(401).send('Unauthorized: Invalid token');
      }
    }
    ```

    ```python title="Example Flask decorator for scope validation" collapse={1-14}
    # Security: ALWAYS validate the access token on your server before trusting its claims.
    # This prevents token forgery and ensures the token has not expired.
    import os
    import functools
    from scalekit import ScalekitClient
    from flask import request, jsonify

    # Initialize Scalekit client
    scalekit_client = ScalekitClient(
        env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
        client_id=os.getenv("SCALEKIT_CLIENT_ID"),
        client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
    )

    def check_permissions(required_scope):
        def decorator(f):
            @functools.wraps(f)
            def decorated_function(*args, **kwargs):
                auth_header = request.headers.get('Authorization')
                if not auth_header or not auth_header.startswith('Bearer '):
                    return jsonify({"error": "Unauthorized: Missing token"}), 401

                token = auth_header.split(' ')[1]

                try:
                    # Validate the token using the Scalekit SDK
                    claims = scalekit_client.validate_access_token_and_get_claims(token=token)

                    # Check if the API client app has the required scope
                    if required_scope in claims.get('scopes', []):
                        # API client app has the required scope
                        return f(*args, **kwargs)
                    else:
                        # API client app does not have the required scope
                        return jsonify({"error": "Forbidden: Insufficient permissions"}), 403
                except Exception as e:
                    # Token is invalid or expired
                    return jsonify({"error": "Unauthorized: Invalid token"}), 401
            return decorated_function
        return decorator

    # Example usage in a Flask route
    # @app.route('/deploy', methods=['POST'])
    # @check_permissions('deploy:applications')
    # def deploy_application():
    #     return jsonify({"message": "Deployment successful"})
    ```

---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
