Verify the identity header
Your API sees every call from a Gateway signed in with the same API key, whoever the call is for. The X-Gatelion-Identity header tells your API who it's for: a short-lived token, signed by your Gateway, naming the customer as your own login knows them. This page shows what's in it and how to check it.
What it is and when it's sent
X-Gatelion-Identity is a JSON Web Token (JWT). The Gateway sends it when Tell it who each call is for is turned on for the connection, under Tools & agents → Connections. Then:
- every request the Gateway makes to that connection carries a new token, made for that one call;
- the token is sent only to the connection's own address (its scheme, host and port), never to any other host, such as a sign-in token endpoint;
- the token is valid for 60 seconds;
- the Gateway removes any other
X-Gatelion-header from the request first, so only the Gateway can set it.
It works for your own agents too. Their calls carry principal_type staff and no customer_sub.
What's in the token
The JWT header:
{"alg": "ES256", "typ": "gatelion-identity+jwt", "kid": "3f9a1c0b7d2e4a68"}
kid names the Gateway's key: the first 16 hex characters of the SHA-256 of its public key. Each Gateway signs with its own key, the one it registered with when it was set up.
The claims:
| Claim | Value |
|---|---|
iss | Your issuer for the Gateway's environment: https://<control plane>/identity/<organization id>/<environment id>. |
aud | The connection's origin, such as https://api.skyline.example. The port is left out when it's the default. |
iat, exp | When it was made, and 60 seconds later. Unix seconds. |
jti | A unique id for this token. |
sub | Gatelion's id for the customer (or staff member) the call is for. |
principal_type | customer for a customer's agent, staff for your own agents. A Gateway for customers' agents always sends customer. |
customer_sub | Customers only: the customer's subject at your login, the sub your login gave them. Use it to find their account. |
agent_id | Gatelion's id for the agent making the call. |
agent_name | The agent's name. A customer's agent chose its own name, so don't trust it; use agent_id. |
organization_id, environment_id, gateway_id | Where the call came from. |
execution_id | The call's id in Gatelion's audit trail. Log it, to match your records with Gatelion's. |
tool_id | The tool that was called, such as skyline.bookings.create. |
Where to find the issuer and keys
On Customer access, under Knowing who each call is for, the console shows the Issuer and the Keys (JWKS) address for the environment you're looking at. The keys address is the issuer plus /jwks.json:
https://<control plane>/identity/<organization id>/<environment id>/jwks.json
Each environment has its own issuer and keys, so a test Gateway's token can never pass as production. Use the values for the environment your API serves.
The key set is public. It lists the keys of the environment's Gateways that are running (not stopped or retired), as EC P-256 keys for ES256. It's empty if the organization is suspended. It can be cached for 5 minutes.
Verification checklist
Your API should accept the token only if all of these hold:
- The header is there. Once you rely on it, refuse calls without it.
- The algorithm is ES256 and
typisgatelion-identity+jwt. Refuse anything else, includingnone. - The signature verifies with the key from the key set whose
kidmatches. If thekidis unknown, fetch the key set again once (a new or re-installed Gateway has a new key), but not more often than every minute or so. issis exactly your issuer for the environment you expect.audis exactly your API's own origin.exphasn't passed. Allow at most a few seconds of clock difference, not minutes.- For customers,
principal_typeiscustomerandcustomer_subnames an account at your login. Look the customer up by it, and act only on that customer's data.
Any good JWT library does steps 2 to 6 when you give it the issuer, audience, algorithm and type.
Node.js example
With jose:
import { createRemoteJWKSet, jwtVerify } from "jose";
// Copy both from Customer access (the environment your API serves).
const ISSUER = "https://<control plane>/identity/<organization id>/<environment id>";
const AUDIENCE = "https://api.skyline.example"; // your API's own origin
const keys = createRemoteJWKSet(new URL(`${ISSUER}/jwks.json`), { cacheMaxAge: 5 * 60_000 });
export async function customerFor(header: string | undefined): Promise<string> {
if (!header) throw new Error("No X-Gatelion-Identity header");
const { payload } = await jwtVerify(header, keys, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ["ES256"],
typ: "gatelion-identity+jwt",
clockTolerance: 5,
});
if (payload.principal_type !== "customer" || typeof payload.customer_sub !== "string") {
throw new Error("Not a customer call");
}
return payload.customer_sub; // the customer's subject at your login
}
Call it with req.headers["x-gatelion-identity"] and refuse the request with 401 if it throws. createRemoteJWKSet fetches the keys again by itself when it meets an unknown kid.
C# example
With the Microsoft.IdentityModel.JsonWebTokens package:
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
public sealed class GatelionIdentity(HttpClient http, string issuer, string audience)
{
private readonly JsonWebTokenHandler _handler = new();
private IList<JsonWebKey> _keys = new List<JsonWebKey>();
private DateTimeOffset _keysFetched = DateTimeOffset.MinValue;
/// <summary>The customer's subject at your login, or null if the header isn't valid.</summary>
public async Task<string?> CustomerAsync(string? header)
{
if (string.IsNullOrEmpty(header)) return null;
if (DateTimeOffset.UtcNow - _keysFetched > TimeSpan.FromMinutes(5))
{
_keys = new JsonWebKeySet(await http.GetStringAsync($"{issuer}/jwks.json")).Keys;
_keysFetched = DateTimeOffset.UtcNow;
}
var result = await _handler.ValidateTokenAsync(header, new TokenValidationParameters
{
ValidIssuer = issuer,
ValidAudience = audience,
ValidAlgorithms = new[] { SecurityAlgorithms.EcdsaSha256 },
ValidTypes = new[] { "gatelion-identity+jwt" },
IssuerSigningKeys = _keys,
ClockSkew = TimeSpan.FromSeconds(5),
});
return result.IsValid
&& result.Claims.TryGetValue("principal_type", out var type) && type is "customer"
&& result.Claims.TryGetValue("customer_sub", out var subject)
? subject as string
: null;
}
}
Create one instance per issuer and reuse it. Set ClockSkew yourself: the library's default allows 5 minutes, far longer than the token's 60 seconds. If you need to accept a new Gateway's key before the 5 minutes are up, fetch the key set again when validation fails with an unknown key.
Pitfalls
- A token can be used more than once within its 60 seconds, at the same address. Don't log the header, and don't treat it as a session. If you need each token used once, remember the
jtivalues you've seen for two minutes and refuse repeats. - Older Gateways send no header. A Gateway in your network that hasn't been updated may not send it, even with the switch on. Update it before you make the header required.
identity_unavailable. If the Gateway can't make the header, for example because the tool would call a different host than the connection's address, the call fails with this code before anything is sent to your API. The agent seesfailedwith that code; check the connection's address and its tools.- One environment's keys only. If you verify production calls with your test environment's issuer, every call fails. Copy the issuer for the right environment.
- A Gateway that's stopped or retired, or set up again with a new key, drops out of the key set or changes its
kid. Tokens it signed before stop verifying, as they should.
Related: Set up customer access · Connect a system
Still stuck? Contact support from the console.