API
The HTTP API exposes OAuth and OpenID discovery, token exchange, operational probes, batched authorization checks, and authenticated management operations. The machine-readable contract is openapi.yaml; when the Swagger surface is enabled, the same contract is served at /openapi.yaml and browsable at /. The bundled command-line client for this contract is documented in CTL.md.
Examples below assume:
export SERVICEAUTH_URL=http://localhost:8080
export MANAGEMENT_TOKEN='platform-jwt-for-serviceauth-management'
export APPLICATION_TOKEN='platform-jwt-for-the-application-audience'
HTTP conventions
Successful JSON responses use Content-Type: application/json. JSON request bodies are strict: unknown fields, malformed input, trailing JSON values, and bodies over the configured limit are rejected. Percent-encode identifiers placed in URL paths when necessary.
Every response includes X-Request-ID. A caller-supplied ID is retained only when it is at most 128 characters and contains letters, digits, -, _, or .; otherwise the service generates one.
Errors use this envelope:
{
"error": {
"code": "invalid_request",
"message": "description safe to return to the caller",
"request_id": "hfJ3QJeTzrr2Y7Ou"
}
}
Common statuses are 400 invalid input, 401 missing/invalid token, 403 wrong management audience or missing management permission, 404 missing catalog object, 429 rate limited, 500 internal failure, and 503 unavailable storage or authorization dependencies. An authorization denial is not an HTTP error: /v1/check returns 200 with allowed: false.
Authentication
POST /v1/check requires a platform JWT issued by this service. Subject, optional actor, audience, delegation mode, and cached audience permissions come from signed token context; caller-supplied identity fields are not authoritative.
Every management operation below /v1/manage/ requires a platform Bearer token with:
audequal to the configured management audience,serviceauth-managementby default; and- the route-family permission configured in YAML, or its conventional
management.<family>.<operation>default.
Pass tokens as Authorization: Bearer <token>. The development-only --insecure-management setting bypasses management authentication and must not be enabled in production.
Discovery, token exchange, and operations
| Method and path | Authentication | Description |
|---|---|---|
GET /.well-known/oauth-authorization-server | None | OAuth issuer, token endpoint, JWKS URI, and supported grant metadata. |
GET /.well-known/openid-configuration | None | OpenID discovery-compatible issuer, token endpoint, JWKS URI, and supported grant metadata. This service does not provide interactive OpenID Connect authorization or ID tokens. |
GET /.well-known/jwks.json | None | Active and configured inactive public verification keys. |
POST /oauth2/token | Trusted JWT in form | RFC 8693 token exchange. |
GET /health/live | None | Process liveness; returns {"status":"ok"}. |
GET /health/ready | None | Database, active-model, and signer readiness; returns 200 or 503. |
GET /metrics | None | Prometheus text metrics when enabled. |
GET / | None | Swagger UI when SERVICEAUTH_SWAGGER_UI/--swagger-ui is enabled. |
GET /openapi.yaml | None | Embedded OpenAPI document when the Swagger surface is enabled, with its servers entry set to the configured platform issuer. |
Token exchange uses application/x-www-form-urlencoded:
curl -sS "$SERVICEAUTH_URL/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode "subject_token=$EXTERNAL_JWT" \
--data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:jwt' \
--data-urlencode 'audience=documents-api'
For delegated exchange, include both actor_token and actor_token_type=urn:ietf:params:oauth:token-type:jwt. The target audience must enable delegation. The response is never cacheable:
{
"access_token": "eyJ...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 900
}
Permission checks
POST /v1/check evaluates one to the configured maximum number of checks and preserves request order and IDs.
curl -sS "$SERVICEAUTH_URL/v1/check" \
-H "Authorization: Bearer $APPLICATION_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"checks":[
{"id":"read-123","permission":"document.read","resource":{"type":"document","id":"123"}},
{"id":"edit-123","permission":"document.edit","resource":{"type":"document","id":"123"}}
]}'
{"results":[{"id":"read-123","allowed":true},{"id":"edit-123","allowed":false}]}
Each permission must exist in YAML and apply to the requested resource type. The token’s signed authorization context, rather than a request field, supplies the subject and optional actor.
Management API
All management paths share the /v1/manage/ prefix. The required permission names below are defaults; CONFIG.md describes how a deployment can map each family to a different audience-applicable permission.
Audiences
| Method and path | Permission | Result |
|---|---|---|
POST /v1/manage/audiences | management.audiences.write | Create or replace an audience; 201. |
GET /v1/manage/audiences | management.audiences.read | {"audiences":[...]}. |
GET /v1/manage/audiences/{id} | management.audiences.read | One audience. |
PATCH /v1/manage/audiences/{id} | management.audiences.write | Partial update. |
DELETE /v1/manage/audiences/{id} | management.audiences.write | Idempotent deletion; 204. |
Create or replace an audience:
curl -sS "$SERVICEAUTH_URL/v1/manage/audiences" \
-H "Authorization: Bearer $MANAGEMENT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"id":"documents-api","display_name":"Documents API","token_ttl_seconds":900,"delegation":{"enabled":true,"mode":"intersection"}}'
Delegation modes are disabled, subject, intersection, actor, and union. A patch may contain any subset of display_name, token_ttl_seconds, and delegation.
Resources and relationships
| Method and path | Permission | Result |
|---|---|---|
POST /v1/manage/resources | management.resources.write | Create a configured resource; 201. |
GET /v1/manage/resources/{type}/{id} | management.resources.read | Resource metadata and relationships. |
PATCH /v1/manage/resources/{type}/{id} | management.resources.write | Replace metadata; 200. |
DELETE /v1/manage/resources/{type}/{id} | management.resources.write | Delete catalog and authorization state; idempotent 204. |
PUT /v1/manage/resources/{type}/{id}/relationships/{relation} | management.resources.write | Add or replace relationship targets; 204. |
DELETE /v1/manage/resources/{type}/{id}/relationships/{relation} | management.resources.write | Remove targets, or all targets with no body; 204. |
Create a resource with optional relationship targets:
curl -sS "$SERVICEAUTH_URL/v1/manage/resources" \
-H "Authorization: Bearer $MANAGEMENT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"type":"document","id":"123","metadata":{"title":"Quarterly plan"},"relationships":{"parent":{"type":"folder","id":"finance"}}}'
A relationship value may be one target or an array. Relationship names, target types, cardinality, requiredness, and inherited permissions come from the active YAML schema.
Groups and memberships
| Method and path | Permission | Result |
|---|---|---|
POST /v1/manage/groups | management.groups.write | Create a group; 201. |
GET /v1/manage/groups/{id} | management.groups.read | One group. |
DELETE /v1/manage/groups/{id} | management.groups.write | Idempotent deletion; 204. |
POST /v1/manage/groups/{id}/members | management.groups.write | Add a principal or nested group; 204. |
DELETE /v1/manage/groups/{id}/members | management.groups.write | Remove an exact member; 204, or 404 if absent. |
Membership examples:
{"member":{"type":"principal","source":"corporate","subject":"alice"}}
{"member":{"type":"group","group":"engineering"}}
Both groups must exist before a nested membership is added, and cycles are rejected.
Grants
| Method and path | Permission | Result |
|---|---|---|
POST /v1/manage/grants | management.grants.write | Create a resource-scoped role grant; 201. |
GET /v1/manage/grants | management.grants.read | {"grants":[...]}. |
DELETE /v1/manage/grants/{id} | management.grants.write | Idempotent deletion; 204. |
Grant a configured role to a principal:
curl -sS "$SERVICEAUTH_URL/v1/manage/grants" \
-H "Authorization: Bearer $MANAGEMENT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"subject":{"type":"principal","source":"corporate","subject":"alice"},"role":"editor","resource":{"type":"document","id":"123"}}'
The subject may instead be a group. id is optional and generated when absent. create_resource_if_missing:true may create only a parentless application resource whose type has no required relationships; audiences and groups use their own APIs. Semantically identical retries return the existing grant.
Lifecycle and consistency
Successful management mutations are committed to PostgreSQL, reconciled synchronously to embedded OpenFGA, and recorded in the audit log before success is returned. A background reconciler retries interrupted outbox work. Deleting catalog objects removes associated grants and relationship tuples. Platform tokens retain audience-level permissions until expiry, while /v1/check evaluates current resource state.
The API never exposes the private signing key or the complete YAML configuration. The repository’s configuration reference is CONFIG.md; the runtime documentation surface at / is API-only.