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.
| Environment | Format | Example |
|---|---|---|
| Production | clerk.yourdomain.com | clerk.acme.com |
| Development | your-app-name.clerk.accounts.dev | acme-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 theiraudclaim. - Issuer: the
issvalue 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
| Parameter | Value |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Your IdP-issued JWT |
subject_token_type | urn: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:
issmatches the issuer shown in the Portal for your registered provider.audcontains the Swirls audience (swirls:aud:...).- The subject claim (default
sub) is non-empty. expis in the future, andnbf(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-serverThis 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.
| HTTP | error | Cause |
|---|---|---|
| 415 | invalid_request | Wrong Content-Type. Use application/x-www-form-urlencoded. |
| 400 | unsupported_grant_type | grant_type is not the token-exchange URN. |
| 400 | invalid_request | Missing or malformed subject_token or subject_token_type. |
| 400 | invalid_grant | Unknown issuer, invalid signature, wrong or missing audience, expired token, replayed token, or missing subject claim. |
| 500 | server_error | Unexpected 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
matchblock must all hold. See access control for the full semantics. org_roleapplies to Swirls users only. Federated principals are not organization members, so match on claims you control at your identity provider instead.
Next steps
- Access control:
roleandpolicyblocks, match semantics, and enforcement. - API reference: all available API namespaces and methods.
- SDK reference: typed TypeScript client.
- Swirls Cloud: managed runtime and Portal.