Skip to content

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:

  1. POST /config/auth with Authorization: Basic <base64(user:pass)> → JWT (field accessToken, not access_token — Raziel's response shape differs from the other four services).
  2. GET /config/endpoints with Authorization: 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 AuthenticationProviderauthenticate_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.