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.
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 and project. In the project sidebar, open Auth. You must be an organization owner or admin. To define a reusable organization provider first, open Auth from the organization menu.
Register a provider
On project Auth, click Inherit provider beside an organization provider, or Register provider to configure your own. On organization Auth, Register provider creates a reusable default. Select your identity provider. Swirls currently supports Clerk and Supabase.
You can also register programmatically with the SDK idp client. Pass projectId to idp.list, idp.register, and idp.remove for project configuration; omit it for organization configuration. Use idp.inherit({ projectId, id }) to select an organization provider for a project. An issuer can be registered in multiple projects, with a different audience in each project. Duplicate registrations within one project are 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 registering or inheriting a project provider, the binding appears with a Pending status. Organization defaults show Organization default; select them from a project to provision an exchange audience. It becomes Active automatically after the first successful token exchange.
Copy the two values shown:
- Project 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 this project's Swirls audience in the aud claim. Other non-Swirls audiences may coexist, but a token that targets multiple registered Swirls projects is rejected. Use a separate JWT template or trusted hook configuration for each project; do not let callers choose an arbitrary project audience.
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.
Organization defaults and project overrides
An organization provider is reusable configuration. Each project explicitly selects it with Inherit provider, receiving its own audience, activation state, and federated identities. Organization registration alone grants no project access, and its audience cannot exchange for an organization-wide credential.
A project can inherit one issuer and register another independently. The same issuer can have only one binding in a project. To change that issuer between inherited and project-specific configuration, remove the project binding, add its replacement, and update the IdP JWT audience. There is no automatic fallback to organization configuration.
Inherited bindings copy the provider settings and retain a link to the organization default. Configuration changes use remove and register: replacing an organization default requires each project to select the replacement. Removing a project binding leaves the organization default intact. Removing an organization default revokes all its inherited bindings and their issued credentials on the next verification; independent project providers remain intact.
Configure with the SDK
Use an organization owner/admin session credential for these management calls. A federated workload credential cannot configure providers.
import { Swirls } from "@swirls/sdk/client"
const admin = new Swirls({ apiKey: ownerOrAdminSessionToken })
const source = await admin.client.idp.register({
provider: "clerk",
instanceIdentifier: "clerk.acme.com",
})
const binding = await admin.client.idp.inherit({ projectId, id: source.id })
// Configure the IdP template for this project with binding.audience.
const { providers } = await admin.client.idp.list({ projectId })
// Or register an independent provider directly in a project:
const independent = await admin.client.idp.register({
projectId: otherProjectId,
provider: "clerk",
instanceIdentifier: "clerk.acme.com",
})
// independent.audience differs from binding.audience.idp.list({}) lists organization defaults. idp.remove({ projectId, id }) removes a project binding; omitting projectId targets the organization default and cascades to its inherited bindings. See the SDK reference for response fields and scope rules.
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. Fabric selects the project from the verified iss and aud claims. There is no projectId exchange parameter; supplying a project in a later SDK call cannot change the token's scope.
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: () => mintIdpJwtForProjectUser(),
}),
})Token caching
Keep each token source or cache bound to one project registration and one subject. Create separate sources for different users or projects. A process-wide cache that returns one caller's credential to another caller crosses the identity boundary.
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. - "subject_token must identify exactly one project registration" — check both
issandaud, and ensure the token names only one registered Swirls project audience.
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 is confined to the project that registered the identity provider, including when another project belongs to the same organization. A federated principal is not an organization member. It can call supported workload APIs (agent sessions, workflow execution, queries, views, streams, disks, and database rows), but cannot administer organizations, configure providers, or deploy projects. Web chat session management, streaming, resume, and session realtime grants accept these credentials within their registered project. Workflow discovery, execution status/replay, authorized workflow/agent traces, execution realtime grants, review listing/details/approve/reject, file uploads/previews, and project run/review event subscriptions are also supported.
The supported application calls are:
| Application surface | Federated operations |
|---|---|
| History and updates | projects.listExecutions, deployments.listExecutions, projects.mintActorProjectGrant (content-free invalidations) |
| Workflows | listWorkflows, getWorkflowSnapshot, listRunnableWorkflows, executeWorkflow, getExecution, getExecutionReplay, getExecutionTrace, mintActorExecutionGrant |
| Approvals | reviews.listReviews, getReview, approveReview, rejectReview |
| Files in workflow inputs, reviews, and chat | fileValues.createUploadGrant, initUpload, completeUpload, previewFile |
| Data and disks | View/stream/database discovery and the documented row/schema/query APIs; shared-disk discovery, directory/tree listing, and file reads |
| Live workflow updates | Authenticated GET /projects/{projectId}/runs/events, /reviews/events, and /traces/{correlationId}/events on the API |
Standalone workflow runs, files, and the review queue are shared within the registered project. Reviews remain a project-wide approval queue, including reviews requested by workflows started in chat; expose approval actions only to your application’s reviewers. A federated reviewer may approve or reject its pending reviews; the decision records that federated principal as reviewedBy. Your application's server must decide which users may obtain the project's audience and expose those review actions. There is no reviewer-assignment permission model. Private chat sessions retain their separate end-user ownership checks. Workflows started from private chat inherit that session’s owner/participant checks, including history, counts, replay, latest-run summaries and live run events. Traces accept workflow roots and agent-session roots that the caller may read, and filter every actor/event to that project and organization.
A repeated or concurrent decision on a completed review returns 409 Conflict. A resource outside the credential's project returns not-found or an empty list. A successfully exchanged token does not grant organization administration, deployment, connection-secret access, or arbitrary SQL execution.
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.
The SDK token source refreshes before its cached token expires. It does not automatically retry API requests after a 401. If a binding was removed, re-exchanging fails too; configure a valid project binding and its audience before retrying. Recreate the token source when changing a user's identity or project registration.
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.
Moving from organization configuration
Existing organization registrations are preserved as reusable defaults. Their old organization-wide credentials are rejected. For each intended project, select Inherit provider on its Auth page (or register its own provider), update that project's JWT template or hook with the new project audience, and exchange a fresh token. Do not reuse the organization audience or share a project audience across projects.
Removing an organization provider also removes all bindings inherited from it; independent project providers remain intact. Removing an inherited binding from one project leaves the organization provider and other projects intact. To override an inherited provider for the same issuer, remove that project binding and register its replacement; a new audience is issued.
Removing a project provider prevents new exchanges and invalidates its previously issued federation credentials on their next verification. Claims are isolated per registration, even for the same issuer and subject in multiple projects.
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.