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 dashboard
In the Swirls dashboard, 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 dashboard. 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 dashboard.
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 dashboard 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 with an error code and an error_description that names the specific problem with the request you made:
{
"error": "invalid_grant",
"error_description": "no key in the identity provider's published JWKS matches this token (kid \"a1b2c3\", alg \"RS256\"); the signing key is most likely a legacy or rotated-out key that is not published"
}The description always describes the token you presented, never the registration it was aimed at. The audience issued for your registration is never echoed back, so a failed exchange cannot be used to discover it.
| 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, unaccepted signing algorithm, no matching key in the JWKS, invalid signature, wrong or missing audience, expired token, replayed token, or missing subject claim. Read error_description for which one. |
| 503 | server_error | Your identity provider's JWKS endpoint could not be reached. Retry. |
| 500 | server_error | Unexpected server error. |
invalid_grant on every exchange
Read error_description first, then work down this list:
- "no key in the identity provider's published JWKS matches this token" — your IdP is signing with a key it does not publish. On Supabase this is almost always the legacy shared JWT secret: creating an asymmetric standby key publishes it at
/auth/v1/.well-known/jwks.json, but tokens keep being signed with the legacy HS256 secret until you promote that key to current in Project Settings → JWT Keys. Decode your JWT and compare its headerkidagainst thekidvalues your JWKS endpoint serves. - "signed with
HS256, which is not accepted" — same root cause, caught one step earlier. Swirls only accepts asymmetric signatures, so symmetric signing must be turned off at the provider. - "
audclaim does not include the audience issued for this registration" — copy the audience again from the dashboard. Anaudarray is fine: your provider's own value (Supabase'sauthenticated, for example) can sit alongside the Swirls audience. - "no identity provider is registered for issuer" — the
issin your token does not match any registration. Check the project ref or Frontend API domain you registered.
Claim values themselves are never a cause. Custom claims may be null, empty strings, arrays, or nested objects; only iss, aud, exp, nbf, and the subject claim are validated.
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 dashboard.