Use STS Endpoint
STS is served on port 8080 and issues scoped Caracal mandate JWTs.
Applications normally call it through the OAuth or application SDK. The public exchange follows RFC 8693 framing with Caracal parameters; internal policy and signing-key endpoints are service-to-service only.
Public Endpoints
Section titled “Public Endpoints”| Method | Path | Purpose |
|---|---|---|
POST | /oauth/2/token | OAuth token exchange for resource, session, Gateway, or delegated mandates. |
GET | /.well-known/jwks.json?zone_id={zone} | Public signing keys for mandate verification, scoped per zone. |
GET | /approvals/{id} | Approval hold state; ?wait={seconds} long-polls until the state changes. |
POST | /approvals/{id}/decision | Federated user approval decision, authenticated with the Federated user’s session mandate. |
GET | /health | Liveness check. |
GET | /ready | Readiness check. |
GET | /metrics | Prometheus metrics. |
GET | /metrics.json | JSON metrics. |
Token Exchange Request
Section titled “Token Exchange Request”POST /oauth/2/token accepts body-only application/x-www-form-urlencoded parameters. Query parameters, duplicate singleton fields, and other media types are rejected. Repeated resource fields are allowed; STS trims, exact-deduplicates, and sorts them before evaluation.
| Parameter | Purpose |
|---|---|
grant_type | OAuth grant type. |
subject_token, subject_token_type | Existing authority to exchange: a session mandate this STS issued. |
resource | One or more target resource identifiers. |
scope | Requested scopes. |
zone_id | Zone boundary. |
application_id | Calling application. |
client_secret | Application authentication. |
session_id, agent_session_id, delegation_edge_id | Protocol names for Authority record ID, Session ID, and Delegation ID. They are distinct identifiers. |
ttl_seconds | Requested TTL. |
approval_id | Consumes an approved Approval hold during retry. |
grant_type must be urn:ietf:params:oauth:grant-type:token-exchange. subject_token_type must match the presented token class. scope is a space-delimited string. ttl_seconds must be a positive integer within server limits. Resource identifiers, zone, application, Session, Authority-record, and Delegation bindings are validated server-side; caller-supplied identifiers never establish authority by themselves.
RFC 8693 actor_token is not supported: a request that carries one is rejected with invalid_token. Public client_assertion and client_assertion_type fields are also rejected; application authentication uses client_secret. The acting application’s identity reaches policy input as input.context.actor_claims.caracal_client_id.
A minimal application-principal exchange:
curl -s http://localhost:8080/oauth/2/token \ --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \ --data-urlencode "zone_id=$CARACAL_ZONE_ID" \ --data-urlencode "application_id=$CARACAL_APPLICATION_ID" \ --data-urlencode "client_secret=$CARACAL_APP_CLIENT_SECRET" \ --data-urlencode 'resource=resource://pipernet' \ --data-urlencode 'scope=pipernet:read'Subject Identity
Section titled “Subject Identity”Every exchange records a Subject - the identity work is done for. STS never invents one: the Subject is the sub claim of the presented subject_token, or the authenticated application’s own id when no subject token is presented. The Subject is therefore one of two kinds: an application Subject (the default) or a Federated user. Two subject token classes are accepted:
urn:ietf:params:oauth:token-type:access_token- a session mandate this STS issued for the same zone; resource mandates are rejected as subject tokens (RFC 8693 subject-confusion mitigation). The chain keeps the Subject kind of the presented mandate.urn:ietf:params:oauth:token-type:id_token- Federated user federation: an end user’s identity token from a registered Federated user issuer (Admin API resource:subject-issuers). The application authenticates with its client credentials and relays the token; STS verifies it against the issuer’s JWKS and creates an Authority record whosesubis copied verbatim. Caracal never authenticates the user itself. A federation exchange names no resources and mints no scopes: the record is an identity and revocation anchor, not resource authority.
A Federated user’s identifier is opaque to Caracal. It may be a user ID, UUID, employee ID, or customer ID. STS requires only that the same identity always presents the same value. The Authority record stores that sub; provider connections, approval bindings, revocation, and audit can attribute activity to it. Scope authority is decided by application bindings, resource grants, Delegation narrowing, and label confinement - never by the Subject identifier or kind.
Minted mandates carry the Subject kind as the sub_type claim (application or user), and audit decision events record it as subject_kind, so downstream systems can distinguish the kinds without parsing identity out of identifiers.
When an SDK starts a Session with subjectAuthorityRecordId, subject_authority_record_id, or SubjectAuthorityRecordID, that field supplies a Subject authority record ID. It links the Session to the Authority record for attribution and lifecycle. It does not, by itself, make later resource mandates carry that record’s Federated user sub.
Federated User Identifier Guarantees
Section titled “Federated User Identifier Guarantees”Any stable identifier format a trusted issuer signs is accepted - UUIDs, emails, numeric ids, prefixed ids such as auth0|507f..., URIs, ARNs, or unicode names. Caracal never parses, normalizes, case-folds, or trims the value: it is compared byte-for-byte everywhere, so two byte-distinct values are two Subjects, and the recorded value is always byte-identical to what the issuer signed. A federation exchange rejects only values that are unsafe to store, index, log, or display:
| Rejected | Reason |
|---|---|
| Empty, or longer than 512 bytes | Bounded storage, indexing, and display. |
Inside the reserved caracal: namespace | Anchors Caracal-internal Subject sentinels such as the shared provider connection; an external issuer must never mint one. |
| Invalid UTF-8, or containing U+FFFD | A replacement character would let two byte-distinct upstream values collapse into one stored identity. |
| Control characters (including NUL, newlines, escapes) | Log forging and storage safety. |
| Bidirectional-override characters (U+202A-U+202E, U+2066-U+2069) | Visual reordering of surrounding UI text. |
| Leading or trailing whitespace | Rejected rather than trimmed, preserving the byte-exact identity. |
Responses
Section titled “Responses”Successful exchanges return an OAuth-style token response with access_token, token_type, expires_in, and related fields. When policy gates the mint on human approval, the STS answers 401 with error: interaction_required plus the hold’s approval_id, approval_type, state, tier, binding, and approval_expires_at, so the caller can wait on /approvals/{id} and retry. Other errors use the shared error and error_description shape.
{ "access_token": "<mandate JWT>", "token_type": "Bearer", "expires_in": 900, "target_resources": ["resource://pipernet"]}Public clients never receive Gateway’s private upstream directive or provider credential.
Retry and Idempotency
Section titled “Retry and Idempotency”The exchange endpoint has no public idempotency key. SDK OAuth clients make one network attempt because the STS may mint before a response is lost. Do not automatically replay a timed-out exchange. Approval retry is different: after an approved hold, send the exact approval_id with the same bound resource/scope request; consumed, rejected, expired, or mismatched holds fail.
GET /approvals/{id}?wait={seconds} returns pending, approved, rejected, expired, or consumed; the server bounds the wait. The Federated user decision endpoint requires that user’s session mandate and the exact approval binding.
Internal Endpoints
Section titled “Internal Endpoints”| Method | Path | Purpose |
|---|---|---|
POST | /internal/policy/simulate | Simulate policy input. |
GET | /internal/policy/status/{zoneID} | Inspect policy load status. |
POST | /internal/zones/{zoneID}/signing-key/rotate | Rotate zone signing key. |
Internal endpoints are for service/admin integration, not normal application traffic.
They require the service authentication configured for API-to-STS calls and carry no external stability guarantee.
Next Step
Section titled “Next Step”Use Proxy Through Gateway to understand how Gateway validates inbound authority and exchanges with STS per request.

