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
- Prerequisites
- Creating a Service Account
- Obtaining JWT Tokens
- Using JWT Tokens in API Requests
- Token Details
- Service Account Management
- Troubleshooting
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:
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¶
-
Secure Storage: Store
client_secretin environment variables or secret management systems (e.g., AWS Secrets Manager, HashiCorp Vault) -
Token Caching: Cache and reuse JWT tokens until they expire to reduce token requests
-
Least Privilege: Assign service accounts to groups with minimal required permissions
-
Regular Rotation: Rotate client secrets periodically (e.g., every 90 days)
-
Monitoring: Log and monitor service account usage for security auditing
-
Error Handling: Implement retry logic with exponential backoff for token requests
-
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