IP allowlist check
Checks whether an IP address is allowed by your organization's Opti ID IP allowlist, using the Cloud Platform
Services endpoint …. Try it below, then see
how your app calls it.
Response
Step 1: get a token from Azure
Step 2: send the check to the Gateway
Response headers
How your app calls it
How it works
-
Your app asks Azure for a token. It says which identity it is
(
AZURE_CLIENT_ID) and which API the token is for (OPTIMIZELY_CLOUDPLATFORM_API_RESOURCE_ID). No password or secret is needed: the web app's managed identity proves who it is. -
Your app sends the end user's IP to the Gateway
(
OPTIMIZELY_CLOUDPLATFORM_API_URL), with the token in theAuthorizationheader. - The Gateway checks the token. It must be issued for the Cloud Platform Services API, and the identity must be registered for a DXP environment. From that the Gateway knows your project, environment and organization. Your app never sends them.
-
Cloud Platform Services checks the IP against your organization's Opti ID IP
allowlist and returns
AlloworDeny.
Settings
DXP sets these on every web app. Your app reads them; it doesn't need any other configuration or secret.
| Setting | What it is | On this environment |
|---|---|---|
AZURE_CLIENT_ID |
Client ID of the web app's managed identity. The token is issued to this identity, and the Gateway uses it to find your project and environment. | not set |
OPTIMIZELY_CLOUDPLATFORM_API_RESOURCE_ID |
The API the token is for. Azure writes it into the token as the audience, and the Gateway rejects tokens for any other API. | not set |
OPTIMIZELY_CLOUDPLATFORM_API_URL |
The Gateway's address: where the check is sent. | not set |
Step 1: Get a token from Azure
The app calls the managed identity endpoint of its own web app. IDENTITY_ENDPOINT and
IDENTITY_HEADER are set by Azure App Service; the Azure Identity library reads them and makes
this call for you. The token is valid for about 24 hours, and the library caches it and renews it before it
expires, so this call isn't made on every check. With this environment's values:
GET {IDENTITY_ENDPOINT}?api-version=2019-08-01&resource={OPTIMIZELY_CLOUDPLATFORM_API_RESOURCE_ID}&client_id={AZURE_CLIENT_ID}
X-IDENTITY-HEADER: {IDENTITY_HEADER}
200 OK
{
"access_token": "eyJ0eXAiOiJKV1Qi…",
"token_type": "Bearer",
"resource": "{OPTIMIZELY_CLOUDPLATFORM_API_RESOURCE_ID}",
"expires_on": "…"
}
Step 2: Send the check to the Gateway
-
Send the end user's IP. On DXP it is in the
CF-Connecting-IPrequest header, set by Cloudflare. Don't send your server's IP or the connection's remote address. - Don't send an organization, project or instance ID. The Gateway works them out from the token.
POST {OPTIMIZELY_CLOUDPLATFORM_API_URL}/v1/environment/restrictions/checks
Authorization: Bearer eyJ0eXAiOiJKV1Qi… ← access_token from step 1
Content-Type: application/json
{"ipAddress": "203.0.113.10"} ← the end user's IP
200 OK
{"decision": "Allow", "reason": "…", "organizationId": "…", "ipAddress": "203.0.113.10"}
Step 3: Handle the response
| 200 | decision is Allow or Deny, with Turnstile's reason, the organizationId that was checked and the checked ipAddress. |
|---|---|
| 400 IP_ADDRESS_INVALID | Not a single IPv4 or IPv6 address. Ranges such as 10.0.0.0/8 are rejected. |
| 401 | The token is missing, expired or issued for another API, or the managed identity isn't registered as a Cloud Platform Services instance in Turnstile. |
| 403 ORGANIZATION_NOT_RESOLVED | The organization that owns your environment can't be found. |
| 429 | Over the rate limit (100 requests per second per caller, bursts of 200). Retry after Retry-After. |
| 503 TURNSTILE_UNAVAILABLE | No decision could be made. Your app decides whether to let the user in (fail-open) or block them (fail-closed). |
- Cloud Platform Services caches decisions: Allow for about 5 minutes, Deny for about 1 minute.
- An uncached check can take several seconds. Allow for that in your timeout, and treat a timeout like a 503.
In code
For developers: step 1 uses the Azure Identity library, step 2 is a plain HTTP call.
C# (.NET)
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Azure.Core;
using Azure.Identity; // 1.13 or later
// Step 1: token. Create the credential once (for example as a singleton) and reuse it.
var credential = new ManagedIdentityCredential(
ManagedIdentityId.FromUserAssignedClientId(Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")));
var scope = Environment.GetEnvironmentVariable("OPTIMIZELY_CLOUDPLATFORM_API_RESOURCE_ID") + "/.default";
AccessToken token = await credential.GetTokenAsync(new TokenRequestContext(new[] { scope }), cancellationToken);
// Step 2: check
var apiUrl = Environment.GetEnvironmentVariable("OPTIMIZELY_CLOUDPLATFORM_API_URL")!.TrimEnd('/');
var endUserIp = httpContext.Request.Headers["CF-Connecting-IP"].ToString();
using var request = new HttpRequestMessage(HttpMethod.Post, $"{apiUrl}/v1/environment/restrictions/checks")
{
Content = JsonContent.Create(new { ipAddress = endUserIp })
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
// httpClient from IHttpClientFactory
using var response = await httpClient.SendAsync(request, cancellationToken);
// Step 3: response
if (response.StatusCode == HttpStatusCode.OK)
{
var check = await response.Content.ReadFromJsonAsync<RestrictionCheck>(cancellationToken: cancellationToken);
var allowed = check?.Decision == "Allow";
}
else if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
{
// No decision: apply your fail-open or fail-closed policy.
}
public sealed record RestrictionCheck(string Decision, string? Reason, string? OrganizationId, string IpAddress);
Node.js / TypeScript
import { ManagedIdentityCredential } from "@azure/identity";
// Step 1: token. Create the credential once and reuse it.
const credential = new ManagedIdentityCredential({ clientId: process.env.AZURE_CLIENT_ID });
const token = await credential.getToken(`${process.env.OPTIMIZELY_CLOUDPLATFORM_API_RESOURCE_ID}/.default`);
// Step 2: check (endUserIp from the CF-Connecting-IP request header)
const apiUrl = process.env.OPTIMIZELY_CLOUDPLATFORM_API_URL!.replace(/\/+$/, "");
const response = await fetch(`${apiUrl}/v1/environment/restrictions/checks`, {
method: "POST",
headers: { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json" },
body: JSON.stringify({ ipAddress: endUserIp }),
});
// Step 3: response
if (response.status === 200) {
const { decision } = await response.json();
const allowed = decision === "Allow";
} else if (response.status === 503) {
// No decision: apply your fail-open or fail-closed policy.
}