Skip to main content

Single Sign-On (SSO)

Modulo supports SSO via OpenID Connect (OIDC) and Security Assertion Markup Language (SAML) 2.0. When an employee signs in for the first time through an identity provider (IdP), Modulo creates their account and organization membership automatically via Just-In-Time (JIT) provisioning.

SSO requires a Team-tier license (MODULO_LICENSE_KEY). The Community tier does not include SSO. Configuration is admin-only at /settings/sso.

Prerequisites

Two environment variables control where SSO redirects land. Both must be correct before you register redirect URIs at your IdP.

MODULO_PUBLIC_URL

The externally reachable URL of your Modulo instance (for example, https://your-modulo.example.com). Modulo builds the OIDC callback URL from this value:

https://<host>/api/v1/auth/oidc/<provider-slug>/callback

The IdP must accept redirects to this exact URL. If MODULO_PUBLIC_URL is wrong, every login attempt will fail with a redirect error.

MODULO_FRONTEND_URL

Where the browser lands after login (the /auth/callback route). Defaults to MODULO_PUBLIC_URL. You only need to set this if the single-page application (SPA) is served from a different origin than the API. In the standard self-hosted setup (nginx serves the SPA and the API on the same origin), no extra setting is needed.

The provider slug

When you create an SSO provider in the Modulo UI, there is no “provider ID” or “slug” field. The slug is derived automatically from the provider name:

  1. Lowercase the name
  2. Replace every run of non-alphanumeric characters with a single hyphen
  3. Strip leading/trailing hyphens
  4. Truncate to 58 characters
  5. If the slug already exists, append -2, -3, and so on (globally unique across all organizations)

Examples:

The provider slug
Provider name Slug
Google google
Azure AD azure-ad
Okta (Production) okta-production
My Long Provider Name Here 12345 my-long-provider-name-here-12345

This slug determines the redirect URI you must register at your IdP. The callback URL for a provider named Google is:

https://your-modulo.example.com/api/v1/auth/oidc/google/callback

A mismatch between the slug and the redirect URI registered at the IdP is the most common cause of the redirect_uri_mismatch error.

Google Workspace worked example

This walks through configuring Google as an OIDC provider, step by step.

Google Cloud Console setup

  1. Go to Google Cloud Console and create a new project, or select an existing one.
  2. Open the Google Auth Platform (console.cloud.google.com/auth/overview). The wizard walks you through three steps:
    • Branding: set the app name and user-support email.
    • Audience: choose Internal (Google Workspace organizations, no verification needed) or External (requires test users while in Testing status).
    • Contact information and the user-data policy agreement. Click Create when done. You land on the Overview page.
  3. Go to Clients in the left nav (console.cloud.google.com/auth/clients) and click Create client.
    • Application type: Web application.
    • Under Authorized JavaScript origins, add your Modulo URL: https://your-modulo.example.com.
    • Under Authorized redirect URIs, add the callback URL: https://your-modulo.example.com/api/v1/auth/oidc/google/callback.
    • Click Create. Copy the Client ID and Client secret.
    • (Alternative: the classic path APIs & Services > Credentials > Create Credentials > OAuth client ID works too and creates the same object.)
  4. Scopes (optional for basic sign-in): the openid, email, and profile scopes are non-sensitive and included by default. You can skip the Data Access step unless you need additional scopes.
  5. Test users (External audience only): while the publishing status is Testing, add every user who needs to sign in under Audience > Test users. Google blocks sign-in for unlisted users when the app is in Testing status.

Modulo setup

  1. Log in to Modulo as an admin and go to /settings/sso.
  2. Click Add Provider and select OIDC.
  3. Fill in the fields:
    • Name: Google (this determines the slug google)
    • Client ID: paste the Client ID from Google
    • Client secret: paste the Client secret from Google
    • Discovery URL: https://accounts.google.com/.well-known/openid-configuration
    • Scopes: openid email profile
  4. Click Create.
  5. Click Test on the newly created provider. The test should report the discovered endpoints (authorization endpoint, token endpoint, userinfo endpoint).
  6. Ensure the provider is enabled (the toggle should be on).

Verify the flow

  1. Log out of Modulo.
  2. The login page should now show a Google button.
  3. Click the button, complete the Google login, and confirm you land back in the Modulo app.

Login experience

When a user visits the login page, Modulo fetches enabled SSO providers via GET /api/v1/auth/sso/providers. The response lists each enabled OIDC provider (by slug) and whether SAML is enabled. Each OIDC provider becomes a button linking to /api/v1/auth/oidc/<slug>/login. SAML providers show a button linking to /api/v1/auth/saml/<slug>/login.

SAML setup

SAML 2.0 follows the same provider-slug pattern as OIDC. Each SAML provider gets its own per-provider URLs for metadata, ACS (Assertion Consumer Service), and login initiation. This is what makes multi-org SSO work on a single Modulo instance. In SAML terms Modulo is the service provider (SP) and your identity system is the identity provider (IdP).

Per-provider URLs

When you create a SAML provider named Okta, the slug okta is derived automatically (same rules as OIDC). The three per-provider URLs are:

Per-provider URLs
Endpoint URL Purpose
SP metadata GET /api/v1/auth/saml/okta/metadata Download this and give it to your IdP
ACS POST /api/v1/auth/saml/okta/acs The IdP posts SAML responses here
Login GET /api/v1/auth/saml/okta/login Redirect users here to start SAML login

Each provider has its own Entity ID (the SP’s identity in the SAML exchange). The default Entity ID is:

https://your-modulo.example.com/api/v1/auth/saml/okta

You can override the Entity ID on the provider form if your IdP requires a specific value.

What to give your IdP

  1. Download the SP metadata from GET /api/v1/auth/saml/<slug>/metadata.
  2. Register the ACS URL (POST .../<slug>/acs) and Entity ID at your IdP.
  3. Upload the IdP’s metadata XML to Modulo (paste it into the provider form, or provide a metadata URL).

Audience restriction (required)

Your IdP MUST include an AudienceRestriction in the SAML assertion that matches the provider’s Entity ID. Modulo rejects SAML responses that do not carry an AudienceRestriction matching the expected Entity ID.

This is the most common cause of “works in my IdP tester, fails in Modulo” reports. Most IdP testers do not enforce AudienceRestriction, so a misconfigured assertion passes the tester but fails at Modulo. Check that your IdP’s audience value matches the Entity ID shown in the provider metadata.

Legacy single-tenant endpoints

The original singleton SAML endpoints still exist and work for one-org installs:

Legacy single-tenant endpoints
Endpoint URL
ACS POST /api/v1/auth/saml/acs
Metadata GET /api/v1/auth/saml/metadata
Login GET /api/v1/auth/saml/login

These resolve IdP configuration from environment variables (MODULO_SAML_IDP_METADATA_URL / MODULO_SAML_IDP_METADATA_XML) rather than the database. New multi-org setups should use the per-provider URLs described above.

Provisioning and roles

What happens at first sign-in depends on the provider’s auto_provision field and its allowed_domains list. Three modes:

Provisioning and roles
auto_provision allowed_domains Who can join at sign-in
false (default) any Invitation only. A user must already hold an active membership, or hold a valid pending invitation for that email, which the sign-in consumes and grants the role it specifies. Anyone else is denied.
true non-empty Allowlisted domains. An authenticated user whose verified email domain is in the list is auto-provisioned. Everyone else falls back to the invitation path.
true empty Anyone who authenticates. Dangerous, and disabled unless the operator-level sso_unrestricted_provisioning feature flag is on. Saving the provider in this state without the flag is rejected with HTTP 422.

Two rules apply to every mode:

  • A pending invitation always wins. It is consumed first and grants the role the invitation specifies, not the provider default.
  • An existing membership’s role is never changed. Provisioning never promotes or demotes a member who is already active.

When a membership is granted, the role comes from the provider’s default_role field if set, otherwise from the MODULO_SSO_DEFAULT_ROLE environment variable (default runner). Valid values are operator and runner.

A denied sign-in leaves no partial state: Modulo refuses before any account or membership write, so a rejected identity creates no orphan row.

Group to team mappings

You can map IdP groups to Modulo teams. When a user authenticates, their group memberships are checked against the mappings. If a match is found, the user is added to (or their role is updated in) the corresponding Modulo team.

Each mapping has three fields:

Group to team mappings
Field Description
idp_group The group name or claim value from the IdP
team_id The UUID of the Modulo team
team_role The role to assign within the team (default: viewer)

For this to work, the IdP must emit a groups claim in the OIDC ID token. For SAML providers, the claim can be groups, memberOf, or Group.

Google limitation: Google’s standard OIDC ID token does not include a groups claim. To use group mappings with Google, you need extra claim provisioning at the IdP (for example, through a custom claim in Google Workspace or an intermediate service). Group mapping does not work out of the box with plain Google OIDC.

Troubleshooting

Troubleshooting
Symptom Likely cause Fix
Redirect lands on the wrong host MODULO_FRONTEND_URL or MODULO_PUBLIC_URL is incorrect Set both variables to the correct externally reachable URL
redirect_uri_mismatch at the IdP The redirect URI registered at the IdP does not match the slug-based callback URL Check the provider name in Modulo, derive the slug, and update the redirect URI at the IdP to match exactly
“Invalid state parameter” error Expired or blocked cookie, or a stale login tab Clear cookies, close stale tabs, and try again
No SSO button on the login page, or 402 error SSO feature not enabled (Community tier or missing license) Set MODULO_LICENSE_KEY to a Team-tier license
Connection test fails Discovery URL unreachable from the Modulo server Verify network access from the server to the IdP’s discovery endpoint
Users are provisioned but not added to teams Group mappings not configured, or the IdP does not emit a groups claim Configure group mappings and verify the IdP sends group information
Sign-in is denied with HTTP 401 and no account created auto_provision is off, or the user’s email domain is not in allowed_domains, and no invitation exists Either enable auto_provision with the user’s domain in allowed_domains, or send them an invitation
Provider saved with empty allowed_domains is rejected with HTTP 422 auto_provision=true with no allowlist is the unrestricted mode Add the allowed domains, or enable the sso_unrestricted_provisioning feature flag deliberately
SAML login works in IdP tester but fails in Modulo Missing or mismatched AudienceRestriction Check that the IdP sends an AudienceRestriction matching the provider’s Entity ID (visible in the SP metadata)
SAML response rejected with “audience” error Entity ID mismatch between IdP and Modulo The IdP’s audience value must exactly match the Entity ID in the provider metadata, including the full URL
IdP-initiated SSO fails silently IdP sends response to wrong ACS URL Register the per-provider ACS URL (/api/v1/auth/saml/<slug>/acs), not the legacy /saml/acs