Skip to content

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.

MethodPathPurpose
POST/oauth/2/tokenOAuth 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}/decisionFederated user approval decision, authenticated with the Federated user’s session mandate.
GET/healthLiveness check.
GET/readyReadiness check.
GET/metricsPrometheus metrics.
GET/metrics.jsonJSON metrics.

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.

ParameterPurpose
grant_typeOAuth grant type.
subject_token, subject_token_typeExisting authority to exchange: a session mandate this STS issued.
resourceOne or more target resource identifiers.
scopeRequested scopes.
zone_idZone boundary.
application_idCalling application.
client_secretApplication authentication.
session_id, agent_session_id, delegation_edge_idProtocol names for Authority record ID, Session ID, and Delegation ID. They are distinct identifiers.
ttl_secondsRequested TTL.
approval_idConsumes 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:

Terminal window
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'

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 whose sub is 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.

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:

RejectedReason
Empty, or longer than 512 bytesBounded storage, indexing, and display.
Inside the reserved caracal: namespaceAnchors Caracal-internal Subject sentinels such as the shared provider connection; an external issuer must never mint one.
Invalid UTF-8, or containing U+FFFDA 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 whitespaceRejected rather than trimmed, preserving the byte-exact identity.

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.

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.

MethodPathPurpose
POST/internal/policy/simulateSimulate policy input.
GET/internal/policy/status/{zoneID}Inspect policy load status.
POST/internal/zones/{zoneID}/signing-key/rotateRotate 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.

Use Proxy Through Gateway to understand how Gateway validates inbound authority and exchanges with STS per request.