> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Air-Gapped Devin SSO with Your IdP

> Require login to air-gapped Devin Desktop and CLI through your own OIDC identity provider: auth file, client settings, group gating, audit events, IDP codes.

> [!NOTE]
>
> This documentation is for the federal deployments of Devin. [Back to Devin Docs](/get-started/devin-intro)

By default, access to the air-gapped build is controlled only by the [license file](/federal/air-gapped/license). You can additionally require each user to sign in to **your own OIDC identity provider**. SSO is layered on top of the license: the license is still required, and the login flow runs against the issuer you configure.

## Create the auth file

| | |
| - | - |
| Default path | `/etc/devin/auth.json` |
| Default path (Windows) | `C:\ProgramData\Devin\auth.json` |
| Override | `DEVIN_AUTH_FILE=<path>` |

The file selects exactly one mode. If the file does not exist, the product behaves as if it contained `"mode": "license"`.

```json /etc/devin/auth.json theme={null}
{
  "schema_version": 0,
  "mode": "sso",
  "issuer": "https://sso.example.com",
  "client_id": "devin-cli",
  "scopes": ["openid", "profile", "email"],
  "groups_claim": "groups",
  "required_group": "devin-users",
  "additional_endpoint_hosts": []
}
```

| Field | Required | Meaning |
| - | - | - |
| `schema_version` | yes | Must be `0`. |
| `mode` | yes | `"sso"` to require login, or `"license"` for license-only access (the default when no file exists). |
| `issuer` | yes (sso) | The OIDC issuer URL. Must be `https://` (plain `http://` is accepted only for loopback hosts) with no query string, fragment, or embedded credentials. Discovery is performed at `<issuer>/.well-known/openid-configuration`. |
| `client_id` | yes (sso) | The client you register at the IdP. |
| `scopes` | no | Defaults to `openid profile email`. Must include `openid` if set. `offline_access` is requested automatically when the IdP advertises it so that a refresh token is granted. |
| `groups_claim` | no | ID-token claim that carries group membership. Defaults to `groups`. |
| `required_group` | no | If set, login is refused unless this value appears in the groups claim. |
| `additional_endpoint_hosts` | no | Extra hosts on which the IdP's discovery document may advertise endpoints. Leave empty unless you need it; see [Discovered endpoints must stay on the issuer](#discovered-endpoints-must-stay-on-the-issuer). |

Unknown fields are rejected. In `license` mode only `schema_version` and `mode` are allowed.

> [!TIP]
>
> If your IdP uses a certificate issued by a private CA, set `DEVIN_CA_BUNDLE` to a PEM bundle so the CLI trusts it; see [Network and certificates](/federal/air-gapped/network-and-certificates).

## Register the client at your IdP

The CLI runs the OIDC **authorization-code flow with PKCE (S256)** as a **public client**; there is no client secret. The client you register must meet these requirements:

1. A **public** client (no client secret) with the ID you put in `client_id`, for example `devin-cli`, with the authorization-code grant enabled and PKCE method `S256`.
2. `http://127.0.0.1/callback` registered as a valid redirect URI. The CLI listens on a random loopback port (`http://127.0.0.1:<port>/callback`) for the browser flow and uses the fixed `http://127.0.0.1/callback` for the manual flow. Per RFC 8252 the IdP should ignore the loopback port; if yours does not, register a matching wildcard.
3. If you want to use `required_group`, the ID token must carry the group-membership claim named in `groups_claim` (`groups` by default).
4. The IdP returns an ID token on refresh (most do when the `openid` scope is granted), and the ID-token lifetime for this client is short. See [How revocation propagates](#how-revocation-propagates) for why.

## Sign in

Log in from a terminal on the workstation:

```sh theme={null}
devin auth login
```

```text theme={null}
Opening your browser to sign in with your identity provider...
If the browser does not open, visit:

  https://sso.example.com/authorize?client_id=devin-cli&...

(Use `devin auth login --force-manual-token-flow` on headless machines.)
Logged in as jsmith via https://sso.example.com.
Token expires at 2026-03-02 15:04:05 UTC.
```

On a headless host or over SSH, use the manual flow. The CLI prints a URL that you open in a browser on any machine; after signing in, the browser is redirected to a `127.0.0.1` URL that will not load. Copy that full URL (including its query parameters) and paste it back into the terminal.

```sh theme={null}
devin auth login --force-manual-token-flow
```

```text theme={null}
Open this URL in a browser (on any machine) and sign in:

  https://sso.example.com/authorize?client_id=devin-cli&...

After signing in, the browser is redirected to a 127.0.0.1 URL that will not load — copy that full URL (including its query parameters) and paste it here.
Paste the full redirect URL:
```

Devin Desktop uses the same login through the bundled agent; a user who is not signed in is prompted to sign in, in Devin settings, before starting work.

## Check status and sign out

```sh theme={null}
devin auth status
```

```text theme={null}
Auth mode: SSO (issuer: https://sso.example.com)
Status: logged in
User: jsmith
Subject: 6f1c2a0e-...
Groups: devin-users
Token expires at 2026-03-02 15:04:05 UTC.
```

If the cached login is missing or can no longer be refreshed, the output is `Status: not logged in (<reason>)` followed by `Run \`devin auth login\` to sign in.\`

```sh theme={null}
devin auth logout
```

```text theme={null}
Logged out: local SSO tokens deleted.
```

Logout is local only: it deletes the token cache and does not contact the IdP.

## What the CLI validates

The ID token is validated locally: signature against the IdP's JWKS, `iss`, `aud`, expiry (with a 120-second clock-skew tolerance), the login `nonce`, and the group gate. The ID and refresh tokens are cached in the local credentials store with owner-only permissions (`0600`) and refreshed silently against the IdP.

When SSO is configured and no valid login exists (and silent refresh fails), the CLI exits at startup with an error telling the user to run `devin auth login`.

The CLI never follows HTTP redirects from the IdP: a `3xx` from the discovery, JWKS, or token endpoint fails the login with an error naming the endpoint, so `issuer` must point at the URLs the IdP actually serves.

### Discovered endpoints must stay on the issuer

The `authorization_endpoint`, `token_endpoint`, and `jwks_uri` learned from discovery must have the **same scheme, host, and port** as `issuer` (default ports count as equal). An endpoint on any other host is refused and login fails with an error like:

```text theme={null}
IdP discovery document advertises jwks_uri 'https://keycloak.corp.internal:8443/realms/devin/protocol/openid-connect/certs'
on host 'keycloak.corp.internal:8443', which is not the configured issuer's host 'sso.corp.example:443';
refusing to trust it. If this IdP really serves that endpoint from a different host,
add "keycloak.corp.internal" to `sso.additional_endpoint_hosts` in the auth file
```

Some IdP topologies legitimately serve endpoints from more than one host. The common on-prem case is Keycloak with `hostname-backchannel-dynamic=true`, where `token_endpoint` and `jwks_uri` are rewritten to the internal hostname the CLI connected through. If, and only if, you have confirmed with the IdP's own configuration that the extra host is intentional, add it:

```json theme={null}
"additional_endpoint_hosts": ["keycloak.corp.internal"]
```

Entries are `host` (any port) or `host:port`, matched case-insensitively. There is no wildcard and no way to disable the check. The allowlist never relaxes TLS: an `http://` endpoint is still refused under an `https://` issuer. Do not add a host just because a login error names it; the error is the check working.

## How revocation propagates

The group gate is evaluated on the cached ID token, so removing a user from `required_group` (or disabling the user) at the IdP takes effect on their next token refresh: at the latest when the current ID token expires, plus the 120-second skew tolerance. Keep the ID-token lifetime for this client short if fast revocation matters.

A refresh response that omits a new ID token keeps the cached ID token in use, but only for up to 24 hours past that token's `exp`; after that the user must sign in again even if the IdP still accepts the refresh token.

### Revalidation in running sessions

While Devin Desktop (or any client of the bundled agent) is running, the agent re-checks the login every **60 seconds** and forces a token refresh against the IdP at least every **15 minutes**. If a check fails, the agent records an `sso_revalidation_failed` audit event with the `IDP` code, stops accepting new work, and shuts down; the user must sign in again and restart.

## What gets audited

SSO activity is written to the local [audit log](/federal/air-gapped/on-disk-formats#audit-log) as `auth` events. Every event that has a validated token at hand carries the SSO identity on `actor` (`sso_issuer`, `sso_subject`, `sso_user`, `sso_groups`).

| `action` | When |
| - | - |
| `sso_login` | Login completed (`devin auth login` or Desktop). |
| `sso_login_denied` | Login refused after the IdP answered: the required group is missing (`error_code: "IDP004"`), or the returned ID token failed validation or was absent (`IDP005`). `actor` carries the identity the IdP vouched for; nothing is cached. |
| `sso_refresh` | Tokens refreshed against the IdP. |
| `sso_logout` | `devin auth logout` or Desktop sign-out. |
| `sso_revalidation_failed` | A running agent lost its login and is shutting down; `error_code` carries the `IDP` code. |

## IDP error codes

| Code | Meaning | What to do |
| - | - | - |
| `IDP001` | The auth file is unreadable or invalid. | Fix `/etc/devin/auth.json` (or the `DEVIN_AUTH_FILE` path); the message names the offending field. |
| `IDP002` | No cached login for the configured issuer. | Run `devin auth login`. |
| `IDP003` | The login is invalid or expired and could not be silently refreshed (including an IdP `invalid_grant` response). | Run `devin auth login` again. |
| `IDP004` | Logged in, but the user is not a member of `required_group`. | Add the user to the group at the IdP, or correct `required_group` / `groups_claim`. |
| `IDP005` | The IdP could not be reached or returned an error. | Check network reachability, TLS trust (`DEVIN_CA_BUNDLE`), and the IdP's own logs. |
| `IDP006` | The local SSO token cache is unreadable, malformed, or not private to the current user. | Run `devin auth login`; a fresh login rewrites the cache. |

The complete catalog is on the [Error codes](/federal/air-gapped/error-codes) page.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.