Auth¶
VARS services use four distinct auth patterns. vars_client.auth provides one
AuthenticationProvider per pattern, all wired automatically by
VARSClient — this page is for when you need to build a
Kiota RequestAdapter by hand, or just want to understand what's happening
under the hood.
JWT exchange¶
Annosaurus, Vampire Squid, Oni, and Panoptes all exchange an API key for a 24-hour JWT:
POST /v1/auth
Authorization: APIKEY <secret>
200 OK
{"token_type": "Bearer", "access_token": "<jwt>"}
Read-only requests need no auth at all; write requests need the JWT attached
as Authorization: Bearer <jwt>.
from vars_client.auth.jwt_exchange import JWTExchangeAuthProvider
auth = JWTExchangeAuthProvider("https://annosaurus.example.org", "my-api-key")
Panoptes mounts its auth endpoint at a different path, so pass it explicitly:
JWTExchangeAuthProvider(
"https://panoptes.example.org", "my-api-key", exchange_path="/panoptes/v1/auth"
)
Raziel bootstrap¶
Raziel's exchange is two-stage:
POST /config/authwithAuthorization: Basic <base64(user:pass)>→ JWT (fieldaccessToken, notaccess_token— Raziel's response shape differs from the other four services).GET /config/endpointswithAuthorization: Bearer <jwt>→ the base URLs and secrets for the rest of the VARS stack.
from vars_client.auth.raziel import RazielAuthProvider
auth = RazielAuthProvider("https://raziel.example.org", "user", "password")
endpoints = await auth.get_endpoints() # step 2, one-off config fetch
username/password are both optional. Without them, get_endpoints()
skips stage 1 entirely and calls /config/endpoints with no Authorization
header — Raziel documents this as valid, it just omits each entry's secret
from the response:
auth = RazielAuthProvider("https://raziel.example.org") # anonymous
endpoints = await auth.get_endpoints() # same shape, secrets omitted
An anonymous RazielAuthProvider is also a no-op as an AuthenticationProvider
— authenticate_request attaches nothing rather than attempting an exchange
it has no credentials for.
RazielAuthProvider.get_endpoints() is the mechanism behind
RazielConfigProvider — you won't normally
call it directly.
Static key¶
Beholder carries a static X-Api-Key header on every request. No exchange,
no expiry:
from vars_client.auth.static_key import StaticKeyAuthProvider
auth = StaticKeyAuthProvider("beholder-secret")
No auth¶
Skimmer is unauthenticated and GET-only — see Skimmer.
Shared machinery: JWTBootstrapAuthProvider¶
JWTExchangeAuthProvider and RazielAuthProvider are both thin
configurations of JWTBootstrapAuthProvider
(vars_client.auth.jwt_bootstrap), which handles:
- Caching the JWT and attaching it as
Authorization: Bearer <jwt>on every request. - Refreshing proactively a few minutes before the 24h expiry, so requests don't race a token that's about to die.
- Concurrency-safe refresh — concurrent callers share one in-flight exchange instead of each triggering their own.
invalidate(), to force the next request to re-exchange (used after a 401, see below).
Retrying after a 401¶
RefreshOnUnauthorizedMiddleware (vars_client.auth.middleware) pairs with
any JWTBootstrapAuthProvider. It sits in the Kiota HTTP middleware pipeline
and, on a 401 response, invalidates the cached token, re-exchanges, and
retries the request once with the fresh token:
from kiota_http.httpx_request_adapter import HttpxRequestAdapter
from kiota_http.kiota_client_factory import KiotaClientFactory
from vars_client.auth.jwt_exchange import JWTExchangeAuthProvider
from vars_client.auth.middleware import RefreshOnUnauthorizedMiddleware
auth = JWTExchangeAuthProvider("https://annosaurus.example.org", "my-api-key")
http_client = KiotaClientFactory.create_with_custom_middleware(
[*KiotaClientFactory.get_default_middleware(None), RefreshOnUnauthorizedMiddleware(auth)]
)
adapter = HttpxRequestAdapter(auth, http_client=http_client)
adapter.base_url = "https://annosaurus.example.org" # the `base_url=` constructor kwarg is a no-op
HttpxRequestAdapter(base_url=...) doesn't work
Kiota's HttpxRequestAdapter constructor accepts a base_url keyword
argument, but it's never assigned anywhere in the implementation — only
the base_url property setter actually takes effect. Always set
adapter.base_url = ... after construction. VARSClient already does
this for you.
VARSClient wires this middleware automatically for every JWT-exchange
service; you only need it yourself if you're constructing an adapter
directly.