SWIRLS_
Platform

OIDC Federation

Exchange a JWT from your own identity provider for a short-lived Swirls access token. No long-lived API keys required.

OIDC Federation lets your workload authenticate to Swirls using a JWT issued by your own identity provider. Your workload exchanges that JWT at the Swirls token endpoint and receives a short-lived Swirls access token. Use that token to call the Swirls API.

No long-lived Swirls API keys to store or rotate. Your organization keeps full control at its own identity provider.

How it works

Your workload requests a JWT from your identity provider (Clerk or Supabase). It presents that JWT to the Swirls token endpoint. Swirls verifies the JWT against the registered provider configuration and returns a short-lived access token. Your workload uses that access token to call the Swirls API, then discards it. When the token expires, repeat the exchange.

Configure a provider

Open the Auth section in the Portal

In the Swirls Portal, select your organization. In the left sidebar, open Auth.

Register a provider

Click Register provider. Select your identity provider. Swirls currently supports Clerk and Supabase.

You can also register programmatically with the SDK idp client. An issuer can be registered by only one Swirls organization; registering an issuer that another organization already claimed is rejected.

Enter the instance identifier

The identifier is not a URL. Enter the value that matches your provider.

Enter your Frontend API domain.

EnvironmentFormatExample
Productionclerk.yourdomain.comclerk.acme.com
Developmentyour-app-name.clerk.accounts.devacme-dev.clerk.accounts.dev

Find this in the Clerk dashboard under API Keys.

Enter your project reference. This is the 20-character string from your Supabase project URL and project settings.

Example: abcdefghijklmnopqrst

Find this in the Supabase dashboard under Project Settings > General.

Set the subject claim (optional)

Swirls reads the subject from the sub claim by default. If your tokens carry the subject in a different claim, enter that claim name here.

Save and copy the provider values

After saving, the provider appears in the list with a Pending status. It becomes Active automatically after the first successful token exchange.

Copy the two values shown:

  • Audience: a unique string like swirls:aud:9fK2x7.... Your JWTs must include this in their aud claim.
  • Issuer: the iss value your JWTs must carry. This is derived automatically from your instance identifier. It is shown here for reference.

Configure your identity provider to include the Swirls audience

Your JWTs must carry the Swirls audience in the aud claim. You can add it alongside any audiences already present.

Create a JWT Template in the Clerk dashboard.

In the template's claims, set:

{
  "aud": "swirls:aud:9fK2x7..."
}

Replace the audience value with the one copied from the Portal. Mint tokens against this template when your workload needs to call Swirls.

Your project must use asymmetric JWT signing keys (RS256 or ES256), which publish a JWKS that Swirls verifies against. Projects still on the legacy shared HS256 secret are not supported. See Supabase JWT signing keys.

To add the Swirls audience, use a Custom Access Token Hook that adds the audience value to the aud claim. You can keep any existing audiences and add the Swirls value alongside them.

The aud claim must include the Swirls audience value copied from the Portal.

Exchange a JWT for a Swirls access token

Swirls implements RFC 8693 OAuth 2.0 Token Exchange.

Endpoint: POST https://auth.swirls.ai/oauth2/token

Content-Type: application/x-www-form-urlencoded

ParameterValue
grant_typeurn:ietf:params:oauth:grant-type:token-exchange
subject_tokenYour IdP-issued JWT
subject_token_typeurn:ietf:params:oauth:token-type:jwt

No client ID or client secret. The subject token authenticates the request.

JWT requirements

Before exchanging, verify your JWT satisfies all of these:

  • iss matches the issuer shown in the Portal for your registered provider.
  • aud contains the Swirls audience (swirls:aud:...).
  • The subject claim (default sub) is non-empty.
  • exp is in the future, and nbf (if present) is not.
  • The signature uses an asymmetric algorithm (RS256 for Clerk; RS256 or ES256 for Supabase) and verifies against your IdP's public keys. Symmetric algorithms are rejected.

Example

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"

Successful response

HTTP 200:

{
  "access_token": "...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 900
}

The access_token is an opaque bearer token. Do not parse or inspect it. Present it directly in API requests.

Tokens are short-lived (approximately 15 minutes). There is no refresh token. When the token expires, mint a fresh JWT from your IdP and exchange again.

SDK helper

If you call Swirls through the TypeScript SDK, skip the manual exchange. createFederatedTokenSource from @swirls/sdk/auth performs the exchange, caches the token, and re-exchanges before expiry:

import { Swirls } from "@swirls/sdk/client"
import { createFederatedTokenSource } from "@swirls/sdk/auth"

const swirls = new Swirls({
  apiKey: createFederatedTokenSource({
    getSubjectToken: () => mintIdpJwtForUser(),
  }),
})

Token helper pattern

Calling the API directly instead? Wrap the exchange in a helper that caches the token until near expiry:

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) {
    const err = await res.json()
    throw new Error(`Token exchange failed: ${err.error}`)
  }

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

  return cached.token
}

This follows the same pattern as AWS and GCP workload credential helpers: exchange once, cache until near expiry, then re-exchange.

Discovery

Swirls publishes OAuth 2.0 authorization server metadata at:

GET https://auth.swirls.ai/.well-known/oauth-authorization-server

This returns an RFC 8414 metadata document listing the token endpoint and the supported token-exchange grant. Standard OAuth client libraries can discover the endpoint from this URL.

Error reference

Errors are returned as standard OAuth JSON error objects. Error details are intentionally opaque to avoid leaking information about token validation.

HTTPerrorCause
415invalid_requestWrong Content-Type. Use application/x-www-form-urlencoded.
400unsupported_grant_typegrant_type is not the token-exchange URN.
400invalid_requestMissing or malformed subject_token or subject_token_type.
400invalid_grantUnknown issuer, invalid signature, wrong or missing audience, expired token, replayed token, or missing subject claim.
500server_errorUnexpected server error.

Authenticate to Swirls endpoints

Present the access token as a bearer token in every API request:

Authorization: Bearer <access_token>

The Swirls API base URL is https://swirls.ai/api.

The token grants access scoped to the organization that registered the identity provider. A federated principal is not an organization member; it authenticates as itself, confined to that organization's data.

Example

curl -s https://swirls.ai/api/<endpoint> \
  -H "Authorization: Bearer $SWIRLS_ACCESS_TOKEN"

See the API reference for available endpoints and their paths.

Handling token expiry

The token expires after approximately 15 minutes. API requests with an expired token receive a 401 Unauthorized response.

On a 401, mint a fresh JWT from your identity provider and exchange it for a new access token. The token helper pattern above handles this automatically when you call getSwirlsToken before each request.

Map identity claims to access roles

Federated principals can be scoped to specific agents using the access control blocks in your .swirls files. The verified claims from the JWT your principal exchanged become their access attributes. role blocks match on those claims, and policy grants decide which agents each role reaches, with workflow and tool narrowing declared per grant.

Your identity provider never assigns a Swirls role. It only asserts facts, and your deployed role blocks decide which facts to trust. The claim keys named in match rules are the only claims the policy engine reads.

Declare roles that match your claims

Put the facts in the JWT, then match on them:

role finance {
  match {
    department: "finance"
  }
}

role support_lead {
  match {
    groups: [ "support-leads" ]
  }
}

policy {
  allow finance -> agent accountant {
    workflows: [ brex, quickbooks ]
  }
  allow support_lead -> agent support
}

Declaring any policy grant flips the project to deny by default: a federated principal whose claims match no granting role cannot run the deployment.

Add the claims at your identity provider

Add the claims to the same JWT Template that carries the Swirls audience:

{
  "aud": "swirls:aud:9fK2x7...",
  "department": "{{user.public_metadata.department}}",
  "groups": "{{user.public_metadata.groups}}"
}

Store the values in Clerk user metadata and they flow into every token minted from the template.

Extend the same Custom Access Token Hook that adds the Swirls audience. Add the claims your roles match on:

claims := jsonb_set(claims, '{department}', to_jsonb(user_department));

Source the values from your own tables or user metadata.

How the claims are evaluated

  • Claims are captured at each token exchange, after signature, issuer, and audience verification. Access decisions reflect the most recent exchange, so a claim change at your identity provider takes effect the next time the principal exchanges a token.
  • A scalar claim is an equality test, a list claim is a membership test, and multiple conditions in one match block must all hold. See access control for the full semantics.
  • org_role applies to Swirls users only. Federated principals are not organization members, so match on claims you control at your identity provider instead.

Next steps

On this page