Skip to content

Service Account Authentication Guide

This guide provides comprehensive instructions for authenticating with Centrali using service accounts and obtaining JWT tokens for API requests.

Table of Contents


Overview

Centrali uses OAuth 2.0 client credentials flow for service account authentication. Service accounts are machine-to-machine clients that can authenticate and obtain JWT tokens to access Centrali APIs programmatically.

Authentication Flow: 1. Create a service account to obtain client_id and client_secret 2. Exchange these credentials for a JWT access token 3. Use the JWT token in API requests via the Authorization header


Prerequisites

Before you can create a service account, you need:

  • Workspace Access: You must belong to a workspace
  • User Authentication: A valid user JWT token to authenticate workspace-scoped API calls
  • Workspace Slug: The slug identifier for your workspace
  • Permissions: Appropriate permissions to create service accounts in your workspace

Creating a Service Account

Step 1: Obtain User JWT Token

First, you need to authenticate as a user through the standard OIDC flow. This is typically done through the Centrali web interface. Once authenticated, you'll have a user JWT token.

Step 2: Create a Service Account

Endpoint: POST /workspace/{workspaceSlug}/api/v1/service-accounts

Headers:

Authorization: Bearer <your-user-jwt-token>
Content-Type: application/json

Request Body:

{
  "name": "My API Service Account",
  "description": "Service account for production API integration",
  "grant_types": ["client_credentials"],
  "scope": "openid"
}

Parameters: - name (required): A descriptive name for your service account - description (optional): A detailed description of the service account's purpose and scope - grant_types (optional): Array of grant types, defaults to ["client_credentials"] - scope (optional): OAuth scopes, defaults to "openid"

Example using curl:

curl -X POST "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production API Client",
    "description": "Main service account for production API access",
    "grant_types": ["client_credentials"],
    "scope": "openid"
  }'

Response (201 Created):

{
  "id": 123,
  "clientId": "ci_a1b2c3d4e5f6g7h8i9j0",
  "clientSecret": "sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "name": "Production API Client",
  "description": "Main service account for production API access",
  "workspace": "acme-corp",
  "grant_types": ["client_credentials"],
  "scope": "openid",
  "createdAt": "2025-01-15T10:30:00Z",
  "createdBy": "user-uuid-123"
}

⚠️ IMPORTANT: - The clientSecret is shown only once during creation - Store it securely - you cannot retrieve it again - If lost, you'll need to rotate the secret via the rotation endpoint


Obtaining JWT Tokens

Once you have a service account, use the OAuth 2.0 client credentials flow to obtain JWT tokens.

Token Endpoint

Endpoint: POST /oidc/token

Content-Type: application/x-www-form-urlencoded

Request Parameters: - grant_type: Must be client_credentials - client_id: Your service account client ID (starts with ci_) - client_secret: Your service account client secret (starts with sk_) - scope (optional): Requested scopes, e.g., openid

Example: Obtain JWT Token

Using curl:

curl -X POST "https://auth.centrali.localhost/oidc/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=ci_a1b2c3d4e5f6g7h8i9j0" \
  -d "client_secret=sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" \
  -d "scope=openid"

Using Python:

import requests

token_url = "https://auth.centrali.localhost/oidc/token"

payload = {
    "grant_type": "client_credentials",
    "client_id": "ci_a1b2c3d4e5f6g7h8i9j0",
    "client_secret": "sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "scope": "openid"
}

response = requests.post(
    token_url,
    data=payload,
    headers={"Content-Type": "application/x-www-form-urlencoded"}
)

token_data = response.json()
access_token = token_data["access_token"]
print(f"Access Token: {access_token}")

Using Node.js:

const axios = require('axios');
const qs = require('querystring');

const tokenUrl = 'https://auth.centrali.localhost/oidc/token';

const payload = {
  grant_type: 'client_credentials',
  client_id: 'ci_a1b2c3d4e5f6g7h8i9j0',
  client_secret: 'sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
  scope: 'openid'
};

axios.post(tokenUrl, qs.stringify(payload), {
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
})
.then(response => {
  const accessToken = response.data.access_token;
  console.log('Access Token:', accessToken);
})
.catch(error => {
  console.error('Error:', error.response?.data || error.message);
});

Successful Response

Status: 200 OK

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJzdWIiOiJjaV9hMWIyYzNkNGU1ZjZnN2g4aTlqMCIsImlzcyI6Imh0dHBzOi8vYXV0aC5jZW50cmFsaS5sb2NhbGhvc3QiLCJhdWQiOiJodHRwczovL2NlbnRyYWxpLmxvY2FsaG9zdCIsImlhdCI6MTcwNTMyNjAwMCwiZXhwIjoxNzA1MzUxMjAwLCJ3b3Jrc3BhY2UiOiJhY21lLWNvcnAiLCJpc1NlcnZpY2VBY2NvdW50IjoidHJ1ZSIsImdyb3VwcyI6WyJkZXZlbG9wZXJzIl19.signature",
  "token_type": "Bearer",
  "expires_in": 25200,
  "scope": "openid"
}

Response Fields: - access_token: The JWT token to use for API authentication - token_type: Always Bearer - expires_in: Token validity in seconds (7 hours = 25200 seconds) - scope: Granted scopes


Using JWT Tokens in API Requests

Once you have a JWT token, include it in the Authorization header of all API requests.

Example: List Structures

curl -X GET "https://centrali.localhost/workspace/acme-corp/api/v1/structures" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

Example: Create a Record

curl -X POST "https://centrali.localhost/workspace/acme-corp/api/v1/records" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "recordSlug": "products",
    "data": {
      "name": "Laptop Pro",
      "price": 1299.99,
      "inStock": true
    }
  }'

Token Details

Token Lifetime

  • Access Token: 7 hours (25,200 seconds)
  • Refresh Tokens: Not available for service accounts (client credentials flow)
  • Token Reuse: Store and reuse tokens until they expire to reduce load

Token Claims

Service account JWT tokens include the following claims:

{
  "sub": "ci_a1b2c3d4e5f6g7h8i9j0",           // Client ID (subject)
  "iss": "https://auth.centrali.localhost",    // Issuer
  "aud": "https://centrali.localhost",         // Audience
  "iat": 1705326000,                           // Issued at (Unix timestamp)
  "exp": 1705351200,                           // Expiration (Unix timestamp)
  "workspace": "acme-corp",                    // Workspace slug
  "isServiceAccount": "true",                  // Service account flag
  "groups": ["developers", "api-users"]        // Assigned groups
}

Token Validation

Centrali services validate JWT tokens by: 1. Verifying the signature using the issuer's public keys (JWKS) 2. Checking token expiration (exp claim) 3. Validating the issuer (iss claim) 4. Ensuring the token has required claims (workspace, isServiceAccount)


Service Account Management

List Service Accounts

curl -X GET "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts" \
  -H "Authorization: Bearer <user-jwt-token>"

Query Parameters: - page (optional): Page number for pagination - limit (optional): Results per page

Get a Service Account

curl -X GET "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts/123" \
  -H "Authorization: Bearer <user-jwt-token>"

Update Service Account Name

curl -X PUT "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts/123/name" \
  -H "Authorization: Bearer <user-jwt-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Service Account Name"
  }'

Update Service Account Description

curl -X PUT "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts/123/description" \
  -H "Authorization: Bearer <user-jwt-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Updated description for this service account"
  }'

Rotate Client Secret

If your client secret is compromised or you need to rotate it for security:

curl -X POST "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts/123/rotate" \
  -H "Authorization: Bearer <user-jwt-token>"

Response:

{
  "id": 123,
  "clientId": "ci_a1b2c3d4e5f6g7h8i9j0",
  "clientSecret": "sk_newSecret0123456789abcdef0123456789abcdef0123456789abcdef0123",
  "name": "Production API Client",
  "workspace": "acme-corp",
  "updatedAt": "2025-01-16T14:20:00Z"
}

⚠️ Important: The old secret is immediately invalidated. Update all systems using the old secret.

Revoke a Service Account

Temporarily disable a service account without deleting it:

curl -X POST "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts/123/revoke" \
  -H "Authorization: Bearer <user-jwt-token>"

Delete a Service Account

Permanently delete a service account:

curl -X DELETE "https://auth.centrali.localhost/workspace/acme-corp/api/v1/service-accounts/123" \
  -H "Authorization: Bearer <user-jwt-token>"

Troubleshooting

Error: "Invalid client credentials"

Cause: The client_id or client_secret is incorrect.

Solution: - Verify you're using the correct credentials - Ensure there are no extra spaces or line breaks - Check that the service account hasn't been revoked or deleted - If the secret is lost, rotate it to generate a new one

Error: "Token expired"

Cause: Your JWT token has expired (lifetime: 7 hours).

Solution: - Request a new token using the token endpoint - Implement automatic token refresh in your application

Error: "Forbidden: Invalid API Key" or "Unauthorized"

Cause: - Missing Authorization header - Malformed JWT token - Token belongs to a different workspace

Solution: - Ensure you're including the Authorization: Bearer <token> header - Verify the token is for the correct workspace - Check that your service account has the necessary permissions

Error: "Service account not found"

Cause: The service account was deleted or doesn't exist.

Solution: - Verify the service account ID - List all service accounts to confirm it exists - Create a new service account if necessary

Token Claims Missing Workspace

Cause: Service account wasn't properly created with workspace association.

Solution: - Ensure you created the service account via the workspace-scoped endpoint - Recreate the service account using the correct endpoint


Best Practices

  1. Secure Storage: Store client_secret in environment variables or secret management systems (e.g., AWS Secrets Manager, HashiCorp Vault)

  2. Token Caching: Cache and reuse JWT tokens until they expire to reduce token requests

  3. Least Privilege: Assign service accounts to groups with minimal required permissions

  4. Regular Rotation: Rotate client secrets periodically (e.g., every 90 days)

  5. Monitoring: Log and monitor service account usage for security auditing

  6. Error Handling: Implement retry logic with exponential backoff for token requests

  7. Multiple Accounts: Use separate service accounts for different applications or environments


Complete Example: Python Script

Here's a complete example that obtains a token and makes an API request:

import requests
import time
from datetime import datetime, timedelta

class CentraliClient:
    def __init__(self, client_id, client_secret, workspace_slug, base_url="https://centrali.localhost"):
        self.client_id = client_id
        self.client_secret = client_secret
        self.workspace_slug = workspace_slug
        self.base_url = base_url
        self.token_url = f"https://auth.centrali.localhost/oidc/token"
        self.access_token = None
        self.token_expiry = None

    def get_access_token(self):
        """Obtain a new JWT access token"""
        if self.access_token and self.token_expiry and datetime.now() < self.token_expiry:
            return self.access_token

        payload = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": "openid"
        }

        response = requests.post(
            self.token_url,
            data=payload,
            headers={"Content-Type": "application/x-www-form-urlencoded"}
        )

        if response.status_code != 200:
            raise Exception(f"Failed to obtain token: {response.text}")

        token_data = response.json()
        self.access_token = token_data["access_token"]
        expires_in = token_data.get("expires_in", 25200)
        self.token_expiry = datetime.now() + timedelta(seconds=expires_in - 300)  # Refresh 5 min early

        return self.access_token

    def make_request(self, method, endpoint, **kwargs):
        """Make an authenticated API request"""
        token = self.get_access_token()
        headers = kwargs.pop("headers", {})
        headers["Authorization"] = f"Bearer {token}"

        url = f"{self.base_url}/workspace/{self.workspace_slug}/api/v1{endpoint}"
        response = requests.request(method, url, headers=headers, **kwargs)

        return response

# Usage
client = CentraliClient(
    client_id="ci_a1b2c3d4e5f6g7h8i9j0",
    client_secret="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    workspace_slug="acme-corp"
)

# List structures
response = client.make_request("GET", "/structures")
structures = response.json()
print(f"Found {len(structures['data'])} structures")

# Create a record
response = client.make_request(
    "POST",
    "/records",
    json={
        "recordSlug": "products",
        "data": {
            "name": "Laptop Pro",
            "price": 1299.99
        }
    }
)
record = response.json()
print(f"Created record: {record['id']}")

Support

For additional help: - Check the Centrali API Documentation - Review the OAuth 2.0 Client Credentials Flow - Contact your workspace administrator for permission issues