Skip to content

Automate auth

The goal is that no request ever carries a pasted token. Set a provider up once and every request that references it authenticates itself.

Background: Auth provider and Sender.

Your situationUse
Keycloak, Clerk, Auth0, Firebase, or Supabaseintegration
A login endpoint that returns a token in the bodyrequest
Anything else — a handshake, a signed assertion, two callsscript

Create the provider, choose the identity provider, and fill in its non-secret config — issuer or base URL, realm, client id, scope, domain. Client secrets and passwords are stored as keychain entries and referenced by name.

keycloak and auth0 take a grant of password or client_credentials. firebase and supabase exchange email and password for a token.

authProviders:
- name: users-api
kind: integration
integration: keycloak
config:
baseUrl: https://sso.example.com
realm: acme
clientId: test-harness
grant: password
secretRefs:
- users-api.CLIENT_SECRET
senders:
- id: admin@example.com
label: Staging admin
secretRef: users-api.admin.PASSWORD

When the login is just an API call, describe it as one. The provider carries a whole request plus a single extraction saying where the token is.

authProviders:
- name: legacy-login
kind: request
request:
name: Login
method: POST
url: "{{baseUrl}}/auth/login"
body:
contentType: application/json
content: '{"user":"svc","pass":"{{pw}}"}'
extract:
var: token
from: body
path: data.access_token

The request may use any protocol, so fetching a token over gRPC and using it on HTTP calls is ordinary rather than special.

For everything else. Your JavaScript runs in the engine’s sandboxed QuickJS runtime with a controlled fetch and returns { token, expiresIn? }.

async function authenticate({ env, config, secret, fetch, sender }) {
const res = await fetch(`${env.baseUrl}/oauth/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "password",
client_id: config.clientId,
client_secret: secret("users-api.CLIENT_SECRET"),
username: sender.id,
password: secret(sender.secretRef),
}).toString(),
});
if (!res.ok) throw new Error(`login failed: ${res.status}`);
const json = await res.json();
return { token: json.access_token, expiresIn: json.expires_in };
}

Returning expiresIn is worth doing — it is how the cache knows when to refetch. Full contract: Auth script contract.

Injection is a property of the provider, so it is not repeated on every request:

inject:
target: header # header | query | cookie
name: Authorization
template: "Bearer {{token}}"

Omit it entirely and you get Authorization: Bearer {{token}}. For an API-key style header, set name: X-API-Key and template: "{{token}}".

One provider, several identities. Each sender has an id, an optional label and tenant, and a secretRef naming its own keychain entry.

Mark one as the default. A request can override it:

auth:
provider: users-api
sender: viewer@example.com

This is how you check an authorization rule: send the same endpoint as an admin and as a viewer and compare the statuses.

Reference the provider from a request’s Auth tab, or set it once as the collection default so every request in the file inherits it:

defaults:
auth:
provider: users-api

Chains inherit it too — a chain step that references a request carries that request’s auth.

Test on the provider resolves a token and reports success with the value masked. Copy token hands you the raw JWT from the same cache your requests use, for when you need it in a terminal.

The token is fetched on every send. The provider is not reporting an expiry. Return expiresIn from a script, or check the integration’s config.

secret(...) throws. The keychain entry named in secretRefs or a sender’s secretRef does not exist on this machine. Secrets are per-machine by design and never sync — set it here.

It works for one identity and not another. A sender without a secretRef has no credential. Check that each sender points at its own keychain entry.