Enterprise SSO Guide
The Experimentation Platform supports Enterprise Single Sign-On (SSO) via SAML 2.0 and OpenID Connect (OIDC). SSO allows your organization to authenticate users through an existing Identity Provider (IdP) instead of platform-managed passwords.
Supported Providers
| Provider | Protocol | JIT Provisioning | Group Mapping |
|---|---|---|---|
| Okta | SAML 2.0 | Yes | Yes |
| Azure Active Directory | SAML 2.0 + OIDC | Yes | Yes |
| Google Workspace | OIDC | Yes | Yes |
| GitHub | OIDC | Yes | Yes |
| OneLogin | SAML 2.0 | Yes | Yes |
| Auth0 | SAML 2.0 + OIDC | Yes | Yes |
Overview
SSO operates alongside the existing AWS Cognito authentication. When SSO is configured for an organization domain:
- Users whose email matches the organization's domain are redirected to the IdP for authentication.
- The IdP authenticates the user and sends a signed assertion or token to the platform.
- The platform validates the assertion, maps IdP groups to platform roles, and issues a session JWT.
- If the user does not yet have a platform account, one is created automatically (JIT provisioning).
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /auth/sso/saml/{config_id}/metadata | Returns SP metadata XML to give to the IdP |
POST | /auth/sso/saml/{config_id}/acs | ACS (Assertion Consumer Service) endpoint — IdP posts SAML assertions here |
GET | /auth/sso/oidc/{provider}/login | Initiates the OIDC authorization code flow |
GET | /auth/sso/oidc/{provider}/callback | OIDC callback endpoint that exchanges the code for tokens |
POST | /auth/sso/configs | Create a new SSO configuration (admin only) |
GET | /auth/sso/configs | List all SSO configurations (admin only) |
GET | /auth/sso/configs/{config_id} | Retrieve a specific SSO configuration (admin only) |
PUT | /auth/sso/configs/{config_id} | Update an SSO configuration (admin only) |
DELETE | /auth/sso/configs/{config_id} | Delete an SSO configuration (admin only) |
Creating an SSO Configuration
SAML Configuration
curl -X POST https://api.example.com/auth/sso/configs \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"org_name": "Acme Corp",
"org_domain": "acme.com",
"provider_type": "saml",
"entity_id": "https://acme.okta.com",
"sso_url": "https://acme.okta.com/app/sso/saml",
"x509_certificate": "MIIC...",
"role_mapping": {
"admin-group": "ADMIN",
"developers": "DEVELOPER",
"analysts": "ANALYST",
"viewers": "VIEWER"
},
"jit_provisioning": true,
"is_enforced": false
}'
OIDC Configuration
curl -X POST https://api.example.com/auth/sso/configs \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"org_name": "Acme Corp",
"org_domain": "acme.com",
"provider_type": "oidc",
"oidc_provider": "google",
"client_id": "123456789-abc.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"role_mapping": {
"admin-team": "ADMIN",
"engineering": "DEVELOPER"
},
"jit_provisioning": true,
"is_enforced": false
}'
Configuration Fields
| Field | Type | Required | Description |
|---|---|---|---|
org_name | string | Yes | Human-readable organization name |
org_domain | string | Yes | Email domain that triggers this SSO config (e.g., acme.com) |
provider_type | string | Yes | "saml" or "oidc" |
entity_id | string | SAML | IdP entity ID / issuer from the IdP metadata |
sso_url | string | SAML | IdP SSO redirect URL |
x509_certificate | string | SAML | PEM-encoded X.509 signing certificate from the IdP (without headers) |
oidc_provider | string | OIDC | "google", "github", "microsoft", or "auth0" |
client_id | string | OIDC | OAuth2 client ID from the IdP |
client_secret | string | OIDC | OAuth2 client secret from the IdP |
role_mapping | object | No | Maps IdP group names to platform roles |
jit_provisioning | boolean | No | Create users on first login (default: true) |
is_enforced | boolean | No | Block password login for this org's domain (default: false) |
Provider-Specific Setup
Okta SAML 2.0
- In Okta, go to Applications → Create App Integration → SAML 2.0.
- Retrieve the SP metadata XML from:
GET /auth/sso/saml/{config_id}/metadata - In Okta, set the following:
- Single sign-on URL (ACS URL):
https://your-platform.com/auth/sso/saml/{config_id}/acs - Audience URI (SP Entity ID): The
SAML_SP_ENTITY_IDenvironment variable value - Name ID format:
EmailAddress - Application username:
Email
- Single sign-on URL (ACS URL):
- Under Attribute Statements, add:
email→user.emailfirst_name→user.firstNamelast_name→user.lastName
- Under Group Attribute Statements, add:
- Name:
groups, Filter:Matches regex .*
- Name:
- Download the IdP metadata XML or copy the certificate and SSO URL.
- Create the SSO config via the API with
entity_id,sso_url, andx509_certificatefrom the Okta metadata.
Azure Active Directory (SAML)
- In Azure, go to Enterprise Applications → New Application → Create your own application → Integrate with Azure AD (non-gallery).
- Under Single sign-on → SAML, configure:
- Identifier (Entity ID): value of
SAML_SP_ENTITY_ID - Reply URL (ACS URL):
https://your-platform.com/auth/sso/saml/{config_id}/acs
- Identifier (Entity ID): value of
- Under Attributes & Claims, ensure the
emailaddressclaim maps touser.mail. - Add a group claim: Groups assigned to the application.
- Download the Certificate (Base64) and copy the Login URL and Azure AD Identifier.
- Create the SSO config using the downloaded certificate and the Login URL as
sso_url.
Azure Active Directory (OIDC)
- In Azure, go to App Registrations → New Registration.
- Set the redirect URI to:
https://your-platform.com/auth/sso/oidc/microsoft/callback. - Under Certificates & Secrets, create a new client secret.
- Under API Permissions, add
openid,profile,email, andGroupMember.Read.All. - Create the SSO config:
curl -X POST https://api.example.com/auth/sso/configs \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"org_domain": "acme.com",
"provider_type": "oidc",
"oidc_provider": "microsoft",
"client_id": "<Application (client) ID>",
"client_secret": "<client secret value>",
"role_mapping": { "ExperimentationAdmins": "ADMIN" }
}'
Google Workspace (OIDC)
- In Google Cloud Console, go to APIs & Services → Credentials → Create OAuth 2.0 Client ID.
- Set the application type to Web application.
- Add the authorized redirect URI:
https://your-platform.com/auth/sso/oidc/google/callback. - Copy the Client ID and Client Secret.
- Create the SSO config:
curl -X POST https://api.example.com/auth/sso/configs \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"org_domain": "acme.com",
"provider_type": "oidc",
"oidc_provider": "google",
"client_id": "123456789-abc.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"role_mapping": {}
}'
Google OIDC does not natively provide group membership. If role mapping by group is required, use Google Workspace Directory API and configure role_mapping with Google Group email addresses (e.g., "admins@acme.com": "ADMIN").
GitHub (OIDC)
- In GitHub, go to Settings → Developer Settings → OAuth Apps → New OAuth App (for a personal app) or Organization Settings → OAuth Apps (for an org app).
- Set the Authorization callback URL to:
https://your-platform.com/auth/sso/oidc/github/callback. - Create the SSO config:
curl -X POST https://api.example.com/auth/sso/configs \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"org_domain": "acme.com",
"provider_type": "oidc",
"oidc_provider": "github",
"client_id": "Ov23li...",
"client_secret": "abc123...",
"role_mapping": {
"acme-org/admin-team": "ADMIN",
"acme-org/engineers": "DEVELOPER"
}
}'
GitHub team slugs in role_mapping should be in the format {org-name}/{team-slug}.
JIT User Provisioning
When jit_provisioning: true (the default), users who authenticate via SSO for the first time are automatically provisioned with a platform account.
The following attributes are populated from the IdP assertion or token:
| Platform Field | SAML Source | OIDC Source |
|---|---|---|
email | NameID or email attribute | email claim |
first_name | first_name attribute | given_name claim |
last_name | last_name attribute | family_name claim |
role | Derived from groups via role_mapping | Derived from groups via role_mapping |
If no role mapping matches, the user is provisioned with the VIEWER role by default.
Group-to-Role Mapping
The role_mapping field maps IdP group names or email addresses to platform roles.
{
"role_mapping": {
"admin-group": "ADMIN",
"developers": "DEVELOPER",
"data-analysts": "ANALYST",
"product-managers": "VIEWER"
}
}
Rules:
- Matching is case-sensitive.
- A user's role is determined by the first matching group in the mapping. Order groups from highest to lowest privilege.
- Users in no mapped group receive the
VIEWERrole if JIT provisioning is enabled. - If a user is in multiple mapped groups, the highest-privilege role wins (ADMIN > DEVELOPER > ANALYST > VIEWER).
Enforced SSO
When is_enforced: true, all users whose email matches org_domain must authenticate via SSO. Password-based login is blocked for those accounts.
curl -X PUT https://api.example.com/auth/sso/configs/{config_id} \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_enforced": true}'
Before enforcing SSO:
- Verify that all users in the organization can successfully complete the SSO flow.
- Ensure at least one ADMIN user is accessible through the IdP.
- Test the enforcement with a non-admin account before applying it to the entire organization.
Enforced SSO does not affect platform super-admin accounts, which can always log in with credentials as a break-glass mechanism.
Security
State Tokens and CSRF Protection
The platform generates a cryptographically random state token for each OIDC flow initiation and SAML authentication request. The token is verified upon callback to prevent CSRF attacks. Tokens are single-use and expire after 10 minutes.
SAML Assertion Validation
The platform validates all of the following before accepting a SAML assertion:
- XML signature using the configured
x509_certificate NotBeforeandNotOnOrAftertime constraints (5-minute clock skew tolerance)Audiencerestriction matches the SP entity IDInResponseTomatches the pending authentication request ID (replay prevention)- HTTP POST binding is required; HTTP Redirect binding is not accepted for assertions
Certificate Rotation
To rotate the IdP signing certificate without downtime:
- Add the new certificate as a second entry in a
x509_certificatesarray (if your IdP supports multiple certificates simultaneously). - Update the SSO config with the new certificate once the IdP is rotated.
- Remove the old certificate.
OIDC Token Validation
OIDC ID tokens are validated using the IdP's JWKS endpoint (/.well-known/openid-configuration). The platform caches the public keys with automatic refresh on key rotation.
Environment Variables
Configure the following in your deployment environment before enabling SSO:
# Feature flag
SSO_ENABLED=true
# SAML Service Provider settings
SAML_SP_ENTITY_ID=https://your-platform.com
SAML_SP_ACS_URL=https://your-platform.com/auth/sso/saml/acs
SAML_SP_PRIVATE_KEY=<base64-encoded PKCS8 private key>
SAML_SP_CERTIFICATE=<base64-encoded X.509 public certificate>
# OIDC provider credentials
OIDC_GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
OIDC_GOOGLE_CLIENT_SECRET=GOCSPX-...
OIDC_GITHUB_CLIENT_ID=Ov23li...
OIDC_GITHUB_CLIENT_SECRET=abc123...
OIDC_MICROSOFT_CLIENT_ID=<Azure Application (client) ID>
OIDC_MICROSOFT_CLIENT_SECRET=<Azure client secret>
OIDC_MICROSOFT_TENANT_ID=<Azure Directory (tenant) ID>
OIDC_AUTH0_CLIENT_ID=...
OIDC_AUTH0_CLIENT_SECRET=...
OIDC_AUTH0_DOMAIN=your-tenant.auth0.com
# State token security
# Generate with: openssl rand -hex 32
SSO_STATE_SECRET=<random-256-bit-hex-secret>
# JIT provisioning default role when no group mapping matches
SSO_DEFAULT_ROLE=VIEWER
# Session cookie settings
SSO_SESSION_COOKIE_SECURE=true
SSO_SESSION_COOKIE_SAMESITE=Lax
Troubleshooting
"SAML signature validation failed"
- Verify that
x509_certificatein the SSO config matches the current signing certificate in the IdP. - Ensure the certificate is PEM-encoded without the
-----BEGIN CERTIFICATE-----/-----END CERTIFICATE-----headers. - Check for trailing whitespace or newlines in the certificate string.
"Invalid ACS URL"
- Confirm the ACS URL configured in the IdP matches exactly:
https://your-platform.com/auth/sso/saml/{config_id}/acs - The
config_idin the URL must match the UUID returned byPOST /auth/sso/configs.
"State token expired or invalid"
- The OIDC or SAML flow took longer than 10 minutes to complete.
- The user's browser blocked the state cookie (check
SameSiteandSecuresettings). - Multiple tabs initiated different auth flows simultaneously.
"User provisioned but has wrong role"
- Check that the
role_mappingkeys exactly match the group names sent by the IdP. - For SAML, inspect the raw assertion to confirm the
groupsattribute is being sent. Use a SAML tracer browser extension during testing. - For GitHub OIDC, verify the team slug format is
{org-name}/{team-slug}.
"SSO not triggered for my email domain"
- Confirm
org_domainin the SSO config exactly matches the domain portion of the user's email (case-insensitive comparison is applied, but no wildcard support). - Verify the SSO config is active (
is_active: true) by callingGET /auth/sso/configs/{config_id}.