Service Accounts
Service accounts provide machine-to-machine (M2M) access to STOA APIs using the OAuth2 client_credentials grant. They are ideal for CI/CD pipelines, backend services, and automation scripts.
How It Works
- Your application authenticates with Keycloak using
client_idandclient_secret - Keycloak returns a JWT access token
- The token is used as a Bearer token for API requests
- The service account inherits the RBAC role of the user who created it
Creating a Service Account
Via API
The examples below use environment variables. Set them for your STOA instance:
export STOA_API_URL="https://api.gostoa.dev" # Replace with your domain
export STOA_AUTH_URL="https://auth.gostoa.dev" # Keycloak OIDC provider
export STOA_GATEWAY_URL="https://mcp.gostoa.dev" # MCP Gateway endpoint
Self-hosted? Replace gostoa.dev with your domain.
curl -X POST "${STOA_API_URL}/v1/service-accounts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ci-pipeline",
"description": "CI/CD pipeline for API deployments"
}'
Response (credentials shown once):
{
"id": "sa_abc123",
"name": "ci-pipeline",
"client_id": "sa-ci-pipeline-a1b2c3",
"client_secret": "generated-secret-shown-once",
"created_at": "2026-02-13T10:00:00Z"
}
Save the client_secret immediately — it cannot be retrieved after creation.
Via Portal
- Navigate to Service Accounts in the Portal sidebar
- Click Create Service Account
- Enter a name and optional description
- Click Create
- Copy the
client_idandclient_secret(shown once)
Using a Service Account
Get an Access Token
TOKEN=$(curl -s -X POST "${STOA_AUTH_URL}/realms/stoa/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=sa-ci-pipeline-a1b2c3" \
-d "client_secret=your-client-secret" \
| jq -r '.access_token')
Make API Requests
# List APIs
curl -s "${STOA_API_URL}/v1/apis" \
-H "Authorization: Bearer $TOKEN"
# Call a tool via MCP Gateway
curl -s "${STOA_GATEWAY_URL}/v1/tools/payment-tool/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"input": {"amount": 100}}'
Python Example
import httpx
async def get_token(client_id: str, client_secret: str) -> str:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{auth_url}/realms/stoa/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
},
)
return response.json()["access_token"]
RBAC Inheritance
Service accounts inherit the role of the user who created them:
| Creator Role | Service Account Permissions |
|---|---|
cpi-admin | Full platform access |
tenant-admin | Own tenant: read + write |
devops | Own tenant: deploy + promote |
viewer | Read-only access |
Service accounts are scoped to the same tenant as their creator.
Secret Rotation
Regenerate a Secret
curl -X POST "${STOA_API_URL}/v1/service-accounts/{sa_id}/regenerate-secret" \
-H "Authorization: Bearer $TOKEN"
Response:
{
"client_id": "sa-ci-pipeline-a1b2c3",
"client_secret": "new-generated-secret"
}
The old secret is invalidated immediately. Update all systems using this service account before rotating.
Rotation Best Practices
- Rotate every 90 days — align with your secret rotation policy
- Update consumers first — deploy the new secret to all clients before rotating
- Use environment variables — never hardcode secrets in source code
- Monitor token failures — a spike in 401 errors after rotation indicates missed updates
Managing Service Accounts
List Service Accounts
curl "${STOA_API_URL}/v1/service-accounts" \
-H "Authorization: Bearer $TOKEN"
Delete a Service Account
curl -X DELETE "${STOA_API_URL}/v1/service-accounts/{sa_id}" \
-H "Authorization: Bearer $TOKEN"
Deletion removes the Keycloak client and invalidates all tokens immediately.
CI/CD Integration
GitHub Actions
jobs:
deploy-api:
steps:
- name: Get STOA token
run: |
TOKEN=$(curl -s -X POST "$STOA_AUTH_URL/realms/stoa/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=${{ secrets.STOA_CLIENT_ID }}" \
-d "client_secret=${{ secrets.STOA_CLIENT_SECRET }}" \
| jq -r '.access_token')
echo "STOA_TOKEN=$TOKEN" >> $GITHUB_ENV
- name: Sync API spec
run: |
curl -X POST "$STOA_API_URL/v1/apis/sync" \
-H "Authorization: Bearer $STOA_TOKEN" \
-H "Content-Type: application/json" \
-d @openapi.json
Token Caching
Access tokens have a default TTL of 5 minutes. For long-running processes, refresh the token before expiry:
# Check token expiry
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq '.exp'
Related
- Authentication Guide -- OIDC flow for interactive users
- RBAC Permissions -- Role matrix
- Keycloak Administration -- Client configuration
- Security Configuration -- JWT settings