Skip to main content

Claude Desktop & Cowork

Tasks

Claude Desktop and Cowork can authenticate to AIControls with each user's own work account instead of a shared credential. The app runs the sign-in flow against your identity provider, then presents the resulting token on every request. AIControls validates it and attributes the request to that person — so audit, budgets, and model policy all work per-user, with your IdP's MFA and conditional-access rules applying at sign-in.

Why this instead of an access token

Handing every user a personal access token also works, and is simpler to set up. Gateway SSO is worth the extra configuration when you want:

  • No credential to distribute or rotate. Users sign in with the account they already have.
  • IdP-enforced access. MFA, conditional access, and device compliance are evaluated at sign-in. Revoking someone in the IdP cuts off gateway access on their next token refresh.
  • Attribution without an enrollment step. The first request from a new user is already attributed.

Prerequisites

  • Claude Desktop 1.6889.0 or later (earlier versions do not support gateway SSO)
  • Ability to register an application in your identity provider
  • Ability to deploy a managed configuration (MDM) to your users' machines
  • AIControls admin access, to reach Settings → SSO/OIDC

How it works

Claude Desktop runs the OIDC authorization-code flow with PKCE in the user's browser, against your identity provider. It then sends the resulting token, unchanged, as Authorization: Bearer <token> on every request to AIControls.

AIControls acts as a stateless resource server. It never participates in the sign-in flow and never issues a token of its own. On each request it:

  1. Fetches your identity provider's signing keys (discovered from the issuer, then cached).
  2. Verifies the token's signature and expiry.
  3. Verifies the token's issuer and audience both match what you configured.

That third step is what makes the configuration below more than boilerplate. A token's signature only proves your identity provider minted it — not that it was minted for AIControls. Without an audience check, a token issued to any other application in your tenant would be accepted here, and that application could replay it. AIControls will not let this be left out by omission: a provider with no audience configured logs a warning naming itself at startup and rejects every token at request time. The proxy still boots — so an upgrade cannot be taken down by a configuration that predates the requirement — but that provider authenticates nobody until it is corrected.

Step 1 — Register the application with your IdP

Create a public client app registration for Claude Desktop:

  1. In the Entra admin center, go to App registrations → New registration.
  2. Under Redirect URI, choose the Mobile and desktop applications platform and add http://127.0.0.1/callback.
  3. Under API permissions, ensure the delegated scopes openid, profile, email, and offline_access are granted. (offline_access is what returns a refresh token — see Token refresh.)
  4. Copy the Application (client) ID and the Directory (tenant) ID — you need both below.
warning

Use 127.0.0.1, not localhost — most IdPs do not treat them as interchangeable (RFC 8252 §7.3).

Keep the /callback path. Entra wildcards the port for loopback redirect URIs but, despite what Microsoft's documentation says, does not wildcard the path in practice: http://127.0.0.1/callback matches whichever port the app picks, while http://127.0.0.1 alone fails with AADSTS50011.

danger

Register a public client, not a Web or confidential client. Claude is a public PKCE client and sends no client secret. A confidential registration rejects the token exchange with Token exchange failed (HTTP 401).

To include group membership in the token (needed if your model-access policies or budgets are group-scoped), go to Token configuration → Add groups claim and choose the group types to emit.

Optionally add an email claim. Entra omits email from ID tokens unless you ask for it: Token configuration → Add optional claim → ID → email. AIControls works without it — see Step 4 — but adding it keeps user identifiers readable in the audit log.

Step 2 — Choose a token type

The app can send either kind of token. This choice determines the audience value you configure in Step 3.

ModeWhat the app sendsAudience valueWhen to choose it
id_token (default)The OIDC ID tokenThe Claude Desktop client IDStart here. Nothing extra to register.
access_tokenAn OAuth access tokenYour gateway's API scope / resource identifierYour organization requires access tokens for API calls, or you want gateway access to be a separately consentable scope.

In access_token mode the client must also be configured with scopes set to the gateway's registered API scope — it is required in that mode, not optional.

On Entra ID that means your gateway's own app registration must Expose an API with a custom scope, and the client's registration must be granted that scope as a delegated permission. Missing either half fails sign-in with AADSTS65001. You cannot use a Microsoft Graph access token: Graph tokens are not validatable by a third party.

access_token mode is also the way around the Google Workspace refresh caveat below.

tip

On Entra ID in access_token mode, set "accessTokenAcceptedVersion": 2 in your API app registration's manifest. Version 1 access tokens are issued with a different issuer value (https://sts.windows.net/TENANT/) than the one your v2 issuer URL advertises, so validation rejects them. If you cannot change the manifest, add a second provider entry using the v1 issuer.

Step 3 — Configure AIControls

Gateway SSO reuses your SSO / OIDC identity provider configuration. How much you need to configure depends on which app registration the client uses.

Nobody generates a Client ID here. Your identity provider mints it when you create the app registration in Step 1; you paste that same value into the client's configuration and, if it has its own registration, into AIControls.

warning

The client signs in as a public client with PKCE and no client secret, so it needs an app registration of that type. Only Microsoft Entra ID lets one registration serve both the portal and this flow. Google (Web vs Desktop app) and Okta (Web vs Native) fix the client type at creation, so on those providers a dedicated registration is required — see Step 1.

  1. Go to Settings → SSO / OIDC.
  2. If it is not configured yet, choose your provider and enter the Tenant ID (Entra) or Okta domain, plus the Client ID of your portal application. Toggle Enabled.
  3. Under AI client sign-in, enter the client's Client ID in Client ID of the AI app's own registration. Add more than one if several apps have their own registrations.
  4. Click Save. Changes apply immediately — no restart.

The portal's own Client ID stays accepted; these are additive. That is what lets one identity provider configuration serve both the builder portal and the gateway.

warning

Only list applications you control. Every entry widens which tokens the gateway accepts, and any application whose audience appears here can present its users' tokens to your gateway.

One identity provider for both Claude Code and Claude Desktop

This is the usual arrangement, and everything above already covers it: a single directory and a single issuer serving both.

  • Claude Code users sign in to the developer portal against that provider and use the access token it issues.
  • Claude Desktop signs in interactively against the same provider and presents its token straight to the gateway.

The only value you add is the AI client's own Client ID, in Step 3 — or nothing at all if it reuses the portal's registration. You do not need a second provider entry.

note

One identity provider is all AIControls accepts for gateway authentication. If you genuinely need a second directory trusted at the same time — during an acquisition or a migration between identity providers — configure the extra issuers in identity.oidcProviders (see Identity provider fields), or run a separate workspace per directory.

Step 4 — Set the user identifier claim

AIControls attributes each request to a principal, and prefers the user's email address because that is what personal access tokens, portal accounts, and budget assignments key on. When the token has no email claim, it falls back to the stable user identifier.

On Microsoft Entra ID, set the User ID Claim to oid. Entra's sub claim is pairwise: it differs per app registration, so the same person authenticating through a different application appears as a different principal. oid is the tenant-wide immutable user object ID.

  1. In Settings → SSO / OIDC → Advanced, set User ID Claim to oid for Entra ID, or leave it as sub for Okta and other providers.
  2. Set Groups Claim (in the main section above) if your provider emits groups under a non-standard name — groups is the default.

Group membership is passed through unchanged, so existing group-scoped model policies and budgets apply to gateway-SSO users with no further configuration.

Step 5 — Decide whether to auto-provision users

Skip this step unless you use team-scoped model access.

Team membership lives on the portal user record, and someone who signs in only through Claude Desktop never visits the portal, so they have no record. Such a user resolves to the global model ceiling rather than their team's — meaning team-scoped model restrictions do not apply to them. They are not blocked; they are governed by the wrong ceiling.

To close that gap, turn on Auto-Provision Users (in Gateway SSO (advanced), or per-issuer on the Gateway Issuers tab). The first request from a new user then creates a builder-role portal record whose teams are derived from the token's groups via your team group bindings (Settings → Teams), falling back to the default team.

Existing users are never modified, so any teams or roles you have assigned by hand survive later sign-ins.

note

Users appear on the Identities page (Inventory → Identities) either way — that listing comes from request traffic. This setting is specifically about the portal record that carries team membership.

Step 6 — Deploy the managed configuration

Claude Desktop reads gateway SSO settings from managed configuration (MDM). Two keys matter:

  • inferenceCredentialKind — set to "interactive" to select SSO mode.
  • inferenceGatewayOidc — a JSON string (not a nested object) holding the OIDC settings.
{
"inferenceCredentialKind": "interactive",
"inferenceGatewayOidc": "{\"issuer\":\"https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0\",\"clientId\":\"YOUR_CLAUDE_DESKTOP_CLIENT_ID\",\"scopes\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"bearerTokenType\":\"id_token\"}"
}

Also point the app at your gateway URL, which you can find in Settings → Integrations.

Gateway as the authorization server

Use this when your identity provider will not issue tokens to a public PKCE client — Google always, and any provider that refuses secret-less clients. Instead of the app signing in against your IdP directly, it signs in against AIControls, which runs the IdP exchange server-side with the portal client's secret and mints its own token.

Everything in this section replaces Step 1 and Step 6. Steps 3 through 5 still apply.

Register the gateway's own callback

Because AIControls performs the IdP exchange itself here, it is the party your identity provider redirects back to — at a path of its own, separate from the one the admin console uses:

https://YOUR_GATEWAY/oauth/sso/callback

YOUR_GATEWAY here is the gateway's own public URL — the same value clients use as their Issuer URL, shown as Proxy URL on the SSO / OIDC tab. It is not the address of the admin console, which on a default install is served on a different port and can be a different hostname entirely.

Add that as an additional redirect URI on the same IdP client whose Client ID you entered under Settings → SSO / OIDC, alongside the /auth/callback value already in the Redirect URL field. Both are needed: /auth/callback for portal sign-in, /oauth/sso/callback for AI clients signing in through the gateway. This path is fixed and not configurable.

The Redirect URL field must also be filled in, even though this flow does not use its value: leaving it empty means there is no browser callback for the consent page to redirect to, so the Sign in with SSO button does not appear at all.

Every provider allows more than one redirect URI per client:

APIs & Services → Credentials → your Web application client → Authorized redirect URIs → + ADD URI. Allow a few minutes for the change to take effect.

Skip this and sign-in fails at the redirect with redirect_uri_mismatch (Google), AADSTS50011 (Entra), or invalid_redirect_uri (Okta) — after the sign-in screen appears, not before.

Get a client ID for the app

AIControls issues client IDs through dynamic client registration (RFC 7591). There is no console screen for this — register once with a single request and keep the client_id it returns:

curl -X POST https://YOUR_GATEWAY/oauth/register \
-H 'content-type: application/json' \
-d '{
"client_name": "claude-desktop",
"redirect_uris": ["http://127.0.0.1:8123/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'
{"client_id": "oc_09fe6c9b6911e6a5d2c91709eb64bfc9", "token_endpoint_auth_method": "none", ...}

token_endpoint_auth_method: "none" is what makes it a public client, which is what the app needs. Registration is unauthenticated by design, per RFC 7591 — the client ID alone grants nothing without a completed sign-in.

Pick the loopback port yourself and register the exact callback URI; the app must then be told the same port. If you see a redirect_uri mismatch, re-register with the URI the app actually used.

Configure the app

SettingValue
Gateway base URLhttps://YOUR_GATEWAY
Client IDthe oc_… value from above
Issuer URLhttps://YOUR_GATEWAY
Bearer tokenaccess token
Scopesmcp
Redirect portthe port you registered (8123 above)
warning

Bearer token must be "access token", not the "ID token" default. On this path AIControls issues an access token and no ID token, so a client left in ID-token mode has nothing to send.

This is deliberate and will not change. An ID token is an authentication assertion audienced to a client, whereas AIControls is a resource server: accepting client-audienced tokens as API credentials invites audience confusion, carries no scope to enforce least privilege, and has no revocation story. The access token issued here is opaque and its revocation is checked on every request. See Tokens minted by AIControls.

Authorization and token endpoints are discovered automatically — AIControls publishes its metadata at both /.well-known/openid-configuration and /.well-known/oauth-authorization-server, so clients that expect an OpenID Provider and clients that expect an OAuth authorization server both work. If your client asks for those URLs explicitly, they are https://YOUR_GATEWAY/oauth/authorize and https://YOUR_GATEWAY/oauth/token.

What happens at sign-in

  1. The app fetches the discovery document and registers or reuses its client ID.
  2. It opens /oauth/authorize. The page offers Sign in with SSO — which redirects to your IdP — whenever SSO / OIDC is configured with an Issuer URL, Client ID, and Redirect URL (or, rarely, a separate provider is configured under the advanced Admin SSO option). A password form is offered alongside it for local accounts.
  3. Your IdP returns to AIControls, which completes the exchange with the client secret — the step a public client cannot perform.
  4. AIControls mints an access token and the app presents it as the bearer on every /v1/messages call.

Because AIControls issues the final token, its lifetime is governed by AIControls rather than your IdP, and no client secret is ever distributed to end-user machines.

Restrictions on the SSO / OIDC provider apply to this flow as well: a Google Workspace Hosted Domain, or a GitHub organization, is checked against the token's hd claim at step 3, before AIControls issues anything. An account your IdP will happily authenticate — a personal Google account, say — cannot complete sign-in here if it falls outside the domain you set.

Verify

curl -s https://YOUR_GATEWAY/.well-known/openid-configuration | jq '.issuer, .authorization_endpoint'

The issuer must equal your gateway's public URL. If it comes back as http://localhost:8080, set proxy.mcpIssuerURL to the public URL — clients follow the advertised endpoints, and localhost values make sign-in fail after discovery succeeds.

On Kubernetes, set the Helm value:

helm upgrade aicontrols ... --set proxy.mcpIssuerURL=https://YOUR_GATEWAY

When left empty, the chart derives the issuer from your Ingress or Gateway hostname, which is correct for most installs. Set it explicitly whenever clients reach the gateway through a hostname the chart doesn't know about — an external load balancer, a CDN, or custom DNS.

caution

The issuer is the iss claim of every token AIControls mints, and the verifier matches on it. Changing it invalidates tokens already issued, so clients sign in again after the change.

Token refresh, and one Google caveat

Granting offline_access returns a refresh token, and the client refreshes the bearer silently before it expires. Google is the exception — it rejects offline_access as a scope, so a Google-backed client has no refresh token at all. When a refresh fails — the session was revoked, or the user is past the IdP's idle window — they re-authenticate in the browser.

warning

Google Workspace, id_token mode only. Google never returns an id_token on a refresh-token grant, so a Google-backed gateway in the default id_token mode prompts a browser sign-in roughly once per ID-token lifetime (about an hour).

Use access_token mode to avoid it — that mode is unaffected on every provider. Microsoft Entra ID and Okta both return a fresh id_token on refresh and are unaffected either way.

Verify

Sign in from Claude Desktop, send one message, then check Security Events (All activity tab) in the AIControls console. The request should appear attributed to the signing-in user, with their groups populated.

If group-scoped budgets or model policies do not apply as expected, open the audit event and check the user's groups are present — an empty group list means the claim is missing from the token, not that the policy is wrong.

Troubleshooting

Claude Desktop displays the gateway's error message directly, and AIControls includes the token's own issuer and audience in it — enough to tell whether the app signed in against the tenant and application you expected.

The expected values are in the proxy logs, not the response (publishing them would let anyone enumerate your configuration). When the presented values look right but the token is still rejected, check the logs for the warning naming the mismatch.

SymptomCauseFix
aud in the error is not one you configuredThe app's client ID is not registered as an accepted audienceAdd it under Additional Accepted Audiences (Step 3)
Token rejected although the issuer looks correctTrailing slash mismatch — Auth0 and Entra ID v1 publish an issuer ending in /Enter the issuer exactly as the provider publishes it
Startup log warns a provider "will reject all tokens"That provider has no audience configuredAdd an audience, or set skipAudienceCheck deliberately
iss in the error does not match the configured issuerWrong tenant, or an Entra v1 access tokenCheck the tenant ID; set accessTokenAcceptedVersion: 2, or add a provider for the v1 issuer (Step 2)
Token exchange failed (HTTP 401)Registered as a confidential/Web clientRe-register as a public (native / mobile-and-desktop) client (Step 1)
Error 400: invalid_scope, invalid=[offline_access]Google rejects offline_access as a scopeSet the client's scopes to openid profile email
client_secret is missing at the token exchangeGoogle, any client type — it requires a secret that a public PKCE client cannot sendNot configurable around; see Step 1 for the alternatives
Error 401: invalid_client, "OAuth client was not found"The Client ID is wrong or unsubstitutedCheck the value against the console
OIDC discovery failed (HTTP 401)The Issuer URL names a host that serves no discovery document. Pointing it at the gateway is correct only on versions that publish /.well-known/openid-configurationVerify with curl <issuer>/.well-known/openid-configuration; see Gateway as the authorization server
Sign-in succeeds, then Gateway returned no usable modelsNo upstream is configured, so there was nothing to discover — an authentication success, not a failureAdd an upstream under Settings → Models, or list models in the client to skip discovery
Model discovery — found 0 models, with an upstream configuredThe upstream was queried and rejected the request — most often the provider API key. An empty list is not the same as a provider that answeredUse Test Connection on the upstream, then check the proxy log for model discovery: upstream rejected the model list request and the status it names
model discovery failed for every configured upstream (HTTP 502)Every upstream the gateway asked returned an error. The message names each one and its statusFix the credential the status points at; 401 means the key was rejected, not that the model list is empty
No usable models and the token was never checkedGET /v1/models needs no authentication, so reaching it proves the sign-in flow completed, not that the token was acceptedSend one message: 402 (license gate) or 200 both mean the token was accepted; 401 means it was not
AADSTS50011 — redirect URI mismatchThe /callback path is missing; Entra wildcards the port but not the pathRegister http://127.0.0.1/callback exactly (Entra). Okta: register the exact port you pinned with redirectPort
Redirect mismatch naming …/oauth/sso/callback, on the gateway-as-AS pathThe gateway's own callback is not registered on the IdP client — it is a second URI, not a replacement for the one in Redirect URLAdd https://YOUR_GATEWAY/oauth/sso/callback to that client, using the gateway's URL and not the console's (details)
Redirect mismatch naming the console's host or portThe registered URI was built from the admin console's address; the gateway derives this callback from its own public URLRegister the URI under the gateway's URL — the Proxy URL value on the SSO / OIDC tab
/oauth/authorize shows only a password form, no Sign in with SSONo provider is usable for it: SSO / OIDC is either unset or missing a Redirect URL, and no separate provider is configured under the advanced Admin SSO option either. A provider whose discovery endpoint is unreachable also yields no buttonFill in SSO / OIDC's Issuer URL, Client ID, and Redirect URL; then check the proxy logs for an OIDC discovery warning naming the issuer
Browser sign-in prompt roughly every hourGoogle Workspace in id_token mode — Google returns no id_token on refreshSwitch the client to access_token mode (details)
AADSTS65001 in access_token modeThe gateway app does not Expose an API, or the client lacks that delegated permissionDo both, and set the client's scopes to that API scope
Requests rejected for carrying no usable principalToken has neither an email nor the configured user identifier claimSet User ID Claim to a claim the token actually contains — oid on Entra (Step 4)
Intermittent expiry rejectionsClock drift between your IdP and the proxyRaise Clock Skew Tolerance (default 60 seconds)
Team model limits not applying to a userNo portal record, so the global ceiling is usedEnable Auto-Provision Users (Step 5)

Model discovery

Claude Desktop discovers the models your gateway offers from GET /v1/models. Alongside the standard fields, AIControls reports anthropic_family_tier (opus / sonnet / haiku) and supports1m for Anthropic models, so the app can tell which Claude tier an opaque model ID maps to and which models have the 1M-token context window. This works for gateway aliases and for Bedrock/Vertex model IDs. No configuration needed — see Model discovery fields.

Which models the app offers depends on how the upstream is configured. With Live Model Discovery enabled, the gateway asks the provider directly and returns only what that provider currently serves. With it disabled and no explicit model list, the gateway falls back to its pricing catalogue, which retains retired model IDs so historical spend can still be costed — so the picker may offer models the provider no longer accepts. Enable Live Model Discovery, or set an explicit model list on the upstream, to pin the menu.

The list is not filtered by your model allowlists. Allowlists are enforced when a request is made, so a model can appear in the picker and still be denied on use.

How Cowork sessions are attributed

Claude Desktop runs Cowork sessions by embedding the Claude Code binary, so requests from a Cowork task and from Claude Code in a terminal come from the same program. The audit log distinguishes them: Cowork sessions appear as Claude for Work, terminal Claude Code as Claude Code, and requests from the desktop app itself — generating a session title, for example — as Claude Desktop. Expect one Cowork session to produce more than one client label, since the app and its embedded agent both make calls.

Configuration reference

Self-managed deployments can configure providers in YAML instead of the console. Every provider must specify an audience:

identity:
oidcProviders:
- name: entra-gateway
issuer: https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0
# Accept both the portal's client ID and Claude Desktop's.
audiences:
- YOUR_PORTAL_CLIENT_ID
- YOUR_CLAUDE_DESKTOP_CLIENT_ID
userIDClaim: oid # Entra: `sub` is pairwise per app registration
groupsClaim: groups
clockSkewSeconds: 60
autoProvision: true # only needed for team-scoped model access
autoProvisionFallback: none # groups that map to no team get no team,
# not the (possibly permissive) default one
usage: [llm] # accept these tokens at the LLM gateway only

See the Identity provider fields reference for every available field.

warning

Entra ID emits group GUIDs, not display names. If your team group bindings are written against names they will match no user, and everyone auto-provisioned lands on the autoProvisionFallback path. With the default setting that means the default team — which, if it is permissive, grants more model access than the user's real team would, silently. Bind against the GUIDs Entra actually sends, and see When no group binding matches.