Skip to content

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.

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.

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.

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.

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}}.

Generic OAuth 2.0 / OIDC, the vendor-neutral path: any conformant authorization server. Four grants:

GrantWhat it is
authorizationCodeRFC 6749 §4.1 plus PKCE, redirecting to a loopback listener (RFC 8252). Needs a human once, then lives on the refresh token.
clientCredentialsMachine-to-machine, fully headless.
deviceCodeRFC 8628 device authorization — headless for the engine; nothing but text.
refreshTokenExchange 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.

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.

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.

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, query and cookie place a rendered template.
  • body sets 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.
  • variable injects 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.

A provider’s YAML holds names, not credentials:

  • config is non-secret only — base URL, realm, client id, token URL, scope, domain.
  • secretRefs lists OS-keychain key names.
  • oauth and aws follow the same rule: clientSecretRef, refreshTokenRef, accessKeyIdRef, secretAccessKeyRef, sessionTokenRef.
  • A sender’s secretRef names 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.

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.

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.

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.

One provider, many identities — see Sender.