Skip to content

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.

Which mode a connection uses is decided entirely by what’s present in the connection string — there’s no separate flag:

ModeConnection string carriesWhat happens
Client credentialsusername:password@host (or nothing at all)Exchanged for a bearer token via OAuth’s client_credentials grant
API key?apiKey=... query parameterSent as-is (no exchange)
None?auth=none query parameterNo 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/token
Content-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.

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-client
client_secret = chronicle-dev-secret

chronicle://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.

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.

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.

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.

  • 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_credentials exchange against /connect/token on 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.