Skip to content

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

  1. OAuth First: The system first attempts OAuth/JWT authentication (existing behavior)
  2. API Token Fallback: If OAuth fails, the system checks for a Bearer token in the Authorization header
  3. Unified Resolution: Both methods resolve through the same current_active_user dependency

Security Features

  • SHA256 Token Hashing: Raw tokens are hashed before storage
  • Constant-Time Comparison: Token validation uses hmac.compare_digest to 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 created
  • 403 Forbidden: Only administrators can create tokens
  • 404 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_ACCOUNT users authenticate via API tokens only.
  • They cannot log in via GitHub OAuth.
  • Only SERVICE_ACCOUNT users may use Bearer token authentication.

Note: Domain is set via an environment variable.

  • hpc-ingestion-bot@{settings.domain} - For HPC ingestion jobs
  • ci-integration-bot@{settings.domain} - For CI/CD pipelines
  • monitoring-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

  • UserRole enum extended with SERVICE_ACCOUNT
  • API tokens are associated only with users whose role is SERVICE_ACCOUNT

Simulation Model Updates

  • Added hpc_username: str | None field

Migration

Run the migration to add API token support:

make backend-upgrade

This will:

  1. Add SERVICE_ACCOUNT enum value to user_role type
  2. Add hpc_username column to simulations table
  3. Create api_tokens table 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

  1. Check if token is revoked: GET /api/v1/tokens
  2. Check if token is expired
  3. Verify token format: Must start with sbk_
  4. Ensure Bearer scheme: Authorization: Bearer sbk_xxx
  5. 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_ACCOUNT user
  • 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