5 min readCJ Brewer

Ridding ourselves of API keys

Swirls now exchanges JWTs from your identity provider for short-lived access tokens. No more long-lived API keys and no more guessing which user did what.

swirlsauthoidcidentity

Agents are getting good at calling APIs. The credentials they call them with have not kept up. Most agent deployments still ship with a long-lived API key set in an environment variable, scoped to the workload's full capability, rotated when somebody remembers.

We wanted to rid ourselves of API keys. Swirls now exchanges JWTs from your identity provider for short-lived Swirls access tokens. There is no SWIRLS_API_KEY to issue, store, or rotate.

#Why this matters

Long-lived API keys are the leftover credential pattern from before workloads had identity. They live in environment variables and CI secrets, they get scoped broadly because the alternative is fiddly, and they survive the engineer who created them. The risk is structural: a key valid for a year is a key that can be exfiltrated for a year.

The fix is not better key rotation. It is moving the trust root to the identity provider you already operate, and replacing the year-long key with one that expires in minutes.

#How it works

OIDC Federation is a token exchange. Four steps:

  1. Your workload requests a JWT from its identity provider
  2. It presents the JWT to the Swirls token endpoint
  3. Swirls validates the JWT against the provider you registered, and returns a short-lived access token
  4. Your workload calls the Swirls API with that token as a bearer credential, then discards it

The exchanged token is short-lived (approximately 15 minutes) and there is no refresh token. When it expires, you mint a new IdP JWT and exchange again. There is no client secret in the exchange itself — the IdP's signature on the JWT is the credential.

The token endpoint:

curl -s -X POST https://auth.swirls.ai/oauth2/token \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  --data-urlencode "subject_token=$YOUR_IDP_JWT" \
  --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:jwt"

In TypeScript, the same exchange with a small cache so you only mint a new token when the current one is within thirty seconds of expiring:

let cached: { token: string; expiresAt: number } | null = null

async function getSwirlsToken(idpJwt: string): Promise<string> {
  const now = Date.now()
  if (cached && cached.expiresAt - now > 30_000) {
    return cached.token
  }

  const res = await fetch("https://auth.swirls.ai/oauth2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
      subject_token: idpJwt,
      subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
    }),
  })

  if (!res.ok) throw new Error(`Token exchange failed: ${(await res.json()).error}`)

  const data = await res.json()
  cached = { token: data.access_token, expiresAt: now + data.expires_in * 1000 }
  return cached.token
}

Then call the Swirls API the way you would call any bearer-authenticated service:

curl -s https://swirls.ai/api/<endpoint> \
  -H "Authorization: Bearer ${getSwirlsToken(idpJwt)}"

#Who did what

API keys flatten attribution. Every event in the system traces back to the holder of the key — in practice, the engineer who generated it or the workload it lives in. The end user who actually triggered the work disappears.

OIDC Federation keeps the IdP's user ID in the audit trail. The federation exchange records it as the subject; downstream events in the same session correlate back to that exchange:

EVENT           SUBJECT              KIND
exchange        user_3EJlVyjn...     federation_exchange
token minted    auth                 execution_token
graph executed  user_3EJlVyjn...     graph

You can answer the questions a flat key can't: which user triggered which agent, which user accessed which secret, which user's session produced which graph execution. The subject claim from your IdP, the same one your application already trusts, propagates through the runtime's audit log.

#Standards and providers

OIDC Federation in Swirls implements RFC 8693 (OAuth 2.0 Token Exchange) and publishes RFC 8414 discovery metadata at https://auth.swirls.ai/.well-known/oauth-authorization-server. Clerk and Supabase are supported; both must use asymmetric JWT signing keys (RS256 for Clerk; RS256 or ES256 for Supabase). Supabase projects on the legacy shared HS256 secret cannot federate.

The same OIDC posture extends to .swirls files via the auth block — the artifact-side surface of the same identity model. We covered that in Swirls anatomy.

#Getting started

Three steps to register an identity provider:

  1. Open the Auth section of the Swirls Portal
  2. Click Register provider, choose Clerk or Supabase, and enter your instance identifier
  3. Copy the Audience and Issuer values into your IdP configuration so that JWTs include the Swirls audience in their aud claim

The full docs — JWT requirements, error responses, token expiry handling, and the token helper pattern in more languages — live at swirls.ai/docs/platform/oidc-federation.