Configure identity providers
View as MarkdownOnce 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,firstNameandlastName, mapped from the IdP user profile. In Okta:Name Value emailuser.profile.emailfirstNameuser.profile.firstNamelastNameuser.profile.lastName - Group attribute statement (optional but required for
groupsin the JWT): namegroups, filterMatches 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.
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):
- Open the SAML application you created above.
- General tab → Provisioning → switch to “SCIM” → Save.
- A Provisioning tab appears. Click into it → Integration:
- SCIM connector base URL: paste
scim.endpointwith 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).
- SCIM connector base URL: paste
- Provisioning → To App → Edit → enable Create Users, Update User Attributes, Deactivate Users. Save.
- 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.
- 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.
Step 4. (Optional) Sync groups
Group memberships flow through the stack in two independent ways:
- 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
groupsclaim that Materialize can read. Refreshed on every login. - 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):
- In your SAML app configuration, add a
groupsattribute statement (Okta: Sign On tab → Attribute Statements → legacy Group Attribute Statements → Name:groups, Filter:Matches regex .*). - Log in via the console. The
groupsJWT claim will contain the user’s group names.
For the SCIM directory push (audit + future integration):
- Create a group in your IdP and add users to it.
- 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
- SSO (direct OIDC) – the simpler path for OIDC-only deployments
- Operations – day-2: rotating credentials, adding OAuth2 clients
- Troubleshooting – common errors during the IdP-side setup