Loading Gatelion

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:

JSON
{"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:

ClaimValue
issYour issuer for the Gateway's environment: https://<control plane>/identity/<organization id>/<environment id>.
audThe connection's origin, such as https://api.skyline.example. The port is left out when it's the default.
iat, expWhen it was made, and 60 seconds later. Unix seconds.
jtiA unique id for this token.
subGatelion's id for the customer (or staff member) the call is for.
principal_typecustomer for a customer's agent, staff for your own agents. A Gateway for customers' agents always sends customer.
customer_subCustomers only: the customer's subject at your login, the sub your login gave them. Use it to find their account.
agent_idGatelion's id for the agent making the call.
agent_nameThe agent's name. A customer's agent chose its own name, so don't trust it; use agent_id.
organization_id, environment_id, gateway_idWhere the call came from.
execution_idThe call's id in Gatelion's audit trail. Log it, to match your records with Gatelion's.
tool_idThe 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:

Text
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:

  1. The header is there. Once you rely on it, refuse calls without it.
  2. The algorithm is ES256 and typ is gatelion-identity+jwt. Refuse anything else, including none.
  3. The signature verifies with the key from the key set whose kid matches. If the kid is 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.
  4. iss is exactly your issuer for the environment you expect.
  5. aud is exactly your API's own origin.
  6. exp hasn't passed. Allow at most a few seconds of clock difference, not minutes.
  7. For customers, principal_type is customer and customer_sub names 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:

TypeScript
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:

C#
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 jti values 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 sees failed with 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.