API Token Authentication Guide
Audience: maintainers configuring service-account and token-based ingestion for HPC or privileged automation.
Overview
API token authentication enables secure programmatic access to ingestion endpoints from external HPC systems without requiring browser-based GitHub OAuth flows.
Architecture
Authentication Flow
- OAuth First: The system first attempts OAuth/JWT authentication (existing behavior)
- API Token Fallback: If OAuth fails, the system checks for a Bearer token in the Authorization header
- Unified Resolution: Both methods resolve through the same
current_active_userdependency
Security Features
- SHA256 Token Hashing: Raw tokens are hashed before storage
- Constant-Time Comparison: Token validation uses
hmac.compare_digestto prevent timing attacks - One-Time Exposure: Raw tokens are returned only once at creation time
- 32+ Bytes Entropy: Tokens are generated using
secrets.token_urlsafe(32) - Token Prefix: All tokens start with
sbk_for operational clarity - Expiration Support: Tokens can have optional expiration dates
- Revocation Support: Tokens can be revoked by administrators
Usage
Creating an API Token (Admin Only)
curl -X POST https://api.simboard.org/api/v1/tokens \
-H "Authorization: Bearer <ADMIN_TOKEN_OR_OAUTH>" \
-H "Content-Type: application/json" \
-d '{
"name": "HPC Ingestion Bot",
"user_id": "service-account-uuid",
"expires_at": "2027-12-31T23:59:59Z"
}'
Response (201 Created):
{
"id": "token-uuid",
"name": "HPC Ingestion Bot",
"token": "sbk_xxxxxxxxxxxxxxxxxxxxx",
"created_at": "2026-02-19T00:00:00Z",
"expires_at": "2027-12-31T23:59:59Z"
}
Possible Status Codes:
201 Created: Token successfully created403 Forbidden: Only administrators can create tokens404 Not Found: User with the specified user_id does not exist
⚠️ Important: Save the token value immediately. It will never be shown again.
Using an API Token for Ingestion
Path-Based Ingestion
curl -X POST https://api.simboard.org/api/v1/ingestions/from-path \
-H "Authorization: Bearer sbk_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"archive_path": "/hpc/storage/simulations/archive.tar.gz",
"machine_name": "perlmutter",
"hpc_username": "johndoe"
}'
Automated HPC Upload Ingestion
curl -X POST https://api.simboard.org/api/v1/ingestions/from-hpc-upload \
-H "Authorization: Bearer sbk_xxxxxxxxxxxxxxxxxxxxx" \
-F "file=@case-a.tar.gz" \
-F "machine_name=perlmutter" \
-F "case_path=/lcrc/group/e3sm/PERF_Chrysalis/performance_archive/case_a" \
-F "processed_execution_ids=100.1-1" \
-F "processed_execution_ids=101.1-1" \
-F "hpc_username=johndoe"
Browser or Manual Upload
curl -X POST https://api.simboard.org/api/v1/ingestions/from-upload \
-H "Authorization: Bearer sbk_xxxxxxxxxxxxxxxxxxxxx" \
-F "file=@archive.tar.gz" \
-F "machine_name=perlmutter" \
-F "hpc_username=johndoe"
Listing API Tokens (Admin Only)
curl -X GET https://api.simboard.org/api/v1/tokens \
-H "Authorization: Bearer <ADMIN_TOKEN_OR_OAUTH>"
Revoking an API Token (Admin Only)
curl -X DELETE https://api.simboard.org/api/v1/tokens/{token_id} \
-H "Authorization: Bearer <ADMIN_TOKEN_OR_OAUTH>"
Service Accounts
Creating a Service Account User
Service accounts are users with role=SERVICE_ACCOUNT, modeled through the existing UserRole enum. No separate boolean or identity flag is used.
Via CLI script (recommended):
# This script will prompt for admin login (username/password) to obtain a JWT.
uv run python -m app.scripts.users.provision_service_account \
--base-url https://api.simboard.org \
--service-name hpc-ingestion-bot
# With expiration:
uv run python -m app.scripts.users.provision_service_account \
--base-url https://api.simboard.org \
--service-name hpc-ingestion-bot \
--expires-in-days 365
Note: The script will prompt for your admin username and password to authenticate and obtain a JWT. The --admin-token argument is not used.
Via REST API:
# Step 1: Create service account user
curl -X POST https://api.simboard.org/api/v1/tokens/service-accounts \
-H "Authorization: Bearer <ADMIN_TOKEN_OR_OAUTH>" \
-H "Content-Type: application/json" \
-d '{"service_name": "hpc-ingestion-bot"}'
# Step 2: Create API token (use user_id from step 1)
curl -X POST https://api.simboard.org/api/v1/tokens \
-H "Authorization: Bearer <ADMIN_TOKEN_OR_OAUTH>" \
-H "Content-Type: application/json" \
-d '{"name": "hpc-ingestion-bot-token", "user_id": "<USER_ID>"}'
Constraints:
SERVICE_ACCOUNTusers authenticate via API tokens only.- They cannot log in via GitHub OAuth.
- Only
SERVICE_ACCOUNTusers may use Bearer token authentication.
Recommended Service Accounts
Note: Domain is set via an environment variable.
hpc-ingestion-bot@{settings.domain}- For HPC ingestion jobsci-integration-bot@{settings.domain}- For CI/CD pipelinesmonitoring-bot@{settings.domain}- For monitoring and health checks
HPC Username Provenance
The hpc_username field captures the identity of the user who triggered the ingestion job on the HPC system. This is:
- Trusted Input: Provided by trusted HPC ingestion jobs
- Informational Only: Used for provenance and future ownership enforcement
- Not Validated: Not checked against GitHub or other authentication systems
- Optional: Can be omitted if not applicable
Examples
Python Example
import requests
API_BASE = "https://api.simboard.org/api/v1"
API_TOKEN = "sbk_xxxxxxxxxxxxxxxxxxxxx"
headers = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
}
# Ingest from path
response = requests.post(
f"{API_BASE}/ingestions/from-path",
headers=headers,
json={
"archive_path": "/path/to/archive.tar.gz",
"machine_name": "perlmutter",
"hpc_username": "johndoe"
}
)
print(response.json())
For automated archive uploads from remote HPC sites, post multipart form data to
/ingestions/from-hpc-upload with exactly one case archive per request plus the
stable case_path and repeated processed_execution_ids fields. Keep
/ingestions/from-upload for browser/manual uploads only.
Before calling /ingestions/from-hpc-upload, the client must determine the full
discovered execution ID set for that case and send it as repeated
processed_execution_ids form fields. SimBoard's provided upload tooling derives
that set during its pre-upload scan step using the existing parser logic. Custom
clients may use main_parser or equivalent logic that produces the same
per-case execution IDs; main_parser is not a protocol requirement.
Bash Script Example
#!/bin/bash
API_BASE="https://api.simboard.org/api/v1"
API_TOKEN="sbk_xxxxxxxxxxxxxxxxxxxxx"
ARCHIVE_PATH="/hpc/storage/archive.tar.gz"
MACHINE="perlmutter"
HPC_USER="${USER}"
curl -X POST "${API_BASE}/ingestions/from-path" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"archive_path\": \"${ARCHIVE_PATH}\",
\"machine_name\": \"${MACHINE}\",
\"hpc_username\": \"${HPC_USER}\"
}"
For automated upload mode, package one case directory per archive and call:
curl -X POST "${API_BASE}/ingestions/from-hpc-upload" \
-H "Authorization: Bearer ${API_TOKEN}" \
-F "file=@case-a.tar.gz" \
-F "machine_name=${MACHINE}" \
-F "case_path=/remote/performance_archive/case_a" \
-F "processed_execution_ids=100.1-1" \
-F "processed_execution_ids=101.1-1"
Database Schema
ApiToken Model
class ApiToken(Base):
id: UUID # Primary key
name: str # Human-readable identifier
token_hash: str # SHA256 hash of raw token
user_id: UUID # Foreign key to users.id
created_at: datetime # Token creation timestamp
expires_at: datetime | None # Optional expiration
revoked: bool # Revocation flag
User Model Updates
UserRoleenum extended withSERVICE_ACCOUNT- API tokens are associated only with users whose role is
SERVICE_ACCOUNT
Simulation Model Updates
- Added
hpc_username: str | Nonefield
Migration
Run the migration to add API token support:
make backend-upgrade
This will:
- Add
SERVICE_ACCOUNTenum value touser_roletype - Add
hpc_usernamecolumn tosimulationstable - Create
api_tokenstable with proper indexes and constraints
Testing
# Run all token-related tests
cd backend && uv run pytest tests/features/user/test_token_auth.py -v
cd backend && uv run pytest tests/features/user/test_token_api.py -v
cd backend && uv run pytest tests/features/ingestion/test_token_ingestion.py -v
Troubleshooting
Token Not Working
- Check if token is revoked:
GET /api/v1/tokens - Check if token is expired
- Verify token format: Must start with
sbk_ - Ensure Bearer scheme:
Authorization: Bearer sbk_xxx - Check that associated user is active and has
role=SERVICE_ACCOUNT
403 Forbidden
- Only administrators can create, list, and revoke tokens
- Verify user role is
ADMIN
401 Unauthorized
- Token may be invalid, revoked, or expired
- Check Authorization header format
- Verify the token is associated with a
SERVICE_ACCOUNTuser - Verify OAuth is not interfering (OAuth takes precedence)
References
- Token Auth Implementation:
backend/app/features/user/token_auth.py - Token API Endpoints:
backend/app/features/user/token_api.py - Authentication Dependency:
backend/app/features/user/manager.py - Migration:
backend/migrations/versions/20260219_000000_add_api_token_authentication.py