Skip to main content
GKL API does not use the standard ASP.NET authentication pipeline. Every request passes through one custom SecurityMiddleware, which decides who the caller is, and then each action decides for itself what that caller may do. Those are two separate mechanisms, and the second one is opt in. Anyone adding an endpoint needs to understand both. See The API Surface for what exists today.

The Four Ways In

The first three all arrive in the same header. x-api-key carries either an API key or a bearer token, and the middleware works out which by trying the external key, then the internal key, then validating it as a JWT. So a malformed token and a wrong API key are indistinguishable from the outside, and both come back as a bare 401.

How A Request Is Authenticated

1

Anonymous endpoints go straight through

An action marked [AllowAnonymous] skips all of this. There are eight of them, listed in The API Surface.
2

Webhook endpoints are verified by signature

An action marked [HmacVerification] is checked against x-vivenu-signature instead of a key. See below.
3

Everything else needs the header

No x-api-key means 401 immediately, with the body Unauthorized.
4

The value is tried three ways

Against the external API key, then the internal API key, then as a JWT. The first match wins and sets the caller’s claims. If none match, the response is 401 carrying the token validation message.
Order matters. The two service keys are compared before any token parsing happens, so a service key never pays the cost of a failed JWT decode, and a JWT that happens to equal a service key would be treated as that service.

Webhook Verification

Vivenu webhooks are the only callers that do not present a key. The middleware computes HMAC SHA256 over the raw request body, hex encoded and lowercased, and compares it to the x-vivenu-signature header. Request buffering is enabled so the body can be read for hashing and then again by the action. The signing secret is per hub region. It is read from that region’s entry in the REGION_CONFIGURATION document, selected using the hubRegion query string parameter on the request.
If hubRegion is missing from the query string the middleware throws rather than returning 401, so the caller sees a 500. A webhook registered in Vivenu without the region parameter therefore looks like a server fault rather than a configuration mistake. That is the first thing to check when a newly registered webhook fails.
Because the secret is per region, a webhook is only ever valid for the region named in its own URL. Registering the right URL with the wrong region silently fails signature verification and returns 401.

The Claims

A caller ends up with a set of claims, and authorisation is expressed entirely in terms of them. Alongside those, a token carries a TYPE claim and identity claims:
The TYPE claim is what keeps the emailed links narrow. A password reset token carries RESET_PASSWORD and not AUTHENTICATED, so it satisfies the reset endpoint and fails every endpoint that wants a real session. The same holds for email confirmation. Getting a reset link does not get you an account.

Tokens

Tokens are issued by JsonTokenService, signed with HMAC SHA256, and expire after one day by default. The claims come from the user or admin document, plus any special claims the caller adds. Logging in adds TYPE=AUTHENTICATED.
The signing secret is derived from the internal API key. It is the base64 of the UTF32 bytes of that key, and nothing else. So rotating the internal API key does two things at once: it invalidates every token already issued, signing every user out, and it breaks every internal caller still using the old key. Those two blast radii are not obviously connected, and nothing in the code hints at it.

Where The Keys Come From

Both are read once and then cached in memory, so a rotated secret is not picked up until the process restarts. On Lambda that means the change lands unevenly as warm containers age out, and for a while some requests will validate against the old key and some against the new.

Authorisation Is Opt In

There are no [Authorize] attributes and no authorisation policies. Instead, each action calls a guard as its first line:
The guard takes a set of claims the caller must have all of, and one higher claim that overrides them. It passes if either side is satisfied, and otherwise throws UnauthorizedAccessException, which the middleware converts to a 401. Three idioms cover almost every endpoint:
An action that calls no guard is authenticated but not authorised. Any caller holding any valid credential, including any signed in customer, passes. The guard is a line of code that is easy to leave out, and nothing fails if you do.

What That Means Today

Of 88 endpoints, 83 are either anonymous by design, HMAC verified, or reach a guard. Five reach no guard at all: All five are read paths the booking website needs for any signed in customer, so having no admin guard is consistent with their purpose. There is no endpoint in the API that looks like it lost its guard by accident.

One Thing Not To Change

GET /api/v1/configurations/regions is anonymous, because the hub picker on the booking website needs it before anyone signs in. It returns the REGION_CONFIGURATION document, which also holds each region’s webhook HMAC secret, Vivenu API secret and Teams webhook URL. What keeps those out of the response is that the DTO mapper is an allowlist. It projects five fields and nothing else: the region ID, currency, display name, whether it is active, and whether to show it on the website.
Adding a field to ToDto(this RegionConfigurationItem) publishes it to the internet with no authentication. The neighbouring GET /api/v1/configurations/all endpoint is anonymous too, and protects itself by explicitly excluding REGION_CONFIGURATION from its results. Both of those protections are one line long and neither is obvious from the endpoint.

Key Files

All paths are under GklPlatformAPI/GklPlatform/ in the GKL Platform repository.
Last modified on September 8, 2026