Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Authentication schemes

Every scheme a descriptor can declare, what each one needs in your config, and what the credential health states are saying.

ReferenceLast updated

On this page

Each provider descriptor declares one authentication scheme, and that choice decides which config fields your upstream needs. Get the pairing wrong and validation says so before you start the binary, naming both the scheme and the fields it expects.

This build implements 7 authentication schemes, and 6 of them are declared by a shipped descriptor. One further name, query, is reserved and rejected.

Every scheme here describes a self-hosted upstream, whose credential arrives from the environment at startup while the config holds only the variable’s name. A hosted saved provider connection carries its provider’s scheme with it, and there the secret is stored encrypted at rest under a customer-managed KMS key in TokenTriage’s cloud account, readable by nothing you or we can call. Request-supplied mode stores no copy at all: the caller’s key serves that one request. So a storage claim on this page is a claim about this mode.

Scheme Credential arrives as Config fields Descriptors
bearer Authorization: Bearer <key> api_key_env 61
gcp_oauth a self-signed service-account JWT auth.credentials_file_env, or api_key_env 13
header a named header carries the key api_key_env 10
none nothing is injected none 8
aws_sigv4 a Signature Version 4 signature auth.access_key_id_env + auth.secret_access_key_env, or api_key_env 2
snowflake_jwt an RS256 JWT signed with your key pair auth.private_key_file_env + account + user 1
oci_sig request signing, with no signer behind it auth.private_key_file_env + four identifiers 0

The right-hand column counts, out of the 95 built-in provider descriptors TokenTriage compiles in, how many declare each scheme. It is derived from the descriptor registry by the same generator that writes the provider matrix, so it moves when the registry does. That generator names the test behind each gated provider-by-endpoint cell and prints the ungated ones rather than hiding them. Ten descriptors carry no gated cell; the provider reference names each one. Where a test does exist it drives a request through the descriptor against a mock shaped like that provider and checks that the request was authenticated and forwarded, the response relayed byte for byte, usage extracted and the price resolved. Those are mocks, not the providers’ live endpoints. The provider reference prints the evidence cell by cell.

bearer

The default, and the one most providers use.

upstreams:
  groq:
    provider: groq
    base_url: https://api.groq.com/openai   # /openai is Groq's — your client already sends /v1
    api_key_env: GROQ_API_KEY

What it sends. Authorization: Bearer <the value of that variable>, replacing whatever credential the inbound request carried.

With no api_key_env. The inbound Authorization header is forwarded unchanged, so your client’s own key reaches the provider. This is a supported way to run, not an error.

How it fails. With the variable unset, requests are forwarded with the client’s credential and the provider answers. The boot banner prints a line naming the variable, and doctor reports missing_env on its keys check.

Oracle Cloud is on this scheme. The oci descriptor takes an OCI Generative AI API key, which Oracle states is different from an OCI IAM API key, so no request-signing scheme was built for it. The key is region-scoped, and the regional host is your own base_url.

Identical in shape to bearer, with the key written into a provider-specific header instead.

Descriptor Header Value written
anthropic x-api-key the raw key
azure_openai, azure_ai, azure_text api-key the raw key
gemini x-goog-api-key the raw key
elevenlabs xi-api-key the raw key
black_forest_labs x-key the raw key
assemblyai Authorization the raw key
deepgram Authorization Token then the key
fal_ai Authorization Key then the key

2 of the 10 carry a prefix inside the value: Deepgram’s Token and fal.ai’s Key . The trailing space is part of the wire, and it is descriptor data rather than a branch inside the driver, so the next provider with a non-Bearer prefix is a row rather than a patch.

Whichever scheme injects a credential also strips the inbound carriers it supersedes: Authorization, x-api-key, api-key and x-goog-api-key by default, so a client’s own key does not travel alongside the operator’s.

aws_sigv4

Bedrock signs each request rather than presenting a key, so it needs a region and a credential pair.

upstreams:
  bedrock:
    provider: bedrock
    base_url: https://bedrock-runtime.us-east-1.amazonaws.com
    region: us-east-1                       # signing scope and region-class pricing
    auth:
      access_key_id_env: AWS_ACCESS_KEY_ID
      secret_access_key_env: AWS_SECRET_ACCESS_KEY
      session_token_env: AWS_SESSION_TOKEN  # optional, for temporary credentials

region: is required, and validation says why: it is both the signing scope and the thing that selects a region-qualified price. An upstream without it is turned away rather than signed wrongly.

A bearer key short-circuits the whole scheme. Set api_key_env alone, with no auth: block, and a Bedrock API key is presented as a bearer token instead of signing.

Signing keys are cached and refreshed off the request path. A refresh failure keeps the last working material and degrades the reported health rather than blocking traffic.

gcp_oauth

Vertex AI mints a self-signed service-account JWT and presents it as a bearer token. The environment variable holds the path to the key file, not the key.

upstreams:
  vertex:
    provider: vertex_ai
    base_url: https://us-central1-aiplatform.googleapis.com
    params:
      project: my-gcp-project
      location: us-central1
    auth:
      credentials_file_env: GOOGLE_APPLICATION_CREDENTIALS

The primary flow makes no network call. The compact JWT is the token, so there is no token-exchange round trip in the common case. The key file is read once, away from the request path, and neither the path nor the key material appears in a response, a log or a status payload.

api_key_env is accepted instead, for the Gemini AI Studio style of key.

When the self-signed flow cannot be used, the scheme falls back to a token exchange and says so in its health state rather than failing quietly.

snowflake_jwt

Snowflake authenticates a key pair rather than a token, so this scheme signs a short-lived JWT with your private key and presents it as a bearer credential. One descriptor declares it, and it is the only scheme this build added.

upstreams:
  snowflake:
    provider: snowflake
    base_url: https://myorg-myaccount.snowflakecomputing.com
    account: MYORG-MYACCOUNT
    user: SVC_TOKENTRIAGE
    auth:
      private_key_file_env: SNOWFLAKE_PRIVATE_KEY_PATH

account and user are plain config, not secrets. They become the JWT’s iss and sub claims and travel to Snowflake in cleartext, which is why a file like the one above can be committed. private_key_file_env names an environment variable, and that variable holds the path to the PEM, so the key material is two hops away from your config.

The host is yours, not derived. The account-scoped hostname comes from base_url and nothing computes it from account. Two sources for one hostname would be one source too many.

What it sends. An RS256 JWT as Authorization: Bearer, minted and refreshed off the request path, with a 59-minute lifetime renewed 5 minutes before it expires. A second header, X-Snowflake-Authorization-Token-Type: KEYPAIR_JWT, rides along; that one is descriptor data rather than a credential.

A period in account: is turned away rather than normalised. Snowflake’s own prose replaces periods with hyphens and Snowflake’s own published sample generator truncates at the first period, so myorg.myaccount becomes MYORG-MYACCOUNT under one rule and MYORG under the other. Picking one silently would mint a JWT naming an account you did not write, and the only symptom would be a 401 that looks like a rotated key. Write the <orgname>-<account_name> form both rules agree on.

none

Nothing is injected. Used by the 8 self-hosted and declared-free descriptors, where the server is on your own network.

upstreams:
  ollama:
    provider: ollama
    base_url: http://127.0.0.1:11434

Credential references are tolerated and unused, so leaving one behind from an earlier config is not an error.

oci_sig, the scheme with no descriptor behind it

Request signing over an Oracle tenancy is implementable and no built-in descriptor declares it. The count in the table above is zero, and it is zero for a reason worth reading before you reach for this scheme.

Your oci upstream does not use it. The shipped oci descriptor authenticates with bearer and a working API key, so an OCI upstream configured the ordinary way is authenticated in the ordinary way. Nothing on this page changes that.

The axis beneath the scheme ships; the signer was specified and then not built. Declare auth: {scheme: oci_sig} on a descriptor of your own and the config validates, the upstream boots, and the authenticator loads your PEM and reports its health. What it does not do is sign anything: it injects no credential at all, and the provider answers with its own 401. Validation demands 4 identifiers first, each with its own message naming why the signature needs it:

upstreams["oci_sig_demo"].tenancy_ocid: required by the "oci_sig" scheme — it is a literal component of the request signature's keyId (App. B.3)
upstreams["oci_sig_demo"].user_ocid: required by the "oci_sig" scheme — it is a literal component of the request signature's keyId (App. B.3)
upstreams["oci_sig_demo"].key_fingerprint: required by the "oci_sig" scheme — it is a literal component of the request signature's keyId (App. B.3)
upstreams["oci_sig_demo"].compartment_ocid: required by the "oci_sig" scheme — every OCI Generative AI chat request carries a mandatory compartmentId body field (it is not part of the signature)

That capture is tokentriage validate on a config declaring an inline oci_sig provider with all four identifiers omitted; the upstream name is this example’s, and the rest is the binary’s.

The health state will read ok, and it means less than it looks like. For this scheme ok reports that the PEM parsed, not that a request will authenticate. Read it as a fact about the key file. An encrypted key is reported as unreadable_credentials here too, but the passphrase is not what stands between you and a 200: nothing is injected either way.

Why a scheme in this state exists at all is worth one sentence. An earlier build met a declared scheme it could not construct and quietly fell back to a different credential under a different scheme, which authenticated with something else and looked like it worked. That fallback is gone, replaced by three named errors that say what they are declining to do. A declared-but-unbuilt scheme that injects nothing and says so is the opposite failure direction from the one that was removed.

The reserved eighth name

A query scheme, the key as a URL query parameter, is named in the vocabulary and turned away when a descriptor declares it, at descriptor validation and again in the transport.

The objection is the carrier rather than the provider, and it is sharper here than the usual one about access logs and browser history. A transport failure embeds the full outbound URL, query string included, into the Go error it produces, and the pipeline persists that error into the decision ledger’s outcome.error field. A query credential would therefore end up written into your own ledger by a failure path nobody thinks about. Every provider surveyed has a header alternative, so the scheme buys nothing worth that.

Pairing errors

Validation checks the credential block against the descriptor’s scheme, and the messages name both.

What you wrote What you get
auth: block on a bearer or header provider provider uses the "bearer" scheme (single credential via api_key_env); the multi-part auth.* fields are not valid here
credentials_file_env on aws_sigv4 not valid for the "aws_sigv4" scheme (use access_key_id_env/secret_access_key_env, or api_key_env alone)
AWS fields on gcp_oauth the "gcp_oauth" scheme uses credentials_file_env or api_key_env; AWS-style access_key/secret fields are not valid here
neither credential on aws_sigv4 the "aws_sigv4" scheme requires access_key_id_env and secret_access_key_env (or api_key_env alone …)
Bedrock without region: provider "bedrock" requires a region (aws_sigv4 signing scope / region-qualified pricing / a {region} URL template)
AWS or GCP fields on snowflake_jwt the "snowflake_jwt" scheme uses private_key_file_env; the AWS/GCP credential fields are not valid here
snowflake_jwt with no account: required by the "snowflake_jwt" scheme — it is a literal component of the JWT's iss/sub claims (App. B.4)

Neither key-file scheme has a bearer short-circuit. aws_sigv4 and gcp_oauth both accept api_key_env as an alternative; snowflake_jwt and oci_sig do not, because a private key is not a token you can present in its place.

Reading auth health

A running instance reports the scheme and a health state per upstream on the admin plane, as names and states only. No value, path or token fragment appears.

"upstreams": {
  "oci": {
    "base_url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1",
    "dialect": "",
    "in_cooldown": false,
    "auth_scheme": "bearer",
    "auth_health": { "configured": true, "fresh": true, "state": "ok" }
  },
  "snowflake": {
    "base_url": "https://myorg-myaccount.snowflakecomputing.com",
    "dialect": "",
    "in_cooldown": false,
    "auth_scheme": "snowflake_jwt",
    "auth_health": { "configured": true, "fresh": true, "state": "ok" }
  }
}

That is the upstreams block of GET /tt/api/status from a booted instance with those two upstreams configured, taken on 2026-08-24 and re-indented for reading; the payload also carries the mode, the listen address, the tier names and the pricing snapshot. The base_url values are the ones the config named, and the auth fields are the descriptors’ and the authenticators’. dialect reads back empty on both because the field echoes the dialect: key you wrote rather than the wire the descriptor resolved to, and neither upstream needs to write one. Take a descriptor’s wire from the provider reference instead.

One value in that capture is a misconfiguration worth naming. The oci descriptor carries an absolute chat path, /20231130/actions/v1/chat/completions, so the base_url above repeats that prefix and the request forwards to it twice. Point an oci upstream at the bare regional host.

The state is a closed set of six values.

State Means What to do
ok the credential resolved and is current nothing
missing_env a referenced variable is unset export it, or accept client-forwarded credentials
unreadable_credentials the file is present but unreadable, malformed or passphrase-encrypted check the path, and check the key is not encrypted
refresh_failed a background refresh failed; the last good material is still serving check the provider and your clock skew
expired the cached token expired and could not be refreshed the same, with urgency; requests now get the provider’s error
fallback_exchange_active gcp_oauth fell back to a token exchange usually informational