Skip to content

Admin

Connecting an Identity Provider

Wire Auth0, Clerk, Okta, WorkOS, or any OIDC provider so Alter can resolve users from their JWTs — and sync the full user directory.

The identity provider (IDP) is the external service that authenticates the app’s end users. When one is configured, Alter verifies the JWTs the IDP issues, resolves which user is making each call, and looks up that user’s grants automatically.

One IDP per app. Skip this page for backend-only apps that never see end users.

Four layers, each optional after the first:

  1. JWT verification (required) — issuer URL, JWKS URL, audience, claim mappings. Audience validation is always enabled and requires at least one audience binding: token-validation-only configurations require an explicit audience, while interactive OIDC defaults it to the client ID.
  2. OIDC sign-in (optional) — adds a sign-in flow for app.authenticate() and the Wallet.
  3. Webhook deprovisioning (optional) — real-time revocation when the IDP deletes or suspends a user, removes a group member, or deletes a group.
  4. Directory sync (optional) — bulk-imports users and groups from the IDP’s management API, then keeps them reconciled on a schedule.

Every active identity-provider configuration must bind tokens to this application. Set an explicit Audience matching the JWT aud claim, or configure the Wallet OIDC client so its Client ID becomes the audience. Either binding can be established when the provider is created, and either can be added later — but only until the provider locks, which happens the first time a user is resolved from a token it issued. After that the set of accepted audiences is frozen: adding a Wallet Client ID to a locked provider that already has an Audience is rejected, because it changes that set. Two exceptions survive the lock: a configuration that currently has no binding at all (nothing validates today, so gaining one can only restore service), and any change that leaves the accepted set identical. When both are configured, a token is accepted if its aud matches either value: access tokens carry the explicit audience, while OIDC ID tokens (used by Wallet sign-in) always carry the client ID — never the API audience. Tokens bound to neither value are rejected on every verification path, as are tokens listing additional recipients (with one exception: the issuer’s own /userinfo endpoint may accompany an accepted audience — Auth0, for example, adds it to access tokens issued with the openid scope).

Legacy configurations with neither value fail closed. Open User Authentication (OIDC) on the configured provider and save the required JWT audience; this repair remains available after users have signed in.

The Audience can be cleared, as long as the Wallet OIDC Client ID remains set — the client ID then becomes the sole accepted token audience. Clearing both is rejected while the provider is active, because an active provider with no audience binding would reject every token it issues. A disabled provider may be left without a binding — disabling is always available as the safe way to take a misconfigured provider out of service — but it cannot be re-activated until an Audience or a Client ID is set again.

Clear it when the identity provider validates the audience against its own registered list. Some providers accept only audience values registered against the OAuth application and offer no way to register one; against those, any value produces a sign-in failure such as:

Requested audience "your-value" has not been whitelisted by the OAuth 2.0 Client.

The exact wording and quoting come from the provider, so treat this as illustrative rather than a string to match on.

To clear it, open User Authentication (OIDC) on the provider, empty the JWT audience field, and save.

Clearing the field stops the audience being sent at all, which is the configuration those providers expect. Sign-in then validates the ID token against the Client ID, which is the standard OIDC behaviour.

Clearing normally changes which tokens are accepted, so it is blocked once the provider is locked — that is, once the first user has been resolved from a token it issued. The rule the platform actually applies is set-based: a change is permitted on a locked provider whenever the resulting set of accepted audiences is unchanged. The common case is an Audience that already holds the same value as the Wallet OIDC Client ID — it contributes no additional accepted value, so removing it leaves the set identical and is allowed. The same rule is why removing the wallet credentials is allowed in that situation and refused in every other.

Saving an identity provider checks the configured audience against the provider before the configuration is stored, because the audience is sent to the provider on every sign-in and a value the provider does not recognize breaks sign-in for every end user.

The check can establish that a provider rejects an audience. It cannot establish that a provider accepts one: some providers do not evaluate the audience until after the end user has signed in, so an unauthenticated check against them reaches the sign-in page without the audience having been examined at all. The outcome is therefore a rejection, “not verified”, or “nothing to verify” — never a confirmation.

The result is reported on the save, in the portal and on the alter identity-providers create command:

OutcomeWhat it means
Nothing to verifyNo audience is configured, so the Wallet OIDC Client ID is the token audience — a complete configuration with nothing to check.
Not verifiedThe provider did not identify a problem, but did not confirm the audience either — the check could not complete, or wallet sign-in credentials are not configured yet so there was no client to check with. The configuration was saved — confirm it with a test sign-in.
RejectedThe provider identified the audience as unregistered or not authorized for this application.

What a rejection does depends on what the save was changing. A save that sets the audience (or the Wallet Client ID it is checked against) is blocked — nothing is stored, and the error names the provider’s own reason. A save that does not touch either value — for example a group-claim change on a provider whose stored audience is already bad — goes through, and the rejection is reported on the result instead: blocking it would stop you fixing unrelated settings while sign-in stays broken until the audience is corrected.

The two fixes for a rejection are to set the audience to an identifier registered with the provider and authorized for this application, or to clear it so the Wallet OIDC Client ID becomes the token audience (see “Clearing the audience” above). Placeholder values are the common cause — an audience like test is accepted by the form but rejected by the provider, which is what this check catches.

ProviderPath
Auth0, Clerk, Okta, WorkOSFirst-class — pick the tab below.
Other standards-compliant OIDC providers (OneLogin, Ping, Stytch, JumpCloud, …)Use Custom OIDC — supply issuer, JWKS, audience, claim mappings.
Keycloak, Microsoft Entra ID, Amazon Cognito, Google, Firebase Auth, Supabase AuthBroker through Auth0, Clerk, Okta, or WorkOS. Direct integration is rejected at validation time.

Without directory sync, Alter learns about a user the first time that user’s JWT arrives (just-in-time provisioning). Directory sync mirrors the IDP’s entire directory ahead of time, so users and groups exist in Alter before anyone signs in — which is what makes group-bound grants assignable to people who haven’t authenticated yet, and what offboards users who will never send another JWT.

Webhooks and directory sync are complementary, not alternatives:

WebhooksDirectory sync
LatencyReal-time, per eventScheduled — every 6 hours, plus on-demand
CoverageOnly events the IDP emits after setupThe full directory, including pre-existing users
OffboardingImmediate on a delete/deactivate eventUsers absent from the directory are deprovisioned on the next sync
CredentialWebhook signing secretManagement-API credential (read-only)

Directory sync needs a management credential — a read-only credential for the IDP’s management API, created in the IDP’s own console and pasted into the Alter portal (the app’s Identity page → the identity provider card → Directory Import). The portal shows the same per-provider walkthrough inline under “How to set up directory import”.

What each provider supports:

ProviderUsers syncedGroups syncedGroup grants
OktaYesYes — Okta groups + membershipsYes — requires Group claim emits stable group IDs on the IDP config
ClerkYesYes — Clerk organizations + membershipsYes
WorkOSYes — AuthKit usersYes — WorkOS organizations + active membershipsYes — requires Group claim emits stable group IDs on the IDP config
Auth0YesYes — Auth0 Groups + memberships (directory visibility)Not supported — Auth0 group principals are not enabled for grants
Custom OIDCNot availableNot available — OIDC defines no management APINot available

Once a credential is saved, the first import can be triggered immediately with Run directory import; after that, Alter re-imports the full directory automatically every 6 hours. Each run’s outcome (counts, last error) is visible in the same portal section, and every imported, updated, or deprovisioned user lands in the provisioning audit trail.

In the Auth0 Dashboard:

  1. Pick the API or application whose JWTs Alter should accept.
  2. Note the Issuer URL and Audience. Auth0 issuers vary by tenant region:
    • https://<tenant>.us.auth0.com/ (US — default for new tenants)
    • https://<tenant>.eu.auth0.com/ (EU)
    • https://<tenant>.au.auth0.com/ (AU)
    • https://<tenant>.jp.auth0.com/ (JP)
    • https://<tenant>.ca.auth0.com/ (CA)
    • https://<tenant>.uk.auth0.com/ (UK)
    • https://<tenant>.auth0.com/ (legacy / no region prefix)
    • The tenant’s custom-domain URL when a custom domain is configured.

In the Alter portal → app’s Identity page:

  1. Add Identity Provider → paste the issuer URL → Discover. Auth0 is recognized; claim mappings pre-fill.
  2. Paste the audience. Auth0 has no built-in default audience (unlike Okta’s api://default): the value must match an API registered in your tenant under Applications → APIs. If none exists yet, create one (Create API, Identifier = exactly the audience you paste here, signing algorithm RS256). Without it, every sign-in fails at authorize with Service not found: <audience>.
  3. Group grants are not supported on Auth0, so no group claim or post-login Action is needed. Auth0 works for user sign-in and user-principal grants; group memberships still sync for directory visibility via Directory sync (step 4). If you need group-bound grants, use Okta, WorkOS, or Clerk.
  4. Add Provider.

Required only for app.authenticate() or the Wallet sign-in flow.

In Auth0: Applications → Create Application → Regular Web Application. Paste both Alter redirect URIs into Allowed Callback URLs (comma-separated). Paste the Wallet logout URL (bare URL, no query params) into Allowed Logout URLs. Copy Client ID and Client Secret.

Then authorize the application for the audience API (step 2 above): open the API under Applications → APIs and grant this application user-delegated access — the Application Access tab, or set the API’s User-delegated Access policy to allow all applications. Without this, sign-ins fail with client is not authorized to access resource server.

Confirm the application signs ID tokens with RS256: Settings → Advanced Settings → OAuth → JSON Web Token (JWT) Signature Algorithm. New applications default to RS256; an application set to HS256 fails verification, because Alter validates ID tokens against the tenant’s public JWKS and accepts only asymmetric algorithms.

In Alter: User Authentication (OIDC) section → paste credentials → Save.

Alter consumes Auth0 user lifecycle events through a Log Stream. No group Event Stream is needed for authorization because Auth0 group grants are not supported.

In Alter: Webhooks → Enable → copy the signing secret and endpoint URL (shown once).

Log Stream (user lifecycle): in Auth0, Monitoring → Streams → Create Stream → Custom Webhook. Payload URL = the Alter endpoint. Authorization Token = the signing secret (no Bearer prefix — Auth0 adds it). Set Content Type to application/json and Content Format to JSON Object. Filter by Event and subscribe only to these codes: sdu (Success User Deletion → deprovision) and ublkdu (anomaly-detection login block released → reactivates a previously-suspended user). Do not subscribe to sul — it fires on every successful login and would flood the receiver. Auth0 publishes no log code for “admin manually blocked user”, so suspension is not auto-synced from Auth0 today; lifecycle deprovisioning is reliable for explicit user deletes.

Event Stream (optional directory lifecycle): tenants with Auth0 Events API group-event delivery can point an Event Stream at the same Alter endpoint and subscribe only to user.deleted, group.deleted, and group.member.deleted. These events keep directory visibility current; they do not enable Auth0 group-bound grants.

Syncs users, Auth0 Groups (grp_…), and group memberships. Blocked Auth0 users sync as suspended; deleted users are deprovisioned on the next run.

In Auth0:

  1. Applications → Create Application → Machine to Machine Applications.
  2. Authorize the new application for the Auth0 Management API and grant it the read:users, read:groups, and read:group_members permissions.
  3. Copy the tenant Domain, Client ID, and Client Secret from the application’s Settings tab.

In Alter: the identity provider card → Directory Import → paste Domain, Client ID, and Client Secret → Save credential. The credential is verified against the tenant before it is stored; the first import can be triggered immediately with Run directory import.

  • Custom domain mismatch — JWTs use the custom domain but the portal has the tenant URL (or vice versa). Match exactly what’s in the JWT iss claim.
  • Expecting group grants — group-bound grants are not supported on Auth0. Auth0 handles user sign-in and user-principal grants; use Okta, WorkOS, or Clerk if you need group grants.
  • Wrong Management API permission — a Machine-to-Machine app without read:users is rejected when the credential is saved; missing read:groups / read:group_members saves with a notice, and imports skip the group phases until the permissions are granted. Grant the permissions under the application’s APIs tab, not on a different API.

In the Clerk Dashboard: note the instance URL (https://<app>.clerk.accounts.dev for dev, https://clerk.<custom-domain> for production). That’s the issuer.

In the Alter portalIdentityAdd Identity Provider → paste the issuer → Discover. Clerk is recognized; the user-ID claim pre-fills (sub = Clerk user ID). Paste the exact audience emitted by the Clerk session/JWT template; the token must contain that value in aud.

For group grants (groups = Clerk organizations): the group claim must carry the active organization’s ID (org_…) — that ID is what Clerk’s organization webhooks carry, so login and webhook resolve the same group and real-time revocation works. Clerk’s default v2 session token nests organization data under o (o.id, o.rol), which is not a top-level claim path, so configure a Clerk JWT template (Clerk Dashboard → JWT Templates) emitting it as a top-level claim — e.g. "groups": "{{org.id}}" — and enter that claim name in the Group Claim field before the first sign-in (claim mappings lock after first authentication). Add Provider.

Requires a production Clerk instance. In Clerk Dashboard: navigate to OAuth applicationsAdd OAuth application. Select scopes openid, profile, and email. Add both Alter redirect URIs. Copy the Client Secret from the modal immediately (Clerk does not store it and cannot show it again) and the Client ID from the application’s settings page. Paste both into the Alter portal.

Clerk needs the webhook URL first. In Alter: note the Webhook URL; do not click Enable yet.

In Clerk: Webhooks → Add Endpoint → paste the Alter URL. Subscribe to exactly these four events (others are ignored by the receiver and only add delivery noise):

Clerk eventWhat Alter does with it
user.deletedDeprovisions the user and revokes their grants
user.updatedKeeps the user’s email / display name in sync
organizationMembership.deletedRevokes group-grant access the moment a member leaves an organization
organization.deletedHandles a deleted organization (group) — revokes its group grants

Copy the Signing Secret (whsec_...). Back in Alter: toggle Enable Webhooks → paste the secret → Save.

Syncs users, organizations (as groups), and organization memberships. Banned Clerk users sync as suspended; deleted users are deprovisioned on the next run.

In Clerk: Configure → API keys → copy the Secret key (sk_live_… on production instances, sk_test_… in development).

In Alter: the identity provider card → Directory Import → paste the Secret key → Save credential. The key is verified against the Clerk Backend API before it is stored; the first import can be triggered immediately with Run directory import.

  • Dev vs production instance — OIDC sign-in requires production. JWT verification works in either, but issuer URLs differ.
  • Custom JWT template — ensure sub still carries the user ID; override the user-ID claim mapping in the portal before first sign-in if needed.
  • Publishable vs secret key — directory sync needs the Secret key (sk_…), not the publishable key (pk_…). The publishable key is rejected at save time.

In the Okta Admin ConsoleSecurity → API → Authorization Servers → pick the server (default is default). Note the Issuer URI and Audience. The host varies by tenant type:

  • https://<org>.okta.com/oauth2/default — standard production tenants (incl. trial-* and integrator-*)
  • https://<org>.okta-emea.com/oauth2/default — EMEA-region tenants
  • https://<org>.oktapreview.com/oauth2/default — preview / dev tenants
  • https://<org>.okta-gov.com/oauth2/default — Okta for Government (FedRAMP Moderate/High) tenants
  • https://<org>.okta.mil/oauth2/default — Okta for US Military (DoD IL4) tenants

For group-based authorization: edit the authorization server → Claims → add a claim — name groups, include in Access Token, value type Groups, filter Matches regex .*. This emits group names, which is enough for membership visibility.

To use group grants the claim must instead carry stable group IDs (00g…), not names — the IDs are what Okta’s group webhooks carry, so a name-keyed claim would never match and revocation would silently no-op (Alter rejects group grants on an Okta IDP without stable-ID keying). The claim must land in the ID token. There are two supported ways to emit it — pick the one matching the issuer you configured above:

  • Custom authorization server (issuer https://<org>.okta.com/oauth2/<id>, including default):
    1. Claims → add/edit the groups claim → include in ID Token, value type Expression, value getFilteredGroups({"00g…"}, "group.id", 40) (static allow-list of group IDs) or user.getGroups({'group.type': {'OKTA_GROUP'}}).![id] (dynamic, newer orgs).
    2. ScopesAdd Scope → name groups, and confirm the server’s Access Policies grant it. Required for group grants on a custom server: custom authorization servers don’t include groups by default. Alter only requests the scope if the server advertises it (via OIDC discovery), so a missing scope doesn’t break sign-in — but group grants won’t resolve until you add it here.
  • Org authorization server (issuer https://<org>.okta.com, no /oauth2/...): the org server has no Claims editor, so configure the claim on the OIDC app — the app’s Sign On tab → OpenID Connect ID TokenGroup claims (type Expression, same value as above). The groups scope is built-in on the org server, so no extra scope step is needed.

In both cases, tick Group claim emits stable group IDs when adding the provider in Alter.

In the Alter portalIdentityAdd Identity Provider → paste the issuer URI → Discover. Okta is recognized. Paste the audience. Map the group claim (and set Group claim emits stable group IDs if using group grants) before the first sign-in — claim mappings lock after first authentication. Add Provider.

In Okta: Applications → Create App Integration → OIDC → Web Application. Sign-in redirect URIs = both Alter redirect URIs. Sign-out redirect URIs = the Wallet logout URL. Assign to the groups whose users should be able to sign into Alter Wallet. Copy Client ID and Client Secret. Paste into the Alter portal.

Okta owns secret generation: you set the Event Hook secret in the Okta Admin Console, then paste the same value into Alter. The order matters — clicking Enable in Alter opens a paste dialog expecting the value Okta gave you, not a generated secret.

In Alter: note the Webhook URL shown on the Identity Provider detail page; do not click Enable yet.

In Okta: Workflow → Event Hooks → Create Event Hook. URL = the Alter webhook URL. Authentication field = Authorization. Set the secret to a value you control and copy it — Okta does not retrieve it for you later. Subscribe to these events:

Okta eventWhat Alter does with it
user.lifecycle.delete.initiatedDeprovisions the user and revokes their grants
user.lifecycle.deactivateDeprovisions the user and revokes their grants
user.lifecycle.suspendSuspends the user
user.lifecycle.unsuspend, user.lifecycle.reactivate, user.lifecycle.activateRestore a previously-suspended user to active
group.user_membership.removeRevokes group-grant access the moment a member is removed from a group
group.lifecycle.deleteHandles a deleted group — revokes its group grants

Verify and Activate the Event Hook — Okta sends a GET challenge to the Alter URL automatically.

Back in Alter: toggle Enable Webhooks → paste the same secret value → Save.

Syncs users, Okta groups, and group memberships — including users in DEPROVISIONED state, so offboarding done in Okta is mirrored. Suspended Okta users sync as suspended; deactivated (DEPROVISIONED) users are deprovisioned in Alter.

In Okta:

  1. Applications → Create App Integration → API Services.
  2. In the new app: General → Client Credentials → Edit → set Client authentication to Public key / Private keyAdd key → Generate new key. Copy the private key in JWK (JSON) format — Okta shows it only once.
  3. Okta API Scopes tab → grant okta.users.read and okta.groups.read.
  4. Admin roles tab → assign the Read-only Administrator role. Okta rejects management-API calls from API service apps that hold no admin role.

In Alter: the identity provider card → Directory Import → paste the Org URL (https://<org>.okta.com), the app’s Client ID, and the private key JWKSave credential. The Key ID (kid) field is only needed when the pasted JWK omits its own kid. The credential is verified against Okta before it is stored; the first import can be triggered immediately with Run directory import.

  • Custom Authorization Server — issuer URI is https://<org>.okta.com/oauth2/<server-id>, not the org URL.
  • Missing group claim — Okta emits no groups by default. Without the claim, group memberships are not recorded and group-bound grants don’t work.
  • Group claim emits names, not IDs — group grants require stable group IDs in the claim plus the Group claim emits stable group IDs setting (see step 1). A name-emitting claim still records memberships but cannot back a group grant.
  • groups scope missing on a custom authorization server — group grants need the groups scope, which custom servers don’t include by default. Alter requests it only when the server advertises it, so a missing scope doesn’t break sign-in — but group grants silently won’t resolve until you add it (step 1). Symptom: users sign in fine, yet group-bound grants match nobody. The org authorization server has groups built-in — no action there.
  • Group claim emitted only on the Access Token — for group grants the claim must be in the ID token. On the org server the access token is opaque and can’t carry it; on a custom server, set the Expression claim to include the ID Token.
  • Group webhook events not subscribed — without group.user_membership.remove and group.lifecycle.delete on the Event Hook, group-grant revocation waits for the next directory sync instead of firing in real time.
  • Audience mismatch — default is api://default; JWTs with a different audience fail verification.
  • Missing admin role on the API service app — token exchange succeeds but every directory read returns 403, so the credential is rejected at save time. Assign Read-only Administrator to the app itself (not to a user).
  • Public key pasted instead of private — the JWK must contain the private parts (d, p, q, …). The public half from the key dialog cannot sign the client assertion and is rejected at save time.

WorkOS exposes two issuers per environment, and picking the right one matters:

  • Connect issuerhttps://<slug>.authkit.app (WorkOS Dashboard → Connect). A standards-compliant OIDC authorization server that issues id_tokens. Use this issuer — it supports every layer below, including OIDC sign-in.
  • Native AuthKit issuerhttps://api.workos.com/user_management/<client_id>. Verifies AuthKit access tokens, but its token endpoint returns no id_token, so OIDC sign-in (Wallet / app.authenticate()) does not work against it. Use it only for JWT verification of an app that already authenticates with AuthKit directly.

Both issuers publish OIDC discovery documents; Alter resolves the JWKS URL from discovery automatically (the native issuer’s JWKS lives at https://api.workos.com/sso/jwks/<client_id> — not under the issuer path — which is why manual JWKS guesses fail and discovery is the supported path). Custom auth domains change the issuer to the organization’s own domain; those are detected as Custom OIDC, same as Okta custom domains.

In the WorkOS DashboardConnect → Applications (the Connect item in the sidebar’s Products section — not the top-level Applications page, which manages the native AuthKit application): create (or open) the Connect application.

The Issuer URL is the environment’s AuthKit domain, found under WorkOS Dashboard → Domains (Developer section) → AuthKit. It is not displayed on the Connect application page. WorkOS shows only the bare domain, without a scheme — for example youthful-ginger-43.authkit.app (staging environments use an auto-generated phrase). Paste it into Alter exactly as WorkOS shows it — the portal prepends the https:// scheme for you, so the stored issuer is the full URL (typing the full URL yourself works too):

WorkOS Domains page showsStored issuer (Alter adds the scheme)
youthful-ginger-43.authkit.apphttps://youthful-ginger-43.authkit.app

To verify the value, open https://<domain>/.well-known/openid-configuration in a browser — the issuer field must match the stored issuer exactly.

In the Alter portalIdentityAdd Identity Provider → paste the issuer → Discover. WorkOS is recognized; the user-ID claim pre-fills (sub = the AuthKit user ID, user_…). Paste the token’s exact aud value as the Audience.

For group grants (groups = WorkOS organizations): enter org_id in the Group Claim field and tick Group claim emits stable group IDs. WorkOS access tokens carry the active organization’s stable ID (org_…) in the org_id claim — the same ID its organization_membership.* webhooks carry, so login and webhook resolve the same group and real-time revocation works. The claim is a single value (one active organization per session), which Alter handles. Configure this before the first sign-in — claim mappings lock after first authentication. Add Provider.

Requires the Connect issuer (see above). In WorkOS: Connect → Applications → open the application → add both Alter redirect URIs to Redirect URIs. Copy the Client ID and Client Secret from the creation dialog — the secret is displayed only at creation, and only when “Use PKCE” was left unchecked. A PKCE (public) application is never issued a client secret, the creation flow then shows nothing, and the setting cannot be changed afterwards — recreate the application without PKCE. (Alter sends a PKCE code challenge on every sign-in regardless; the unchecked box only means a client secret exists too.)

If the secret was not saved at creation, the dashboard has no re-mint UI. Create a new secret with the WorkOS API, using the environment’s secret API key (sk_…, from the API Keys page) and the application’s app_… ID (shown under the application’s name):

Terminal window
curl -X POST "https://api.workos.com/connect/applications/<app_id>/client_secrets" \
--header "Authorization: Bearer <sk_api_key>"

The response’s secret field is the Client Secret, returned only this once — store it immediately. Paste the Client ID and Client Secret into the Alter portal.

WorkOS has no RP-initiated logout endpoint, so there is no Wallet logout URL to register — signing out of the Wallet ends the Alter session only.

WorkOS owns secret generation: the signing secret is issued when the webhook endpoint is created in the WorkOS Dashboard, then pasted into Alter. In Alter: note the Webhook URL; do not click Enable yet.

In WorkOS: Webhooks → Create WebhookEndpoint URL = the Alter URL. Subscribe to exactly these events:

WorkOS eventWhat Alter does with it
user.deletedDeprovisions the user and revokes their grants
user.updatedKeeps the user’s email / display name in sync
organization_membership.deletedRevokes group-grant access the moment a member leaves an organization
organization_membership.updatedHandles membership deactivation (the event’s status becomes inactive) — treated as a removal; other status changes are ignored
organization.deletedRevokes every grant bound to the organization’s group, then removes the group

Copy the webhook’s Signing Secret. Back in Alter: toggle Enable Webhooks → paste the secret → Save.

Syncs AuthKit users, WorkOS organizations (as groups), and active organization memberships (inactive/pending memberships are not imported — they must not confer group access). AuthKit users have no suspended state; users deleted in WorkOS are deprovisioned on the next run.

In WorkOS: API Keys → copy the environment’s secret API key (sk_…). Production keys are shown once at creation — regenerate the key if it was not saved.

In Alter: the identity provider card → Directory Import → paste the API key → Save credential. The key is verified against the WorkOS User Management API before it is stored; the first import can be triggered immediately with Run directory import.

  • Native issuer used for sign-inhttps://api.workos.com/user_management/<client_id> verifies access tokens but returns no id_token, so Wallet / app.authenticate() sign-in fails against it. Use the Connect issuer (https://<slug>.authkit.app).
  • Guessing the JWKS URL — the native issuer’s JWKS is at https://api.workos.com/sso/jwks/<client_id>, not <issuer>/.well-known/jwks.json. Alter uses OIDC discovery, which resolves it correctly; hand-configured JWKS URLs are the thing that breaks.
  • Group claim not set to org_id — WorkOS pre-fills no group claim. Without org_id (plus Group claim emits stable group IDs), organization memberships are not recorded and group grants cannot be created.
  • dsync.* events subscribed — wrong ID space; they are ignored and only add delivery noise (see the warning above).
  • View-once production API key — production sk_… keys cannot be re-displayed. Store the key when it is created, or regenerate it for the directory-sync credential.
  • “Use PKCE” checked on the Connect application — a PKCE (public) application is never issued a client secret: the creation dialog shows no secret, the setting cannot be changed afterwards, and WorkOS rejects API secret-minting with Cannot create client secrets for applications that use PKCE. Recreate the application with the checkbox unchecked. (Alter sends a PKCE code challenge on every sign-in regardless — the unchecked box only means a client secret exists too.)
  • Organization has no active members — an import whose card shows the group but a synced-user count of 0 usually means the WorkOS organization has no active members: an un-accepted (pending) invitation is not imported. Add the member in Organizations → (org) → Members, then re-run the import. On repeat runs, unchanged counts are healthy idempotence (an import that finds nothing new records nothing new), not a failure.
  • Custom auth domain — a CNAME’d AuthKit domain changes the issuer to the custom domain, which is detected as Custom OIDC (webhooks / directory sync are then unavailable). Use the authkit.app issuer for first-class support.

For any OIDC-compliant provider not listed above (OneLogin, Ping, Stytch, JumpCloud, etc.). Providers in the brokered-only row of the support table — Keycloak, Microsoft Entra ID, Amazon Cognito, Google, Firebase Auth, Supabase Auth — are rejected at validation time by Custom OIDC and must instead be brokered through Auth0, Clerk, Okta, or WorkOS using the broker’s enterprise-connection feature.

Find these from the provider’s documentation:

  • Issuer URL (the value of the iss claim).
  • JWKS URL (usually <issuer>/.well-known/jwks.json per OIDC discovery).
  • Audience (the value of the aud claim).
  • User-ID claim (default sub).
  • Group claim (provider-specific — common values: groups, cognito:groups, roles).

In the Alter portalIdentityAdd Identity Provider → paste the issuer URL → Discover. When OIDC discovery is exposed at <issuer>/.well-known/openid-configuration, Alter pulls JWKS automatically. Paste the exact token Audience, then override claim mappings when the JWT uses non-standard names. Set group / role claims before any user signs in — they lock after first authentication.

Create an OIDC application (often called “Web App” or “Confidential Client”) at the provider. Add both Alter redirect URIs as callbacks. When the provider supports post_logout_redirect_uri, add the Wallet logout URL. Paste Client ID and Client Secret into the Alter portal.

In Alter: Webhooks → Enable → copy the secret and endpoint. In the IDP, configure a webhook subscribing to user-deleted / user-disabled events with the secret as an Authorization header.

When the IDP supports no webhooks, deprovisioning waits until the deleted user’s JWT expires.

Not available for Custom OIDC — OIDC standardizes authentication, not a management API, so there is no directory to read. Users are provisioned just-in-time on first sign-in; offboarding relies on webhooks (when the provider has them) or JWT expiry.

  • Brokered-only providers fail at save time. Issuer URLs from Keycloak, Entra, Cognito, Google (accounts.google.com), Firebase, or Supabase are rejected. Use the broker pattern instead (Auth0 / Clerk / Okta / WorkOS with an enterprise connection).
  • Audience formatting — some providers send a string, some an array. Alter accepts a string or a single-element array matching a configured value (explicit audience or OIDC client ID). Arrays listing additional recipients are rejected — except the issuer’s own /userinfo endpoint alongside an accepted value.

Deleting a provider with active users uses Force delete: Alter first marks the provider as deleting, then deprovisions users and revokes their grants before removing the configuration and Vault references. While that marker exists, sign-ins, directory mutations, webhook changes, and configuration edits are blocked.

If a provider or Vault failure interrupts the cascade, the card remains visible as Deleting. Use Resume force delete after correcting the underlying failure; the cascade is idempotent and continues from the remaining active users.

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.