Configure identity providers

View as Markdown

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 Your IdP speaks OIDC and you only need authentication
SAML via Polis Your IdP only speaks SAML, or you want a single proxy in front of multiple IdPs
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.

Step 2. Add the provider to tfvars

Add an entry to upstream_identity_providers in your terraform.tfvars:

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, the legacy config, versus their newer federated claims model.

Step 2. Get the Polis admin API key

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.

If the IdP exposes a publicly-fetchable metadata URL:

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:

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:

--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:

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:

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).

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).
  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.

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:

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 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.

See also

Back to top ↑