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
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 thex-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.
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 byJsonTokenService, 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.
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:
UnauthorizedAccessException, which the middleware converts to a 401.
Three idioms cover almost every endpoint:
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.
Key Files
All paths are underGklPlatformAPI/GklPlatform/ in the GKL Platform repository.
