# Configure identity providers
Wire your IdP into the advanced SSO stack via OIDC, SAML, or SCIM.
Once the Ory stack is deployed, you connect it to your identity
provider. There are three paths, depending on what your IdP supports
and what you need.

| Path | Use when |
|---|---|
| [Direct OIDC](#direct-oidc) | Your IdP speaks OIDC and you only need authentication |
| [SAML via Polis](#saml-via-polis) | Your IdP only speaks SAML, or you want a single proxy in front of multiple IdPs |
| [SCIM via Polis](#scim-via-polis) | You want users provisioned and deactivated automatically from your IdP |

The SAML and SCIM paths require `enable_polis = true` in your tfvars.
SCIM is layered on top of SAML; you need the SAML connection registered
in Polis first.

## Direct OIDC

The simplest path. Kratos federates upstream OIDC providers directly,
no Polis required. Use this when your IdP supports OIDC and you don't
need SCIM provisioning.

### Step 1. Create an OIDC application in your IdP

Create a new application with these settings:

- **Sign-in redirect URI**: `https://<your-kratos-hostname>/self-service/methods/oidc/callback/<id>`
  where `<id>` matches the entry you'll add to tfvars (e.g. `okta`,
  `entra`, `google`).
- **Grant types**: Authorization Code

You don't set scopes on the application. Kratos requests them at sign-in
from the entry's `scope` list in tfvars, which defaults to `openid`,
`email`, `profile`.

For Okta specifically: Applications → Create App Integration → OIDC →
Web Application → set the redirect URI as above, and assign the users or
groups who should sign in.

Copy the **Client ID** and **Client Secret** from the app's **General**
tab. The issuer URL is not shown on the app. For Okta, use one of:

- `https://<your-okta-domain>`, Okta's built-in org authorization
  server. Available on every Okta org. To find your Okta domain, click
  your username in the upper-right corner of the Admin Console.
- `https://<your-okta-domain>/oauth2/default`, or another custom
  authorization server. Requires Okta's API Access Management. The
  **Issuer URI** is listed under **Security** → **API** →
  **Authorization Servers**. A custom authorization server has its own
  access policy, separate from the app's sign-on policy: on its
  **Access Policies** tab, make sure a policy is assigned to your app
  and has a rule that allows the Authorization Code grant.

Either works here, because Materialize trusts the tokens Hydra issues,
not Okta's.

To map Okta groups to Materialize roles, Okta also needs to send a `groups`
claim. See [Enable role mapping](/self-managed-deployments/sso/advanced/role-mapping/#before-you-begin).

### Step 2. Add the provider to tfvars

Add an entry to `upstream_identity_providers` in your `terraform.tfvars`:

```hcl
upstream_identity_providers = [
  {
    id            = "okta"
    provider      = "generic"
    client_id     = "<from Okta>"
    client_secret = "<from Okta>"
    issuer_url    = "https://your-org.okta.com" # or .../oauth2/default, see Step 1
    scope         = ["openid", "email", "profile"]
    label         = "Sign in with Okta"
  },
]
```

Run `terraform apply`. Kratos will reload its config and pick up the new
provider. The label text becomes the button on the login screen.

### Step 3. Test the login

Open the Materialize Console in an incognito window:
`https://<your-console-hostname>`. You should see the Kratos login
screen with a "Sign in with Okta" button. Click it; you'll bounce
through your IdP and land in the Materialize Console signed in.

## SAML via Polis

Use this when your IdP only supports SAML (Entra SAML, ADFS, Okta SAML,
Auth0 SAML), or when you want a single SSO proxy in front of multiple
IdPs.

The flow at runtime: console → Hydra → Kratos UI → "Sign in via SAML"
button → Polis → your IdP SAML → back through Polis → Kratos issues a
federated identity → Hydra issues an OAuth2 token → console.

### Step 1. Create a SAML application in your IdP

In your IdP, create a SAML 2.0 application with:

- **ACS URL / Assertion consumer URL**:
  `https://<your-polis-hostname>/api/oauth/saml`
- **Audience URI / Entity ID**: `https://saml.boxyhq.com`
- **NameID format**: EmailAddress
- **Attribute statements**: at minimum, `email`, `firstName` and `lastName`,
  mapped from the IdP user profile. In Okta:

  | Name | Value |
  |------|-------|
  | `email` | `user.profile.email` |
  | `firstName` | `user.profile.firstName` |
  | `lastName` | `user.profile.lastName` |
- **Group attribute statement** (optional but required for `groups` in
  the JWT): name `groups`, filter `Matches regex .*` (or a narrower
  filter to scope which groups flow through).

Save and grab the SAML metadata URL (or download the metadata XML).
Assign users (or groups) to the app so they can authenticate through it.

> **Note:** **Okta:** set the `groups` claim in the **Group Attribute Statements**
> table, not the **Add expression** dialog at the top of the Attribute
> Statements section. The Group Attribute Statements table lives under the
> SAML app's **Sign On** tab, inside the collapsed **Show legacy
> configuration** panel. Set Name `groups`, Name format `Unspecified`, and a
> Filter such as `Starts with` and your role-name prefix. The newer
> expression UI does not expose group functions like `Groups.startsWith` or
> `Arrays.flatten` on trial / integrator tenants, which is why the legacy
> table is the reliable path. See Okta's [attribute statements](https://help.okta.com/oie/en-us/content/topics/apps/define-attribute-statements.htm),
> the legacy config, versus their newer [federated claims](https://help.okta.com/oie/en-us/content/topics/apps/federated-claims-overview.htm)
> model.


### Step 2. Get the Polis admin API key

```bash
POLIS_API_KEY=$(kubectl get secret -n ory polis-config \
  -o jsonpath='{.data.API_KEYS}' | base64 -d)
```

### Step 3. Register the SAML connection in Polis

You can register through the Polis admin API (below) or through the Polis
admin UI, which is easier for ongoing management but requires bootstrapping
first, see
[Unlock the Polis admin UI without SMTP](/self-managed-deployments/sso/advanced/operations/#unlock-the-polis-admin-ui-without-smtp).

If the IdP exposes a publicly-fetchable metadata URL:

```bash
curl -X POST https://<your-polis-hostname>/api/v1/sso \
  -H "Authorization: Api-Key $POLIS_API_KEY" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "tenant=<customer-name>" \
  --data-urlencode "product=materialize" \
  --data-urlencode "name=<idp-name>-saml" \
  --data-urlencode "redirectUrl=https://<your-kratos-hostname>/self-service/methods/saml/callback/polis" \
  --data-urlencode "defaultRedirectUrl=https://<your-console-hostname>" \
  --data-urlencode "metadataUrl=https://<idp-metadata-url>"
```

If the metadata URL is gated by API auth (Okta integrator orgs, for
example), POST the raw XML instead:

```bash
curl -X POST https://<your-polis-hostname>/api/v1/sso \
  -H "Authorization: Api-Key $POLIS_API_KEY" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "tenant=<customer-name>" \
  --data-urlencode "product=materialize" \
  --data-urlencode "name=<idp-name>-saml" \
  --data-urlencode "redirectUrl=https://<your-kratos-hostname>/self-service/methods/saml/callback/polis" \
  --data-urlencode "defaultRedirectUrl=https://<your-console-hostname>" \
  --data-urlencode "rawMetadata=$(cat idp-metadata.xml)"
```

If `rawMetadata` fails with "Couldn't fetch XML data" (some shells strip
newlines), base64-encode the file first and use `encodedRawMetadata` instead:

```bash
--data-urlencode "encodedRawMetadata=$(base64 < idp-metadata.xml | tr -d '\n')"
```

The response contains a `clientID` and `clientSecret`. Save them for the
next step.

Verify the connection landed:

```bash
curl -s -H "Authorization: Api-Key $POLIS_API_KEY" \
  "https://<your-polis-hostname>/api/v1/sso?tenant=<customer-name>&product=materialize" | jq .
```

### Step 4. Wire Polis into Kratos as a SAML sign-in provider

Polis is a SAML method in Kratos, not an OIDC provider, so it goes in
`saml_providers` rather than `upstream_identity_providers`. Add an entry:

```hcl
saml_providers = [
  {
    id            = "polis"
    label         = "Sign in via SAML"
    client_id     = "<clientID from Step 3>"
    client_secret = "<clientSecret from Step 3>"
    issuer_url    = "https://<your-polis-hostname>"
    auth_url      = "https://<your-polis-hostname>/api/oauth/authorize"
    token_url     = "https://<your-polis-hostname>/api/oauth/token"
  },
]
```

The `issuer_url` is the base Polis URL, with no `/saml` suffix. Save the
SAML metadata from Step 1 as `idp-metadata.xml` next to your
`terraform.tfvars`: the example's `main.tf` reads it with `file()` and
injects it as `raw_idp_metadata_xml`, so you never paste XML into tfvars.

Run `terraform apply`. The "Sign in via SAML" button will appear on the
Kratos login screen.

### Step 5. Test the SAML login

Open the Materialize Console in an incognito window. Click **Sign in via
SAML**. You'll bounce through Polis to your IdP, authenticate, and land
in the Materialize Console. The federated identity is created in Kratos
on first login, and Materialize JIT-creates the SQL role from your email
claim.

## SCIM via Polis

Adds automatic user and group provisioning and deactivation from your IdP to
Materialize. Builds on the SAML setup above.

### Step 1. Create a SCIM directory in Polis

Same choice as with SAML: register through the API (below) or through the
Polis admin UI ([bootstrapped separately](/self-managed-deployments/sso/advanced/operations/#unlock-the-polis-admin-ui-without-smtp)).

```bash
curl -X POST https://<your-polis-hostname>/api/v1/dsync \
  -H "Authorization: Api-Key $POLIS_API_KEY" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "tenant=<customer-name>" \
  --data-urlencode "product=materialize" \
  --data-urlencode "name=<idp-name>-scim" \
  --data-urlencode "type=okta-scim-v2"
```

For other IdPs, change `type` to `azure-scim-v2`, `onelogin-scim-v2`,
or `generic-scim-v2`.

Save the `scim.endpoint` URL and `scim.secret` bearer token from the
response. Both go into your IdP's SCIM configuration.

### Step 2. Enable SCIM provisioning in your IdP

Steps for Okta (other IdPs vary in wording but follow the same shape):

1. Open the SAML application you created above.
2. **General** tab → Provisioning → switch to "SCIM" → Save.
3. A **Provisioning** tab appears. Click into it → **Integration**:
   - SCIM connector base URL: paste `scim.endpoint` **with a trailing
     slash**. Okta rejects the URL without one ("Invalid Base URL for
     the SCIM Connector").
   - Unique identifier for users: `email`
   - Supported provisioning actions: Import New Users and Profile
     Updates, Push New Users, Push Profile Updates, Push Groups
   - Authentication Mode: **HTTP Header** (not Basic Auth)
   - Authorization / Token: paste `scim.secret`
   - Click **Test Connector Configuration**; it should report the
     connector as configured. The base URL's host must be reachable
     from your IdP's cloud (see [Troubleshooting](/self-managed-deployments/sso/advanced/troubleshooting/)).
4. **Provisioning** → **To App** → Edit → enable Create Users, Update
   User Attributes, Deactivate Users. Save.
5. **Push Groups** tab (appears once SCIM is enabled) → **Push Groups
   → By name** → pick each group you want in Polis's directory → tick
   "Push group memberships immediately" → Save. This creates the group
   entity in Polis; individual users are still pushed via the
   Assignments step below.
6. **Assignments** tab → **Assign → Assign to People** (or **Assign to
   Groups**) → assign the users / groups that should be provisioned.
   Okta pushes a SCIM POST per user within seconds.

### Step 3. Verify the push

Currently-assigned users push immediately. New users push on assignment.

```bash
DIRECTORY_ID=<id from the Step 1 response>
curl -s -H "Authorization: Api-Key $POLIS_API_KEY" \
  "https://<your-polis-hostname>/api/v1/dsync/users?tenant=<customer-name>&product=materialize&directoryId=$DIRECTORY_ID" | jq .
```

You should see the assigned users with their email, name, and external
ID populated.

> **Note:** **Existing assignments don't backfill on enable.** If a user was
> assigned to the SAML app before SCIM was turned on, Okta doesn't
> re-push them. Either unassign and reassign the user (the cleanest
> trigger), or push manually via Okta admin → Directory → People → the
> user → Applications → "..." → Push profile updates.


### Step 4. (Optional) Sync groups

Group memberships flow through the stack in two independent ways:

1. **SAML attribute statement**: on each login, the IdP attaches the
   user's group memberships to the SAML assertion. Polis passes them
   through as an OIDC claim, Kratos writes them onto the identity
   trait, and the Ory stack embeds them in the JWT as a `groups` claim
   that Materialize can read. **Refreshed on every login.**
2. **SCIM directory push**: the IdP synchronizes group entities and
   memberships into Polis's directory. **Refreshed continuously**, but
   doesn't itself change the contents of a JWT already in flight; the
   user has to log in again to see updates.

For the SAML attribute path (recommended):

1. In your SAML app configuration, add a `groups` attribute statement
   (Okta: Sign On tab → Attribute Statements → legacy Group Attribute
   Statements → Name: `groups`, Filter: `Matches regex .*`).
2. Log in via the console. The `groups` JWT claim will contain the
   user's group names.

For the SCIM directory push (audit + future integration):

1. Create a group in your IdP and add users to it.
2. Open the SAML app → **Push Groups** tab → "Push Groups" → "by name"
   → search and add the group → save.

Confirm the push landed in Polis:

```bash
curl -s -H "Authorization: Api-Key $POLIS_API_KEY" \
  "https://<your-polis-hostname>/api/v1/dsync/groups?tenant=<customer-name>&product=materialize&directoryId=$DIRECTORY_ID" | jq .
```

Group memberships flow all the way to the JWT (via the `groups` claim) and
Materialize can automatically translate them into SQL role memberships on
each login. Enable it via the `oidc_group_role_sync_enabled` system
parameter; see [Enable role mapping](/self-managed-deployments/sso/advanced/role-mapping/)
for details and the naming convention.

## What happens when users sign in

The Ory stack issues OIDC tokens with the user's email as the `email`
claim. Materialize is configured with `oidc_authentication_claim =
"email"`, so:

- **First login**: Materialize creates a SQL role named after the user's
  email (JIT role creation). The role has no privileges by default.
- **Subsequent logins**: Same role is reused.
- **Deprovisioned in IdP**: SCIM deactivates the user in Polis, but the
  Materialize SQL role isn't dropped automatically. You'll need to
  `DROP ROLE "user@email"` separately or let it become a stale (but
  inactive) record.

To grant privileges, see the role / permission management docs at
[RBAC](/security/self-managed/access-control/).

## See also

- [SSO (direct OIDC)](/self-managed-deployments/sso/oidc/) -- the simpler path
  for OIDC-only deployments
- [Operations](/self-managed-deployments/sso/advanced/operations/) -- day-2: rotating credentials, adding
  OAuth2 clients
- [Troubleshooting](/self-managed-deployments/sso/advanced/troubleshooting/) -- common errors during the
  IdP-side setup
