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:
-
Installation
Section titled “Installation”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-pythonAlternatively, you can use the REST APIs directly.
-
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}/clientsendpoint 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/authenticationcurl -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..."}from scalekit.v1.clients.clients_pb2 import OrganizationClientorg_id = "<SCALEKIT_ORGANIZATION_ID>"api_client = OrganizationClient(name="GitHub Actions Deployment Service", # A descriptive name for the API clientdescription="Service account for GitHub Actions to deploy applications to production", # A detailed explanation of the client's purpose and usagecustom_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 needsaudience=["deployment-api.acmecorp.com"], # List of API endpoints this client will accessexpiry=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 applicationclient_id = response.client.client_idplain_secret = response.plain_secretSample 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_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcf.."} -
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_idandclient_secretissued 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 theaccess_token.The client sends a POST request to the
/oauth/tokenendpoint: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>" \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.
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
Authorizationheader of subsequent requests to your API server. Your API server validates these tokens before granting access to resources. -
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:
-
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());});});}# This is automatically handled by Scalekit SDK -
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');}}# Token from the incoming API request's authorization headertoken = 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.
-
-
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:applicationsandread: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"}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 needsconst 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 clientconst 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 securelyconsole.log('Created API client:', clientId);} catch (error) {console.error('Error creating API client:', error);}}createAPIClient();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 osfrom 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 needsfrom scalekit.v1.clients.clients_pb2 import OrganizationClientclient_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 clientresponse = 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_idplain_secret = response.plain_secret# Provide these credentials to your customer securelyprint("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 identifierplain_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_secretis never shown again after creation. -
Verify API client’s scopes
Section titled “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
Authorizationheader. 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
scopesfield.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
scopesarray 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 verificationconst 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 kidconst decoded = jwt.decode(token, { complete: true });const publicKey = await getPublicKey(decoded.header);// Verify the token signature and claimsconst verified = jwt.verify(token, publicKey, {algorithms: ['RS256'],complete: true});const decodedToken = verified.payload;// Check if the API client app has the required scopeconst requiredScope = 'deploy:applications';if (decodedToken.scopes && decodedToken.scopes.includes(requiredScope)) {// API client app has the required scope, proceed with the requestnext();} else {// API client app does not have the required scoperes.status(403).send('Forbidden: Insufficient permissions');}} catch (error) {// Token is invalid or expiredres.status(401).send('Unauthorized: Invalid token');}}Example Flask decorator for scope validation 14 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 osimport functoolsfrom scalekit import ScalekitClientfrom flask import request, jsonify# Initialize Scalekit clientscalekit_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"}), 401token = auth_header.split(' ')[1]try:# Validate the token using the Scalekit SDKclaims = scalekit_client.validate_access_token_and_get_claims(token=token)# Check if the API client app has the required scopeif required_scope in claims.get('scopes', []):# API client app has the required scopereturn f(*args, **kwargs)else:# API client app does not have the required scopereturn jsonify({"error": "Forbidden: Insufficient permissions"}), 403except Exception as e:# Token is invalid or expiredreturn jsonify({"error": "Unauthorized: Invalid token"}), 401return decorated_functionreturn decorator# Example usage in a Flask route# @app.route('/deploy', methods=['POST'])# @check_permissions('deploy:applications')# def deploy_application():# return jsonify({"message": "Deployment successful"})