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.

For testing only; your app never sends it. Sent as X-Turnstile-Instance-Id, but the Gateway replaces it with the caller's own instance, so organizationId in the response should not change. Leave empty to omit the header.

How your app calls it

How it works

  1. 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.
  2. Your app sends the end user's IP to the Gateway (OPTIMIZELY_CLOUDPLATFORM_API_URL), with the token in the Authorization header.
  3. 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.
  4. Cloud Platform Services checks the IP against your organization's Opti ID IP allowlist and returns Allow or Deny.

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

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).

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.
}

More

Environment

Settings

Client IP sources