Auth provider
An auth provider is a reusable description of how to obtain a credential, so that no request ever carries a pasted token. You define it once; Tinspec fetches the token, caches it until it expires, and injects it into every request and chain that references the provider.
Five kinds
Section titled “Five kinds”A provider’s kind — and, for an integration, which IdP — is fixed at creation and shown
read-only afterwards. Changing how a credential is obtained is a different provider, not an edit
to this one.
script
Section titled “script”You supply JavaScript. It runs in the engine’s sandboxed QuickJS runtime with a controlled
fetch, and returns a token. This is the escape hatch for any login flow that is not a
first-class integration — a bespoke /auth/login endpoint, a signed assertion exchange, a
two-step handshake.
async function authenticate({ env, config, secret, fetch, sender }) { const res = await fetch(`${env.baseUrl}/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ username: sender.id, password: secret(sender.secretRef), }), }); const json = await res.json(); return { token: json.access_token, expiresIn: json.expires_in };}The same ts.* standard library a request hook gets is bound here
too — hmac, sha256, base64, jwt.sign and the rest — which is what makes a signed-assertion
login expressible without leaving the script.
The exact contract — what each argument gives you — is in Auth script contract.
integration
Section titled “integration”First-class support for an identity provider, configured rather than scripted. The integrations
are keycloak, clerk, auth0, firebase, and supabase.
keycloak and auth0 take a grant (password or client_credentials) in their config;
firebase and supabase exchange email and password for a token.
These five are deliberately not a growing list. A generic authorization server belongs behind
oauth2, below, not a sixth brand here.
request
Section titled “request”The token comes from an ordinary Tinspec request — any protocol, so you can fetch a token over
gRPC and use it on HTTP calls — plus one extraction saying where in the response the token is.
The result is exposed as {{token}}.
oauth2
Section titled “oauth2”Generic OAuth 2.0 / OIDC, the vendor-neutral path: any conformant authorization server. Four grants:
| Grant | What it is |
|---|---|
authorizationCode | RFC 6749 §4.1 plus PKCE, redirecting to a loopback listener (RFC 8252). Needs a human once, then lives on the refresh token. |
clientCredentials | Machine-to-machine, fully headless. |
deviceCode | RFC 8628 device authorization — headless for the engine; nothing but text. |
refreshToken | Exchange a seed refresh token for an access token, honouring rotation. |
Endpoints are discovered, not pasted. Set issuer and the engine fetches
{issuer}/.well-known/openid-configuration and fills in the authorization, token, device and
userinfo endpoints. The explicit *Url fields exist for servers with no discovery document, or a
wrong one, and always win over a discovered value.
PKCE (S256) is on by default for the authorization-code flow. clientAuth chooses how the client
authenticates at the token endpoint — basic, post, or none for a public client. tokenField
picks which field of the token response carries the credential, for an API that wants the
id_token rather than the access_token. extraParams is an open map appended verbatim, because
the parameter vocabulary belongs to the server.
clientId is public by specification and is stored as-is. The client secret and any seed refresh
token are keychain references. A rotated refresh token is kept in memory for the session; the
engine never writes the keychain.
awsSigV4
Section titled “awsSigV4”The odd one out: this kind signs the request rather than producing a token.
A SigV4 signature is a keyed hash over the final request — the method, the canonical path, the
canonical query string, a chosen set of headers, and a hash of the body. There is no {{token}}
you could put in a template that would produce it, because the template’s own text would be part
of what gets hashed.
So signing is a second, distinct output, applied as the very last step of request preparation —
after {{var}} substitution, after any body injection, after the gateway route rewrite, because
only then is the request final.
The consequence is a hard rule: an inject block on a SigV4 provider is an error, not a
silently dropped field. The engine refuses the send and says why.
Its senders are not people either — they are assume-role targets. See Sender.
Configuration is region, service, and keychain references for the access key id, the secret
access key, and optionally a session token; stsEndpoint overrides the STS endpoint used for
assume-role resolution.
Seeding from the spec
Section titled “Seeding from the spec”A bound OpenAPI document already states, in machine-readable form, how the API expects to be
authenticated — the grant, the token URL, the authorization URL, the scopes. Tinspec maps
components.securitySchemes into ready-to-create providers so you confirm a proposal instead of
retyping a form. The one thing the document cannot contain is the client secret, and that goes to
the keychain as a reference.
Two lists come back, because a long list of proposals would be less honest than a short one: the proposals that become real providers, and the schemes that are not providers — a bearer token or an API key belongs on the request as inline auth, and the implicit grant, the password grant and mutual TLS cannot be automated. Each one says why and where to go instead, so nothing is silently dropped.
Injection
Section titled “Injection”Where the credential goes is part of the provider, not repeated on every request:
inject: target: header # header | query | cookie | body | variable name: Authorization template: "Bearer {{token}}"Omit the block and the default applies: Authorization: Bearer {{token}}.
header,queryandcookieplace a rendered template.bodysets a JSON dot-path (auth.token) inside the request’s JSON slot — the HTTP body, a gRPC message, GraphQL variables, or an MCP tool’s arguments. An existing value at that path wins.variableinjects nothing at all. It only exposes{{token}}so you can place it by hand.
A request can override the provider’s placement for itself with its own auth.inject — the
token in a body field for this one call, in the header everywhere else — without forking the
provider. See Request → Auth on a request, which also
covers the cases where a body target is refused up front.
Secrets are references, never values
Section titled “Secrets are references, never values”A provider’s YAML holds names, not credentials:
configis non-secret only — base URL, realm, client id, token URL, scope, domain.secretRefslists OS-keychain key names.oauthandawsfollow the same rule:clientSecretRef,refreshTokenRef,accessKeyIdRef,secretAccessKeyRef,sessionTokenRef.- A sender’s
secretRefnames that identity’s own keychain entry.
The values live in your OS keychain or a machine-local encrypted store, chosen in Settings → Security. This is why an auth provider file is safe to commit, and why cloud sync can move provider definitions between machines without ever moving a credential.
Caching
Section titled “Caching”A fetched token is cached per (provider, sender) pair until it expires and shared across every
send that uses it — including chain steps and requests driven by an
agent. You are not re-authenticating on every call.
Concurrent callers for the same pair wait on one resolution rather than each running the token dance. A resolution that fails caches nothing and releases the gate, so an outage does not become a stuck queue and one caller’s error is never handed to a caller that never made the request.
If you need the raw token for something outside Tinspec, Copy token hands it to the clipboard from that same cache. The provider’s Test action stays masked.
When a token is rejected
Section titled “When a token is rejected”Tinspec re-authenticates on the actual response, not on a timer. A real 401 or 403 drops the cached credential, mints a new one, and retries once — and records what it saw.
That record carries the one comparison nothing else in the app can make: the rejected token’s real age against the lifetime it advertised. A provider whose tokens actually live 55 seconds while claiming an hour otherwise just looks like a flaky API.
Two edges are built into what it reports, and both are refusals to overclaim:
- Under about 30 seconds of advertised life remaining, it says clock skew explains it rather than presenting a finding. A token that expired on time is not a bug.
- If the retry also failed, it says the credential was not the problem — because it was not. Saying “re-authenticated” and stopping there would send you to look at token lifetimes for something that lives elsewhere.
The view
Section titled “The view”A provider is edited in three tabs — Setup (name and source configuration), Senders (the identity directory), and Injection (the default placement). A SigV4 provider’s Injection tab says why there is nothing to place.
Providers are files: .tinspec/auth/<name>.yaml, plus <name>.js for a script provider. They are
the same files the engine reads.
Senders
Section titled “Senders”One provider, many identities — see Sender.