# Sign-in, SSO & API keys

Two-factor and MFA enforcement, session controls, SAML SSO, SCIM provisioning, API keys and service accounts, and OAuth apps.

Canonical page: https://kortix.com/docs/accounts/identity

These controls decide how people and machines sign in to a Kortix account.
Every member uses them to secure their own sign-in and create keys. Owners and
admins use them to set account rules, connect an identity provider, and issue
keys for CI and apps. Each control also has an API route. See the
[REST API](/docs/api).

Open your own settings from the project menu → **Settings**. Open the account's
settings from the project menu → **Switch Project** → **Account settings**. Each
control is in one of these places:

| Place | Who | What is there |
|---|---|---|
| **Settings → Security** | Each person, for themselves | Two-factor authentication, signed-in devices |
| **Settings → Personal access keys** | Each person, for themselves | Keys that act as you, connected apps |
| **Account settings → Settings → Security** | Owners and admins | **Require two-factor authentication**, **Maximum session length**, **Idle timeout**, **Active sessions**, **Admins can open every session** ([Sessions](/docs/work/sessions)) |
| **Account settings → API keys** | Owners and admins | Service accounts, **OAuth apps**, **Key rules** |
| **Account settings → Identity** | Owners and admins | **SAML SSO**, **Directory Sync** (SCIM) |

The account sections need the `account.write` permission, which the owner and admin
roles hold. Roles, members, and groups are on [Accounts & access](/docs/accounts).

## Two-factor and MFA enforcement

### Turn on two-factor for yourself

1. Open **Settings → Security**.
2. Select **Add authenticator app**.
3. Scan the QR code with any authenticator app (TOTP), or enter the key under
   **Can't scan? Enter this key**.
4. Type the 6-digit code. It verifies when you type the sixth digit.

Kortix then asks for a code at sign-in and before sensitive changes: removing a
factor and signing out devices. Remove a factor from the same page.

### Require two-factor for the account

1. Open **Account settings → Settings → Security**.
2. Turn on **Require two-factor authentication**.
3. Read the preview. It counts the members with a verified factor and lists
   every member who has none.
4. Select **Require it**.

When the requirement is on, a browser sign-in without a verified second factor
gets `403` with `code: "account_mfa_required"` on every protected action. The web
app opens a code prompt. A member with no factor enrolls one under
**Settings → Security** first.

- **Exempt:** super-admins, personal access keys, and service account keys. A
  key has no second factor to prompt for.
- **Lockout guard:** Kortix refuses to turn the requirement on when no
  super-admin exists and no member has a verified factor (`409`).
- **Turn it off:** select **Stop requiring it**. Members can then sign in with a
  password alone.

## Session controls

### Session policy

Open **Account settings → Settings → Security** and set two limits, in minutes. Empty
means no limit.

| Setting | Effect | Limit |
|---|---|---|
| **Maximum session length** | How long a browser sign-in lasts before the member signs in again | 1 to 10,080 (7 days) |
| **Idle timeout** | Signs a member out after this many minutes with no request | 1 to 10,080 (7 days) |

A sign-in past either limit gets `401` on its next request to the account. The
member signs in again. Keys are exempt.

### Sign a member out

**Active sessions**, under the policy, lists the 200 most recently active browser
sign-ins in the account. Select **Sign out** on a row. That sign-in gets `401` on
its next request, and the member can sign in again. This needs the
`member.remove` permission.

### Your own devices

**Settings → Security → Devices** lists every browser, desktop, and mobile
sign-in of yours.

- **Sign out** on one device ends that sign-in. You cannot sign out the current
  device from this list. Sign out normally instead.
- **Sign out other devices** ends every sign-in except this browser.

## Single sign-on (SAML)

SAML SSO lets members sign in with your identity provider (IdP). It needs the
`sso` entitlement (see [Enterprise features](#enterprise-features)) and an
organization account. A personal account gets `403`
`code: "sso_personal_account"`. The account Kortix creates at sign-up is
personal. Create an organization account with the project menu → **Switch
Project** → **Create an account…**, and create the team's projects there. A
project cannot move between accounts. An account has one SSO provider.

### Set it up

### Open the guided setup
Open **Account settings → Identity**. On the **SAML SSO** card, select **Configure** and
pick your IdP: Microsoft Entra ID, Okta, Google Workspace, Cloudflare Access,
OneLogin, JumpCloud, PingOne, Auth0, or a custom SAML provider. The wizard shows each click in that IdP's console.
### Register Kortix in your IdP
Create a SAML app in your IdP. Copy the two values from **Service provider
details**: **Identifier (Entity ID)** and **Reply URL (ACS)**. Assign the people
who may sign in.
### Import the IdP metadata
Paste the IdP's metadata URL or its metadata XML. Enter the **Primary email
domain** and the **Group claim name**: the SAML attribute that carries group
membership.
### Verify the domain
Publish the TXT record the card shows, then select **Verify domain**.
### Map groups
Add group mappings, or turn on **Auto-provision groups**. See
[Groups from the IdP](#groups-from-the-idp).
### Enforce SSO
Turn on **Enforce SSO for this domain** when every member can sign in through
the IdP.

From then on, a person who enters a work email on the sign-in page goes to the
IdP. With **Auto-create members** on (the default), Kortix adds them to the
account on their first SSO sign-in.

| Setting | Default | Effect |
|---|---|---|
| **Group claim name** | `groups` | The SAML attribute Kortix reads groups from. Okta sends group names on `groups`. Entra sends group object IDs by default. The Entra guide names the claim `memberOf`, so set **Group claim name** to `memberOf` for Entra. |
| **Auto-create members** | On | Adds every person who signs in through the IdP as a `member`. Off: SSO adds only existing members and people your SCIM directory provisioned. |
| **Auto-provision groups** | Off | Creates a Kortix group for every group the IdP sends, and adds the person to it. |
| **Enforce SSO for this domain** | Off | Refuses every other way to sign in for the primary domain. |

### Verify the domain

The IdP asserts each person's email address. Kortix trusts that address only for
a domain your account verified with DNS.

| Record | Name | Value |
|---|---|---|
| `TXT` | `_kortix-verification.<your-domain>` | `kortix-verification=<token>` |

DNS changes can take a few minutes to appear.

- One domain belongs to one account. A domain that another account verified
  returns `409` `code: "sso_domain_claimed"`.
- A new primary domain starts a new, unverified claim with a new token.
- Until the domain is verified, members still join through SSO and get their
  groups. Their SSO identity matches no invitation from another account, no
  existing identity merges into it, and **Enforce SSO** has no effect.
- With a verified domain, an existing member with the same email merges into
  the SSO identity and keeps their roles. Kortix never merges an owner or a
  super-admin. Their SSO sign-in joins as a separate member.

### Enforce SSO

With a verified domain and **Enforce SSO for this domain** on, every identity in
that domain that did not come from your IdP is refused with `403`:

- password sign-in, magic links, and email codes, in the web app and on the
  headless sign-in routes (`/v1/auth/signup`, `/v1/auth/sign-in/password`,
  `/v1/auth/sign-in/magic-link`, `/v1/auth/verify-otp`). There the body is
  `error: "sso_required"`.
- the existing sessions and keys of those identities, owners and super-admins
  included. Their account and project requests get `code: "sso_required"`.

> **Warning**
> Enforcement covers the account owner's own email too. Before you turn it on,
> sign in once through the IdP and give that SSO identity the role it needs. If
> the IdP fails, a Kortix platform admin marks the domain unverified, which lifts
> enforcement. On a self-hosted instance, your instance's platform admin does
> this.

### Groups from the IdP

Kortix syncs groups on every SSO sign-in:

- It adds the person to each group a claim value maps to.
- It removes the person from a mapped group when the IdP stops sending its
  claim value.
- It keeps groups an admin added by hand. Only mapped groups change.

Add a mapping on the **SAML SSO** card under **Group mappings**: a **Claim
value** and an **IAM group**. The match ignores case and surrounding spaces. Map
Entra object IDs, or Okta group names, exactly as the IdP sends them. Give each
group its project roles under [Groups](/docs/accounts#groups).

### Remove SSO

Select **Remove SSO provider** on the **SAML SSO** card. Existing members keep
their access. New SSO sign-ins stop joining the account, and group sync stops.

## SCIM provisioning

SCIM lets your IdP create, update, and remove members and groups without a
sign-in. It needs the `scim` entitlement. Connect SAML SSO first: SCIM creates
members, and SSO is how they sign in.

### Connect your IdP

1. Open **Account settings → Identity**. On the **Directory Sync** card, select
   **Guided setup** and pick your IdP: Microsoft Entra ID, Okta, OneLogin, JumpCloud,
   PingOne, or any SCIM 2.0 provider.
2. Mint a token. Kortix shows the secret (`kortix_scim_…`) once.
3. In the IdP, enter the **SCIM base URL** and the token as a bearer token.
4. Start provisioning in the IdP.

| Value | What to enter |
|---|---|
| SCIM base URL | `https://api.kortix.com/scim/v2/accounts/<account_id>`. It has no `/v1`. A self-hosted instance uses its own API origin. |
| Auth | `Authorization: Bearer kortix_scim_…` |
| User identifier | `userName` = the person's email |

The guided setup shows each step in the IdP console. Check these settings:

- **Okta:** set **Unique identifier field for users** to `userName` and
  **Authentication Mode** to **HTTP Header**. Okta pushes changes as they
  happen.
- **Microsoft Entra ID:** automatic provisioning needs Entra ID P1 or P2. Keep
  `userName` → `user.userprincipalname`. Entra syncs about every 40 minutes.
  **Provision on demand** pushes one person at once.

Kortix checks the `scim` entitlement on every SCIM request, so a token stops
working when the account loses it.

### What the endpoints do

| Endpoint | Supports |
|---|---|
| `/ServiceProviderConfig`, `/ResourceTypes`, `/Schemas` | Discovery |
| `/Users` | `GET` (list, one), `POST`, `PUT`, `PATCH`, `DELETE` |
| `/Groups` | `GET` (list, one), `POST`, `PUT`, `PATCH` (add and remove members), `DELETE` |

- **Filters:** one `attribute eq "value"` clause. `/Users` matches `userName`,
  `id`, and `externalId`. `/Groups` matches `displayName`, `id`, and
  `externalId`. A filter in any other form returns `400`. A filter on another
  attribute returns an empty list.
- **New person:** a `POST /Users` for an email with no Kortix sign-in creates a
  pending invitation with the `member` role, valid for 14 days. SCIM lists it as
  an active user. A person who already has a Kortix sign-in joins as a `member`.
- **Groups:** a SCIM group is an ordinary Kortix group. Give it project roles
  like any other.
- **Remove a person:** `active: false` or `DELETE` removes the membership, its
  roles and groups, and revokes the person's keys and live session tokens.
- **Last owner:** deactivating the account's last owner returns `409`.

## API keys and tokens

Every Kortix credential starts with a prefix that names its kind.

| Prefix | Credential | Acts as | Get one |
|---|---|---|---|
| `kortix_pat_` | Personal access key | You, with your roles. Optionally one project only. | **Settings → Personal access keys**, `kortix tokens new`, `kortix login` |
| `kortix_pat_` | Project CLI token | You, on one project only | `kortix projects cli-tokens new` |
| `kortix_sa_` | Service account key | The service account. It has no access until a role is assigned. | **Account settings → API keys**, `kortix tokens service-accounts new` |
| `kortix_oat_`, `kortix_ort_` | OAuth access token (1 hour), refresh token (30 days) | The person who approved the app | [Sign in with Kortix](/docs/sdk/auth#sign-in-with-kortix) |
| `kortix_gw_` | Gateway key | Model calls through one project's LLM gateway | `kortix gateway keys new` ([Models](/docs/project/models)) |
| `kortix_scim_` | SCIM token | Directory sync for one account | **Account settings → Identity** |
| `kortix_pat_` | Session token, in `KORTIX_TOKEN` | The agent of one session | Kortix sets it in the sandbox. It ends with the session. |

Kortix shows a secret once, when you create it, and stores only its hash.
Connected apps (`kortix_oat_`) cannot create keys, service accounts, SCIM
tokens, or OAuth apps (`403`).

### Personal access keys

A personal access key acts as the member who created it. It holds exactly that
member's roles and stops working when the membership ends. Every member can
create and revoke their own keys. Revoking someone else's key needs
`token.revoke`.

```bash
kortix tokens new ci-deploy --expires 90d          # print the secret once
kortix tokens new laptop --project <project_id>    # this project only
kortix tokens ls --mine
kortix tokens rm <token_id>
```

`--expires` takes an ISO-8601 instant or a span: `30d`, `12h`, `6w`, `1y`. A key
bound to one project gets `403` on every other project and on account actions.
For a key bound to the current project, use
`kortix projects cli-tokens new [--expires-at <iso>]`.

### Service accounts

A service account is its own principal, for CI and automations that must not
act as a person. It starts with no access.

```bash
kortix tokens service-accounts new nightly-reporter --description "Cron"
kortix access grant --service-account <id> --role member --project <project_id>
kortix tokens service-accounts disable <id>   # stops it authorizing
kortix tokens service-accounts rm <id>        # deletes it
```

A disabled service account cannot be enabled again. Create a new one. Each
agent also has a service account. Bind its ceiling role in **Account settings →
Projects** → the project → **Grant access** → **Agents**, not here. See
[What an agent may do](/docs/project/agents#what-an-agent-may-do).

### Key rules

**Account settings → API keys → Key rules** governs the personal access keys and project
CLI tokens people create in the account. Keys Kortix creates for a running
session follow the session instead.

| Rule | Field | Limit | Effect |
|---|---|---|---|
| **Require an end date** | `require_expiry` | On or off | A key without an expiry is refused: `400` `code: "expiry_required"`. |
| **Longest a key can last** | `max_lifetime_days` | 1 to 730 days | A later expiry is refused: `400` `code: "expiry_too_far"`. |
| **Turn off unused keys** | `idle_revoke_days` | 1 to 365 days | A key unused for this long is revoked on its next use. Keys bound to one project are exempt. |

The first two rules apply
when a key is created. **Turn off unused keys** also applies to existing keys.

## OAuth apps and connected apps

### Register an OAuth app

An OAuth app signs Kortix members in on your own site ("Sign in with Kortix").

1. Open **Account settings → API keys → OAuth apps** and select **Register app**.
2. Enter a name, the type, the redirect URIs, and the scopes.
3. Copy the client id and, for a confidential app, the client secret. The
   secret is shown once.

| Field | Values |
|---|---|
| Type | **Confidential**: a server-side app with a client secret. **Public**: a browser or native app with PKCE and no secret. Fixed at registration. |
| Redirect URIs | Matched byte for byte. `https`, except `localhost`. |
| Scopes | `profile` (who the person is), `email`, `kortix` (act as the person on the Kortix API). Default: all three. |
| **Active** | Off stops new sign-ins without deleting the app. |

**Rotate secret** issues a new secret, and the old one stops working at once.
**Delete app** ends every token the app holds. In code, use
`kortix.iam.oauthClients` (`list`, `get`, `create`, `update`, `rotateSecret`,
`remove`). The sign-in flow is in
[Sign in with Kortix](/docs/sdk/auth#sign-in-with-kortix).

### Connected apps

A connected app is an app you approved with Sign in with Kortix, for example an
MCP client such as Claude Code or Cursor. It acts as you, in every account you
belong to, until you revoke it. See [Kortix MCP server](/docs/connect/mcp).

- **Web:** **Settings → Personal access keys → Connected apps → Revoke access**.
- **CLI:** `kortix tokens apps ls` and `kortix tokens apps rm <client-id>`.

Revoking deletes your approval, and the app's tokens stop working on their next
request. The app must ask you again.

## Enterprise features

An entitlement decides whether a feature exists for the account. The role still
decides who may use it. The Enterprise plan holds all five.

| Entitlement | Unlocks |
|---|---|
| `sso` | [SAML SSO](#single-sign-on-saml), domain verification, Enforce SSO |
| `scim` | [SCIM provisioning](#scim-provisioning) and SCIM tokens |
| `rbac` | [Custom roles](/docs/accounts#custom-roles); creating, editing, and adding members to groups |
| `auditAccess` | Reading and exporting the [audit log](/docs/accounts#audit-log), audit webhooks |
| `branding` | [Branding](/docs/accounts#branding) |

Without the entitlement, the gated route returns `402` with
`code: "entitlement_required"` and `entitlement` set to its key. A SCIM request
(`/scim/v2/...`) from an account without `scim` gets `403`. Removing a
configuration (an SSO provider, a SCIM token) never needs the entitlement. Plans
are on [Plans & credits](/docs/credits).

On a self-hosted instance, a Kortix Enterprise license unlocks all five for
every account: set `ENTERPRISE_LICENSE_AVAILABLE=true`, or install with
`kortix self-host init --enterprise-license`. See
[Self-hosting](/docs/host).
