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/loginand use the session cookie for subsequent requestsAPI key authentication – Pass an
X-API-Keyheader (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 (Nonefor 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 |
|
Get CSRF token (sets cookie) |
POST |
|
Login with |
POST |
|
Logout and clear session |
GET |
|
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 |
|---|---|---|---|
|
string |
Yes |
URL identifier for the trust mark type |
|
boolean |
No |
Auto-renew trust marks of this type (default: true) |
|
integer |
No |
Validity period in hours (default: 8760 = 1 year) |
|
integer |
No |
Hours before expiry to trigger renewal (default: 48) |
|
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 |
|---|---|---|
|
integer |
Maximum number of results (default: 100) |
|
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 |
|---|---|---|---|
|
boolean |
No |
Whether trust marks of this type auto-renew |
|
integer |
No |
Validity period in hours |
|
integer |
No |
Hours before expiry to trigger renewal |
|
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 |
|---|---|---|---|
|
integer |
Yes |
Trust Mark Type ID |
|
string |
Yes |
Entity ID (URL) to issue the trust mark to |
|
boolean |
No |
Override auto-renewal setting (defaults to trust mark type value) |
|
integer |
No |
Override validity period in hours (cannot exceed trust mark type value) |
|
integer |
No |
Override renewal time in hours (cannot exceed trust mark type value) |
|
boolean |
No |
Whether the trust mark is active (defaults to trust mark type value) |
|
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 |
|---|---|---|---|
|
boolean |
No |
Whether the trust mark auto-renews |
|
boolean |
No |
Whether the trust mark is active |
|
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 |
|---|---|---|---|
|
string |
Yes |
Entity identifier URL |
|
object |
Yes |
Entity metadata (from their entity configuration) |
|
object |
Yes |
Metadata to merge/override (TA-enforced values). Pass |
|
object |
Yes |
Entity’s public keys (JWKS) |
|
string |
No |
Required trust mark type URL for this subordinate |
|
integer |
No |
Statement validity in hours (cannot exceed system default) |
|
boolean |
No |
Auto-renew the subordinate statement (default: true) |
|
boolean |
No |
Whether the subordinate is active (default: true) |
|
object |
No |
Extra claims for the subordinate statement |
Validation:
The API will:
Fetch the entity’s
/.well-known/openid-federationconfigurationVerify the entity configuration signature using provided JWKS
Check that the TA is in the entity’s
authority_hintsApply metadata policy validation
Create and sign the subordinate statement
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 |
|---|---|---|---|
|
object |
Yes |
Entity metadata |
|
object |
Yes |
TA-enforced metadata overrides. Pass |
|
object |
Yes |
Entity’s public keys (JWKS) |
|
string |
No |
Required trust mark type URL |
|
integer |
No |
Statement validity in hours (cannot exceed system default) |
|
boolean |
No |
Auto-renew the subordinate statement (default: true) |
|
boolean |
No |
Whether the subordinate is active (default: true) |
|
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 |
|---|---|---|---|
|
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:
subandiss: Trust Anchor entity IDiatandexp: Issuance and expiry timestampsjwks: Trust Anchor’s public keysmetadata.federation_entity: Federation endpointstrust_marks: Trust marks issued to the TA itselfauthority_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 |
|---|---|---|---|
|
integer |
No |
Maximum number of results (default: 100) |
|
integer |
No |
Offset for pagination (default: 0) |
|
string |
No |
Filter by resource ( |
|
string |
No |
Filter by action ( |
|
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:
Subordinate –
registration,revocation,metadata_policy_update,metadata_update,jwks_update. When a single update touches multiple fields, the highest-priority event wins in the order listed.TrustMarkType –
trustmarktype_created,trustmarktype_updated,trustmarktype_deactivated.TrustMark –
trustmark_issued,trustmark_renewed,trustmark_revoked,trustmark_updated.
Logged Operations:
Endpoint |
Action |
|---|---|
|
CREATE |
|
UPDATE |
|
CREATE |
|
UPDATE (renew) |
|
UPDATE |
|
CREATE ( |
|
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 itemsitems: Array of results for current page