Challenge Provider Contract
An edge can delegate browser challenges, bot scoring, and clearance signing to a service you operate. The edge still owns policy enforcement, the protected application origin, and clearance verification. The provider never receives application cookies or returns HTML for the edge to relay.
The operator pins the provider authority and an opaque start token:
UPLINK_CHALLENGE_PROVIDER_URL=https://challenge.example.com
UPLINK_CHALLENGE_PROVIDER_TOKEN=<provider-issued bearer>
The Compose setup command accepts the equivalent --challenge-provider and
--challenge-provider-token flags. Both values are required together.
Discovery
Serve this document from:
GET /.well-known/opentunnels-challenge
Example:
{
"protocolVersion": 1,
"issuer": "https://challenge.example.com",
"jwks": "/.well-known/jwks.json"
}
protocolVersion must be 1. The configured provider URL is an HTTP(S) origin,
not a URL with a path, query, or fragment. issuer is the exact clearance JWT
iss value. jwks is an absolute path on the configured authority.
Remote authorities must use HTTPS. Plain HTTP is accepted only for loopback development. The edge discovers and validates the provider at startup and fails closed when bot-protection policy requires an unavailable provider.
Start a challenge
Version 1 fixes the start endpoint:
POST /api/v1/edge/challenge/start
Authorization: Bearer <configured provider token>
Content-Type: application/json
Request:
{
"edgeId": "edge_123",
"host": "app-device.edge.example.com",
"clientIp": "203.0.113.20",
"userAgent": "Mozilla/5.0 ...",
"level": "standard",
"verifierVersion": "opentunnels-challenge-v1",
"parentOrigin": "https://app-device.edge.example.com"
}
The provider decides how to validate the opaque bearer. It should bind provider credentials to the permitted edge or tenant and rate-limit starts. The edge bounds the request, response, and call duration.
A successful response contains only a browser session URL:
{
"challengeUrl": "https://challenge.example.com/api/v1/challenge#<session-token>"
}
The URL may use any path, query, or fragment, but its origin must exactly match the configured provider authority. The fragment is convenient for an opaque, short-lived session token because browsers do not send it in the HTTP request.
Browser boundary
The edge renders its own application-origin shell and places challengeUrl in a
sandboxed provider iframe. The provider owns all interaction inside that frame.
After a successful check, it sends this message to the exact parentOrigin from
the start request:
parent.postMessage(
{
type: "opentunnels.challenge.clearance.v1",
clearance: "<signed JWT>"
},
parentOrigin
);
The edge accepts the message only from the configured provider origin and the specific iframe window. It then verifies the JWT, writes an HTTP-only host cookie, and resumes the original local path. The provider does not receive the return path.
The provider page must allow the protected edge origin to frame it. Because edge
hostnames are dynamic, providers commonly use a broad frame-ancestors https:
policy and rely on the signed session’s exact parentOrigin, exact-host clearance,
and targeted postMessage to prevent disclosure to another parent.
Clearance JWT
Publish Ed25519 keys at the discovered JWKS path using standard OKP/Ed25519
JWKs with stable kid values. Sign compact JWTs with alg: "EdDSA".
Every clearance contains:
{
"iss": "https://challenge.example.com",
"aud": "uplink",
"kind": "challenge_clearance",
"action": "challenge.host_access",
"host": "app-device.edge.example.com",
"level": "standard",
"verifierVersion": "opentunnels-challenge-v1",
"bindingSalt": "<random base64url>",
"binding": "<base64url SHA-256 binding>",
"jti": "<unique id>",
"iat": 1784740000,
"nbf": 1784739995,
"exp": 1784741800
}
The lifetime must be positive and no longer than 30 minutes. level is light,
standard, or strict; a higher level satisfies a lower edge policy.
The version 1 binding is:
base64url(
SHA-256(
"uplink/challenge/clearance-binding/v2\0" +
host + "\0" + userAgent + "\0" + bindingSalt
)
)
This binds a copied clearance to the exact host and browser User-Agent without binding it to an IP address. Providers may use IP, ASN, browser signals, or their own telemetry during scoring; those inputs are not part of the reusable token.
The edge caches JWKS keys for five minutes and refetches on an unknown kid.
During rotation, publish old and new keys together for at least the maximum
clearance lifetime plus cache overlap.
Configure and validate
docker compose run --rm setup \
--challenge-provider https://challenge.example.com \
--challenge-provider-token <provider-issued-bearer>
uplink edge add edge.example.com \
--api-key <uek-key> \
--challenge-provider https://challenge.example.com
docker compose run --rm operator doctor \
--domain edge.example.com \
--challenge-provider https://challenge.example.com
The connector records the discovered provider authority as an edge capability;
it never receives the provider token. uplink edge refresh edge.example.com
refreshes that capability after provider configuration changes.
Provider unavailability fails new challenges closed with 503. Already-issued
clearances continue to verify offline.