Admin API Reference

The Admin Portal provides a REST API built with Django Ninja for managing Trust Anchor resources. All endpoints are prefixed with /api/v1/.

Base URL: http://localhost:8000/api/v1/

API Documentation UI: http://localhost:8000/api/v1/docs

Authentication

All /api/v1/ endpoints (except auth endpoints) require authentication. The API accepts two authentication methods:

  • Session authentication – Login via /api/v1/auth/login and use the session cookie for subsequent requests

  • API key authentication – Pass an X-API-Key header (see API Key Authentication)

Both methods grant the same access. Session auth is used by the Vue frontend; API keys are intended for scripts and integrations.

Authentication metadata

Every authenticated request records how the caller authenticated. The metadata is attached to request.auth_result and persisted by the audit log:

  • auth_method"session" or "api_key" (extensible to other backends).

  • tenant – tenant identifier carried by the API key (default: "default"). Session-based requests always use the "default" tenant.

  • api_key_name – name of the API key used (None for session auth).

CSRF is enforced for session-based POST/PUT/PATCH/DELETE requests. API-key requests bypass CSRF because they do not rely on cookies.

Auth Endpoints

Method

Endpoint

Description

GET

/api/v1/auth/csrf

Get CSRF token (sets cookie)

POST

/api/v1/auth/login

Login with {"username": "...", "password": "..."}

POST

/api/v1/auth/logout

Logout and clear session

GET

/api/v1/auth/me

Get current authenticated user info

Login Example:

# Get CSRF token
curl -c cookies.txt http://localhost:8000/api/v1/auth/csrf

# Login
curl -b cookies.txt -c cookies.txt \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <token-from-cookie>" \
  -X POST http://localhost:8000/api/v1/auth/login \
  -d '{"username": "admin", "password": "password"}'

API Key Example:

curl -H "X-API-Key: YOUR_KEY_HERE" \
     http://localhost:8000/api/v1/trustmarktypes

Trust Mark Types

Trust Mark Types define categories of trust marks that can be issued to entities.

Create Trust Mark Type

POST /api/v1/trustmarktypes

Creates a new trust mark type.

Request Body:

{
  "tmtype": "https://example.com/trustmarks/member",
  "autorenew": true,
  "valid_for": 8760,
  "renewal_time": 48,
  "active": true
}

Parameters:

Field

Type

Required

Description

tmtype

string

Yes

URL identifier for the trust mark type

autorenew

boolean

No

Auto-renew trust marks of this type (default: true)

valid_for

integer

No

Validity period in hours (default: 8760 = 1 year)

renewal_time

integer

No

Hours before expiry to trigger renewal (default: 48)

active

boolean

No

Whether this type is active (default: true)

Response (201 Created):

{
  "id": 1,
  "tmtype": "https://example.com/trustmarks/member",
  "autorenew": true,
  "valid_for": 8760,
  "renewal_time": 48,
  "active": true
}

Response (403 Forbidden): Returned if the trust mark type already exists.

Example:

curl -X POST http://localhost:8000/api/v1/trustmarktypes \
  -H "Content-Type: application/json" \
  -d '{
    "tmtype": "https://example.com/trustmarks/member",
    "valid_for": 8760,
    "autorenew": true
  }'

List Trust Mark Types

GET /api/v1/trustmarktypes

Returns a paginated list of all trust mark types.

Query Parameters:

Parameter

Type

Description

limit

integer

Maximum number of results (default: 100)

offset

integer

Offset for pagination (default: 0)

Response (200 OK):

{
  "count": 2,
  "items": [
    {
      "id": 1,
      "tmtype": "https://example.com/trustmarks/member",
      "autorenew": true,
      "valid_for": 8760,
      "renewal_time": 48,
      "active": true
    },
    {
      "id": 2,
      "tmtype": "https://example.com/trustmarks/verified",
      "autorenew": true,
      "valid_for": 720,
      "renewal_time": 48,
      "active": true
    }
  ]
}

Get Trust Mark Type by ID

GET /api/v1/trustmarktypes/{id}

Response (200 OK):

{
  "id": 1,
  "tmtype": "https://example.com/trustmarks/member",
  "autorenew": true,
  "valid_for": 8760,
  "renewal_time": 48,
  "active": true
}

Get Trust Mark Type by URL

GET /api/v1/trustmarktypes/?tmtype=https://example.com/trustmarks/member

Update Trust Mark Type

PUT /api/v1/trustmarktypes/{id}

Request Body:

{
  "autorenew": false,
  "valid_for": 720,
  "renewal_time": 24,
  "active": false
}

Parameters:

Field

Type

Required

Description

autorenew

boolean

No

Whether trust marks of this type auto-renew

valid_for

integer

No

Validity period in hours

renewal_time

integer

No

Hours before expiry to trigger renewal

active

boolean

No

Whether this type is active

All fields are optional. Only provided fields will be updated.

Trust Marks

Trust marks are issued to entities and stored as signed JWTs.

Create Trust Mark

POST /api/v1/trustmarks

Issues a new trust mark to an entity.

Request Body:

{
  "tmt": 1,
  "domain": "https://example-rp.com",
  "valid_for": 24,
  "autorenew": true,
  "additional_claims": {
    "ref": "https://github.com/example/verification"
  }
}

Parameters:

Field

Type

Required

Description

tmt

integer

Yes

Trust Mark Type ID

domain

string

Yes

Entity ID (URL) to issue the trust mark to

autorenew

boolean

No

Override auto-renewal setting (defaults to trust mark type value)

valid_for

integer

No

Override validity period in hours (cannot exceed trust mark type value)

renewal_time

integer

No

Override renewal time in hours (cannot exceed trust mark type value)

active

boolean

No

Whether the trust mark is active (defaults to trust mark type value)

additional_claims

object

No

Extra claims to include in the JWT

Response (201 Created):

{
  "id": 1,
  "tmt_id": 1,
  "domain": "https://example-rp.com",
  "expire_at": "2026-01-15T12:00:00Z",
  "autorenew": true,
  "valid_for": 24,
  "renewal_time": 48,
  "active": true,
  "mark": "eyJhbGciOiJFUzI1NiIsInR5cCI6InRydXN0LW1hcmsrand0In0...",
  "additional_claims": {
    "ref": "https://github.com/example/verification"
  }
}

Response (403 Forbidden): Returned if a trust mark already exists for this entity and type.

Example:

curl -X POST http://localhost:8000/api/v1/trustmarks \
  -H "Content-Type: application/json" \
  -d '{
    "tmt": 1,
    "domain": "https://example-rp.com"
  }'

List Trust Marks

GET /api/v1/trustmarks

Returns a paginated list of all issued trust marks.

Response (200 OK):

{
  "count": 2,
  "items": [
    {
      "id": 1,
      "tmt_id": 1,
      "domain": "https://example-rp.com",
      "expire_at": "2026-01-15T12:00:00Z",
      "autorenew": true,
      "valid_for": 8760,
      "renewal_time": 48,
      "active": true,
      "mark": "eyJ...",
      "additional_claims": null
    }
  ]
}

List Trust Marks by Entity

POST /api/v1/trustmarks/list

Request Body:

{
  "domain": "https://example-rp.com"
}

Renew Trust Mark

POST /api/v1/trustmarks/{id}/renew

Generates a new JWT with extended expiry for an existing trust mark.

Response (200 OK):

{
  "id": 1,
  "tmt_id": 1,
  "domain": "https://example-rp.com",
  "expire_at": "2027-01-15T12:00:00Z",
  "autorenew": true,
  "valid_for": 8760,
  "renewal_time": 48,
  "active": true,
  "mark": "eyJ...",
  "additional_claims": null
}

Update Trust Mark

PUT /api/v1/trustmarks/{id}

Request Body:

{
  "active": false,
  "autorenew": false,
  "additional_claims": {
    "ref": "https://updated-reference.example.com"
  }
}

Parameters:

Field

Type

Required

Description

autorenew

boolean

No

Whether the trust mark auto-renews

active

boolean

No

Whether the trust mark is active

additional_claims

object

No

Extra claims to include in the JWT (triggers re-signing)

Setting active to false revokes the trust mark. The entity will no longer appear in trust mark lists, and status checks will return “revoked”.

Changing additional_claims triggers re-signing of the trust mark JWT with the updated claims.

Subordinates

Subordinates are entities that have registered with the Trust Anchor.

Add Subordinate

POST /api/v1/subordinates

Registers a new subordinate entity.

Request Body:

{
  "entityid": "https://example-rp.com",
  "metadata": {
    "openid_relying_party": {
      "redirect_uris": ["https://example-rp.com/callback"],
      "response_types": ["code"],
      "grant_types": ["authorization_code"]
    }
  },
  "jwks": {
    "keys": [
      {
        "kty": "EC",
        "crv": "P-256",
        "x": "...",
        "y": "...",
        "kid": "key-1"
      }
    ]
  },
  "forced_metadata": {
    "openid_relying_party": {
      "application_type": "web"
    }
  },
  "valid_for": 8760,
  "autorenew": true,
  "active": true,
  "additional_claims": {
    "organization_name": "Example Corp"
  }
}

Parameters:

Field

Type

Required

Description

entityid

string

Yes

Entity identifier URL

metadata

object

Yes

Entity metadata (from their entity configuration)

forced_metadata

object

Yes

Metadata to merge/override (TA-enforced values). Pass {} if none.

jwks

object

Yes

Entity’s public keys (JWKS)

required_trustmarks

string

No

Required trust mark type URL for this subordinate

valid_for

integer

No

Statement validity in hours (cannot exceed system default)

autorenew

boolean

No

Auto-renew the subordinate statement (default: true)

active

boolean

No

Whether the subordinate is active (default: true)

additional_claims

object

No

Extra claims for the subordinate statement

Validation:

The API will:

  1. Fetch the entity’s /.well-known/openid-federation configuration

  2. Verify the entity configuration signature using provided JWKS

  3. Check that the TA is in the entity’s authority_hints

  4. Apply metadata policy validation

  5. Create and sign the subordinate statement

  6. Store in database and sync to Redis

Response (201 Created):

{
  "id": 1,
  "entityid": "https://example-rp.com",
  "metadata": {},
  "forced_metadata": {},
  "jwks": {},
  "required_trustmarks": null,
  "valid_for": 8760,
  "expire_at": "2027-01-15T12:00:00Z",
  "autorenew": true,
  "active": true,
  "additional_claims": null
}

Response (400 Bad Request): Validation errors, missing authority_hints, etc.

Response (403 Forbidden): Entity already registered.

Example:

# First, fetch the entity's configuration
ENTITY_JWT=$(curl -s https://example-rp.com/.well-known/openid-federation)

# Then register with the TA
curl -X POST http://localhost:8000/api/v1/subordinates \
  -H "Content-Type: application/json" \
  -d '{
    "entityid": "https://example-rp.com",
    "metadata": {"openid_relying_party": {"redirect_uris": ["..."]}},
    "jwks": {"keys": [{"kty": "EC", "crv": "P-256", "x": "...", "y": "..."}]},
    "forced_metadata": {}
  }'

List Subordinates

GET /api/v1/subordinates

Response (200 OK):

{
  "count": 3,
  "items": [
    {
      "id": 1,
      "entityid": "https://example-rp.com",
      "metadata": {},
      "forced_metadata": {},
      "jwks": {},
      "required_trustmarks": null,
      "valid_for": 8760,
      "expire_at": "2027-01-15T12:00:00Z",
      "autorenew": true,
      "active": true,
      "additional_claims": null
    }
  ]
}

Get Subordinate by ID

GET /api/v1/subordinates/{id}

Update Subordinate

POST /api/v1/subordinates/{id}

Updates an existing subordinate. The API will re-fetch and re-validate the entity configuration before creating a new signed statement.

Request Body:

{
  "metadata": {},
  "forced_metadata": {},
  "jwks": {},
  "required_trustmarks": null,
  "valid_for": 8760,
  "autorenew": true,
  "active": true,
  "additional_claims": null
}

Parameters:

Field

Type

Required

Description

metadata

object

Yes

Entity metadata

forced_metadata

object

Yes

TA-enforced metadata overrides. Pass {} if none.

jwks

object

Yes

Entity’s public keys (JWKS)

required_trustmarks

string

No

Required trust mark type URL

valid_for

integer

No

Statement validity in hours (cannot exceed system default)

autorenew

boolean

No

Auto-renew the subordinate statement (default: true)

active

boolean

No

Whether the subordinate is active (default: true)

additional_claims

object

No

Extra claims for the subordinate statement

Renew Subordinate

POST /api/v1/subordinates/{id}/renew

Renews a subordinate by re-fetching its entity configuration from the subordinate’s /.well-known/openid-federation endpoint, verifying the response using the stored JWKS, and generating a new signed subordinate statement with a fresh expiry.

This endpoint takes no request body. The subordinate’s forced_metadata, additional_claims, and valid_for settings are preserved from the existing record. The metadata and jwks fields are updated from the freshly fetched entity configuration.

Returns 400 if the subordinate is inactive, the entity configuration cannot be fetched or verified, or if the TA domain is no longer in the entity’s authority hints.

Example:

curl -X POST http://localhost:8000/api/v1/subordinates/1/renew \
  -H "Cookie: sessionid=..."

Fetch Entity Configuration

POST /api/v1/subordinates/fetch-config

Fetches and self-validates an entity’s OpenID Federation configuration from their /.well-known/openid-federation endpoint. Use this before adding a subordinate to inspect their metadata and keys.

If the entity publishes a jwks_uri instead of inline jwks, the endpoint automatically fetches the keys from the URI and returns them as inline jwks in the response. This ensures the response always contains populated keys regardless of how the entity publishes them.

Request Body:

{
  "url": "https://example-rp.com"
}

Parameters:

Field

Type

Required

Description

url

string

Yes

The entity URL to fetch configuration from

Response (200 OK):

{
  "metadata": {
    "openid_relying_party": {
      "redirect_uris": ["https://example-rp.com/callback"]
    }
  },
  "jwks": {
    "keys": [{"kty": "EC", "crv": "P-256", "x": "...", "y": "..."}]
  },
  "jwks_uri": null,
  "authority_hints": ["https://federation.example.com"],
  "trust_marks": [
    {
      "trust_mark_type": "https://example.com/trustmarks/member",
      "trust_mark": "eyJ..."
    }
  ]
}

The jwks_uri field is included in the response when the entity’s configuration contains one. Even when jwks_uri is present, the jwks field will be populated with the resolved keys.

Response (400 Bad Request): Connection errors, invalid URL, signature validation failure, or no OpenID Federation configuration found.

Example:

curl -X POST http://localhost:8000/api/v1/subordinates/fetch-config \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example-rp.com"}'

Server Operations

These endpoints manage the Trust Anchor’s own configuration.

Create Entity Configuration

POST /api/v1/server/entity

Creates (or recreates) the Trust Anchor’s entity configuration JWT and stores it in Redis for the /.well-known/openid-federation endpoint.

Response (201 Created):

{
  "entity_statement": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0In0..."
}

The entity statement includes:

  • sub and iss: Trust Anchor entity ID

  • iat and exp: Issuance and expiry timestamps

  • jwks: Trust Anchor’s public keys

  • metadata.federation_entity: Federation endpoints

  • trust_marks: Trust marks issued to the TA itself

  • authority_hints: Parent authorities (if intermediate)

Example:

curl -X POST http://localhost:8000/api/v1/server/entity

Create Historical Keys JWT

POST /api/v1/server/historical_keys

Reads all JSON files from the historical_keys/ directory, filters to only include keys with an exp field, creates a signed JWT, and stores it in Redis for the /historical_keys endpoint.

Response (201 Created):

{
  "message": "Historical keys JWT created with 2 keys"
}

Response (404 Not Found): Directory not found or no valid keys.

Example:

curl -X POST http://localhost:8000/api/v1/server/historical_keys

Audit Log

State-changing API operations are recorded in an immutable audit log. Read operations (GET) are not logged. See Audit Logging for how event types are derived and how the log is used operationally; this section documents the wire format.

List Audit Log Entries

GET /api/v1/auditlog

Returns a paginated list of audit log entries, newest first.

Query Parameters:

Parameter

Type

Required

Description

limit

integer

No

Maximum number of results (default: 100)

offset

integer

No

Offset for pagination (default: 0)

resource_type

string

No

Filter by resource (Subordinate, TrustMark, TrustMarkType)

action

string

No

Filter by action (CREATE or UPDATE)

event_type

string

No

Filter by spec-defined event type (see below)

Response (200 OK):

{
  "count": 1,
  "items": [
    {
      "id": 42,
      "timestamp": "2026-05-27T11:30:00Z",
      "user_id": 1,
      "username": "admin",
      "auth_method": "api_key",
      "tenant": "default",
      "ip_address": "10.0.0.5",
      "action": "UPDATE",
      "resource_type": "Subordinate",
      "resource_id": 7,
      "resource_repr": "https://example-rp.com",
      "endpoint": "/api/v1/subordinates/7",
      "http_method": "POST",
      "diff": {
        "active": {"old": true, "new": false}
      },
      "response_code": 200,
      "success": true,
      "event_type": "revocation"
    }
  ]
}

The list response omits snapshot_before and snapshot_after to keep payloads small; use the single-entry endpoint to retrieve full snapshots.

Get Audit Log Entry

GET /api/v1/auditlog/{entry_id}

Returns one audit log entry including the full before/after snapshots used to compute the diff.

Response (200 OK):

{
  "id": 42,
  "timestamp": "2026-05-27T11:30:00Z",
  "user_id": 1,
  "username": "admin",
  "auth_method": "api_key",
  "tenant": "default",
  "ip_address": "10.0.0.5",
  "action": "UPDATE",
  "resource_type": "Subordinate",
  "resource_id": 7,
  "resource_repr": "https://example-rp.com",
  "endpoint": "/api/v1/subordinates/7",
  "http_method": "POST",
  "diff": {
    "active": {"old": true, "new": false}
  },
  "response_code": 200,
  "success": true,
  "event_type": "revocation",
  "snapshot_before": {"...": "full model state before update"},
  "snapshot_after": {"...": "full model state after update"}
}

Response (404 Not Found): No entry with the given ID.

Event Types

event_type is derived automatically. Subordinate values align with the OpenID Federation Subordinate Events 1.0 specification:

  • Subordinateregistration, revocation, metadata_policy_update, metadata_update, jwks_update. When a single update touches multiple fields, the highest-priority event wins in the order listed.

  • TrustMarkTypetrustmarktype_created, trustmarktype_updated, trustmarktype_deactivated.

  • TrustMarktrustmark_issued, trustmark_renewed, trustmark_revoked, trustmark_updated.

Logged Operations:

Endpoint

Action

POST /api/v1/trustmarktypes

CREATE

PUT /api/v1/trustmarktypes/{id}

UPDATE

POST /api/v1/trustmarks

CREATE

POST /api/v1/trustmarks/{id}/renew

UPDATE (renew)

PUT /api/v1/trustmarks/{id}

UPDATE

POST /api/v1/subordinates

CREATE (registration)

POST /api/v1/subordinates/{id}

UPDATE

Example:

# Last 10 revocations
curl -H "X-API-Key: YOUR_KEY" \
     "http://localhost:8000/api/v1/auditlog?event_type=revocation&limit=10"

Error Responses

All error responses follow this format:

{
  "message": "Error description",
  "id": 0
}

Common HTTP status codes:

  • 400 Bad Request: Invalid input or validation failure

  • 403 Forbidden: Resource already exists

  • 404 Not Found: Resource not found

  • 500 Internal Server Error: Unexpected error

Pagination

List endpoints use limit-offset pagination:

GET /api/v1/trustmarks?limit=10&offset=20

Response includes:

  • count: Total number of items

  • items: Array of results for current page