Skip to content
Talk to an Engineer Dashboard

Add OAuth2.0 to your APIs

Secure your APIs in minutes with OAuth 2.0 client credentials, scoped access, and JWT validation using Scalekit

APIs are a way to let your customers, partners, and external systems interact with your application and it’s data. It requires authentication to ensure only authorized clients can consume your APIs. Scalekit provides a way to add OAuth2.0 based client credentials authentication to your API endpoints.

In should here’s how it would work:

API ClientUserYour AppScalekit 1. Request to register an API client with necessary scopes 2. Creates an API client in your Scalekit environment 3. Gives you a client_id and client_secret Show the client credentials 4. Saves the API credetials 5. Requests access to protected resource 6. Validates the access tokens 7. Returns the protected resource
  1. Scalekit becomes the authorization server for your APIs. Using Scalekit provides necessary methods to register and authenticate API clients.

    Terminal window
    pip install scalekit-sdk-python

    Alternatively, you can use the REST APIs directly.

  2. Enable API client registration for your customers

    Section titled “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 to get the <SCALEKIT_ACCESS_TOKEN> in case of HTTP requests.
    POST /organizations/{organization_id}/clients
    # 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)
    }'
    Sample response
    Sample response
    {
    "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..."
    }
  3. API client requests Bearer access token for API authentication

    Section titled “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:

    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>" \

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

    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

    Section titled “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.

      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());
      });
      });
      }
    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).

      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');
      }
      }

    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

    Section titled “Register API client’s scopes ”

    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.

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

    Register an API client with specific scopes
    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
    }'
    Sample response
    Sample response
    {
    "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": "test_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcfa6reKG1kKNVbqBKW4H5Ctmb5UZ0X"
    }

    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.

  6. 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.

    Example decoded access token
    {
    "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"
    }

    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.

    Example Express.js middleware for scope validation
    27 collapsed lines
    // 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');
    }
    }