Authentication and bearer tokens
Every call a Chronicle client makes to the Kernel over gRPC needs a bearer token attached to it. This page describes the exchange a new client has to implement to get one, keep it fresh, and attach it correctly — reverse-engineered from the .NET client’s reference implementation, since that’s where the behavior is defined today.
The three authentication modes
Section titled “The three authentication modes”Which mode a connection uses is decided entirely by what’s present in the connection string — there’s no separate flag:
| Mode | Connection string carries | What happens |
|---|---|---|
| Client credentials | username:password@host (or nothing at all) | Exchanged for a bearer token via OAuth’s client_credentials grant |
| API key | ?apiKey=... query parameter | Sent as-is (no exchange) |
| None | ?auth=none query parameter | No credentials presented at all — only works against a server with authentication turned off |
Supplying both client credentials and an API key in the same connection string is an error
(AmbiguousAuthenticationMode). Supplying none of them, without asking for auth=none, is not: the
connection string selects client credentials and uses the development default below. Only a partial
set — a username without a password, or the reverse — is an error (MissingAuthentication). See
Connection string elements for the full grammar.
Client credentials really means OAuth client-credentials
Section titled “Client credentials really means OAuth client-credentials”It’s easy to assume username:password@host is HTTP Basic auth. It isn’t. The username and
password in the connection string map directly onto an OAuth client_credentials grant: username
becomes client_id, password becomes client_secret, and the client exchanges them for an access
token by calling the Kernel’s own token endpoint before making any other RPC:
POST https://{host}:{port}/connect/tokenContent-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id={username}&client_secret={password}The response is parsed for access_token and expires_in (a missing expires_in defaults to
3600 seconds). The client secret is never sent as a Basic-auth header, and it’s never sent on
every call — only once, to get a token, which is what then goes on every call.
Development defaults
Section titled “Development defaults”A connection string with no credentials at all still resolves to client-credentials mode — it substitutes two well-known development values rather than connecting anonymously:
client_id = chronicle-dev-clientclient_secret = chronicle-dev-secretchronicle://localhost:35000 and chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000
are exactly equivalent. This is what makes local development work with no setup, and it’s also why
those two values are not a secret worth protecting — they’re baked into every Chronicle client and
meaningless outside a server explicitly configured to accept them.
Attaching the token to a call
Section titled “Attaching the token to a call”Once a token exists, it goes on every outgoing gRPC call as a metadata header:
authorization: Bearer {access_token}A gRPC client interceptor is the natural place to do this — it needs to run for unary calls and
every streaming variant (client-streaming, server-streaming, duplex), and it needs one more piece
of behavior beyond just attaching the header: if a call comes back Unauthenticated, force a token
refresh and retry the call exactly once before giving up. That single retry is what makes a token
that expires mid-session invisible to the caller instead of surfacing as a hard failure.
Chronicle-issued access tokens target the chronicle audience. After upgrading a server that previously issued tokens without an audience, reacquire those tokens rather than reusing the old cache. External-authority tokens must match the server’s configured audience. Authenticated subjects must be nonblank; Chronicle no longer invents an actor identifier when a subject is missing.
Bearer-only requests do not need the Workbench’s X-CSRF-TOKEN header. That protection applies to authenticated browser-cookie mutations; it does not change the bearer gRPC exchange described here.
Keeping the token fresh
Section titled “Keeping the token fresh”A well-behaved client refreshes proactively rather than waiting to be rejected:
- Track when the token was issued and how long it’s valid for (
expires_in). - Treat it as needing renewal a margin before actual expiry — a 60-second margin is a reasonable default — so a call that starts just before expiry doesn’t race the clock.
- If a refresh attempt fails (the token endpoint is briefly unreachable, say), keep serving the still-technically-valid cached token rather than failing every call until the endpoint recovers. Throttle repeated failed-refresh attempts (a few seconds between retries) so a down token endpoint doesn’t turn into a tight retry loop on every call.
- Treat an explicit refresh request (triggered by the 401-retry above) differently from routine proactive renewal: it should bypass the failure throttle, since it’s evidence-driven rather than a guess that the token might be stale.
Optional: caching a token to disk
Section titled “Optional: caching a token to disk”Short-lived processes — a CLI invoked once per command, a script run from cron — pay the full token-exchange cost on every single invocation if the token only ever lives in memory. A client worth using from tooling like that benefits from an optional decorator that persists the token to a local file (client ID, token, and expiry — nothing else) and reuses it across process invocations until it’s genuinely close to expiring, deleting the cache file and fetching fresh on an explicit refresh. Treat a missing or corrupt cache file as “no token” and fetch normally rather than failing — a cache is an optimization, never a dependency.
What a new client needs to implement
Section titled “What a new client needs to implement”- Determine the auth mode from the connection string (client credentials / API key / none), with the same “empty means development defaults” and “both present is an error” rules.
- For client credentials, perform the OAuth
client_credentialsexchange against/connect/tokenon the configured host. - Attach
authorization: Bearer {token}to every call, across every streaming shape your gRPC library exposes. - Cache the token, refresh it proactively ahead of expiry, and retry once on a 401.
- Never log a raw connection string or a raw token — see Connection string elements for why and how the .NET client avoids it.
Next: Clustering and the connection lifecycle covers what happens before and after that first authenticated call — picking a server and staying connected to it.