Skip to content

Single Sign-On (SSO)

Breeze RMM supports organization-level single sign-on through OpenID Connect (OIDC) and SAML 2.0. SSO allows users to authenticate with their existing identity provider (IdP) instead of managing separate Breeze passwords. Each organization can configure one active SSO provider and optionally enforce SSO-only login to disable password-based authentication entirely.


Concept Description
SSO Provider A configured identity provider (IdP) record tied to a single organization.
Provider type oidc (OpenID Connect) or saml (SAML 2.0).
Provider status inactive (default on creation), testing, or active. Only active providers are used for login.
Auto-provisioning When enabled (default), users who authenticate via SSO are automatically created in Breeze if they do not already exist.
SSO enforcement When enforceSSO is true, password-based login is disabled for the organization. Users must authenticate through the IdP.
Allowed domains Comma-separated list of email domains. If set, only users with matching email domains can authenticate via SSO.
Attribute mapping Maps IdP claim names to Breeze user fields (email, name, firstName, lastName, groups).
PKCE Proof Key for Code Exchange. Breeze uses PKCE (S256 method) for all OIDC authorization requests to prevent authorization code interception.
Provider presets Built-in configurations for common IdPs (Azure AD, Okta, Google Workspace, Auth0) that pre-fill scopes, attribute mappings, and endpoint URLs.

Breeze ships with built-in presets that auto-configure scopes, attribute mappings, and discovery URLs:

Preset ID Provider Issuer Template Scopes
azure-ad Microsoft Azure AD https://login.microsoftonline.com/{tenant}/v2.0 openid profile email
okta Okta https://{domain}.okta.com openid profile email groups
google Google Workspace https://accounts.google.com openid profile email
auth0 Auth0 https://{domain}.auth0.com openid profile email
Preset ID Provider SSO URL Template
azure-ad-saml Microsoft Azure AD (SAML) https://login.microsoftonline.com/{tenant}/saml2
okta-saml Okta (SAML) https://{domain}.okta.com/app/{appId}/sso/saml
onelogin-saml OneLogin (SAML) https://{domain}.onelogin.com/trust/saml2/http-post/sso/{appId}
adfs-saml AD FS https://{adfs-server}/adfs/ls
google-saml Google Workspace (SAML) https://accounts.google.com/o/saml2/idp?idpid={idpId}

You can retrieve the full list of presets (including attribute mappings) from the API:

Terminal window
curl https://breeze.yourdomain.com/api/v1/sso/presets \
-H "Authorization: Bearer $TOKEN"

  1. Register Breeze in your IdP. Create an application/client in your identity provider. Set the redirect URI to https://breeze.yourdomain.com/api/v1/sso/callback. Note the Client ID and Client Secret.

  2. Create the provider in Breeze. Use the POST /api/v1/sso/providers endpoint. If you specify a preset and an issuer, Breeze will auto-discover the authorization, token, userinfo, and JWKS endpoints via the .well-known/openid-configuration document.

  3. Test the configuration. Use the POST /api/v1/sso/providers/:id/test endpoint to verify that Breeze can reach the IdP’s discovery document.

  4. Activate the provider. Set the status to active via POST /api/v1/sso/providers/:id/status.

  5. Optionally enforce SSO. Update the provider with enforceSSO: true to disable password login for the organization.

Terminal window
curl -X POST https://breeze.yourdomain.com/api/v1/sso/providers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orgId": "ORG_UUID",
"name": "Azure AD",
"type": "oidc",
"preset": "azure-ad",
"issuer": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0",
"clientId": "YOUR_CLIENT_ID",
"clientSecret": "YOUR_CLIENT_SECRET"
}'

When you provide an issuer URL and the provider type is oidc, Breeze automatically fetches the IdP’s .well-known/openid-configuration document and populates:

  • authorizationUrl – Authorization endpoint
  • tokenUrl – Token endpoint
  • userInfoUrl – UserInfo endpoint
  • jwksUrl – JSON Web Key Set URI

If discovery fails (for example, if the IdP does not support well-known configuration), a warning is logged but the provider is still created. You can manually set these URLs via the PATCH endpoint afterward.


The Breeze SSO service provides a full SAML 2.0 toolkit:

  • SP Metadata generation – Produces a standards-compliant XML metadata document for your IdP.
  • AuthnRequest building – Generates SAML authentication requests with configurable NameID format and ForceAuthn.
  • Response parsing – Decodes and parses SAML responses, extracting NameID, SessionIndex, and attributes.
  • Attribute mapping – Maps SAML attribute URIs (e.g., http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress) to Breeze user fields.

Breeze generates SP metadata with the following values:

Field Value
Entity ID Configurable per deployment
ACS URL https://breeze.yourdomain.com/api/v1/sso/saml/acs
NameID Format urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
WantAssertionsSigned true

Breeze automatically maps common SAML attribute URIs to user fields:

SAML Attribute URI Breeze Field
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress email
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name name
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname firstName
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname lastName
http://schemas.microsoft.com/ws/2008/06/identity/claims/groups groups
urn:oid:0.9.2342.19200300.100.1.3 email
urn:oid:2.5.4.42 firstName
urn:oid:2.5.4.4 lastName
urn:oid:2.16.840.1.113730.3.1.241 name

The OIDC login flow follows the Authorization Code + PKCE pattern:

  1. User visits login page. The frontend calls GET /api/v1/sso/check/:orgId to determine if SSO is enabled for the organization.
  2. User clicks “Sign in with SSO”. The browser navigates to GET /api/v1/sso/login/:orgId.
  3. Breeze generates PKCE challenge and state. A cryptographic state, nonce, and PKCE codeVerifier/codeChallenge pair are generated. The PKCE verifier, nonce, and redirect path are stored server-side in the sso_sessions table (10-minute TTL); the state is additionally bound to the initiating browser via a signed, HttpOnly cookie (breeze_sso_state, SameSite=Lax, Secure in production, scoped to the callback path, 10-minute Max-Age). The cookie carries an HMAC-SHA256 signature of the state (keyed on APP_ENCRYPTION_KEY/SECRET_ENCRYPTION_KEY, distinct from the JWT secret).
  4. Redirect to IdP. Breeze redirects the user to the IdP’s authorization endpoint with the PKCE challenge, state, nonce, and redirect URI.
  5. User authenticates at IdP. The user signs in at the identity provider.
  6. IdP redirects back. The IdP redirects to GET /api/v1/sso/callback with an authorization code and state parameter.
  7. Breeze validates the browser binding, then claims the state. The state returned by the IdP is verified against the breeze_sso_state cookie using a constant-time comparison (crypto.timingSafeEqual) before anything else runs — a callback that did not originate from the same browser that started the flow is rejected. Only after the binding check does Breeze atomically consume the session row (DELETE ... RETURNING on a matching, non-expired state), which makes each state strictly single-use and immune to replay.
  8. Code exchange. Breeze exchanges the authorization code for tokens at the IdP’s token endpoint, including the PKCE codeVerifier.
  9. ID token verification. If an ID token is returned, Breeze verifies the issuer, audience, expiration, and nonce claims.
  10. User info retrieval. Breeze calls the IdP’s UserInfo endpoint to get the user’s profile.
  11. Attribute mapping. The IdP’s claims are mapped to Breeze user fields using the configured attributeMapping.
  12. Domain check. If allowedDomains is set, the user’s email domain is validated.
  13. User provisioning. If the user does not exist and autoProvision is enabled, a new Breeze user is created and assigned to the organization with the provider’s defaultRoleId.
  14. Session creation. Breeze creates a JWT token pair (access + refresh) and a session record.
  15. Redirect to app. The user is redirected to the original page with a short-lived token exchange code in the URL fragment (#ssoCode=...).
  16. Token exchange. The frontend calls POST /api/v1/sso/exchange with the code to receive the JWT tokens.
Browser Breeze API Identity Provider
│ │ │
│── GET /sso/login/:orgId ──►│ │
│ │── Generate PKCE ──► │
│◄── 302 Redirect ─│──────────────────► │
│ │ │
│──────── Authenticate ──────────────────►│
│ │ │
│◄──── Redirect with code ────────────────│
│ │ │
│── GET /sso/callback?code=...&state=... ►│
│ │── Exchange code ────►│
│ │◄── Tokens ──────────│
│ │── Get user info ───►│
│ │◄── Profile ─────────│
│ │ │
│◄── 302 /#ssoCode=... ──│ │
│ │ │
│── POST /sso/exchange ──►│ │
│◄── { accessToken, refreshToken } ──────│

Field Type Required Default Description
orgId uuid Depends Inferred for org-scoped users Target organization.
name string Yes Display name (1–255 characters).
type oidc or saml Yes Provider protocol.
preset string No Preset ID (e.g., azure-ad, okta). Pre-fills scopes and attribute mapping.
issuer url No IdP issuer URL. Triggers OIDC discovery when set.
clientId string No OAuth 2.0 Client ID.
clientSecret string No OAuth 2.0 Client Secret. Encrypted at rest.
scopes string No From preset or openid profile email Space-separated OAuth scopes.
attributeMapping object No From preset or { email: "email", name: "name" } Maps IdP claims to Breeze fields.
autoProvision boolean No true Create Breeze users on first SSO login.
defaultRoleId uuid No Role assigned to auto-provisioned users. Must be an org-scoped role in the target org.
allowedDomains string No Comma-separated email domains. Empty means all domains are allowed.
enforceSSO boolean No false Disable password login for the organization.
Status Description
inactive Default on creation. The provider is not used for login.
testing Intermediate state for validation. Not used for login.
active The provider is live. Users can authenticate via SSO. Only one active provider per organization is used for login.
Field Type Required Description
email string Yes IdP claim name that contains the user’s email address.
name string Yes IdP claim name for the user’s display name.
firstName string No IdP claim name for the user’s first name. Used as fallback when name is unavailable.
lastName string No IdP claim name for the user’s last name. Used with firstName to construct the display name.
groups string No IdP claim name for group membership. Expected to be an array of strings.

When a user authenticates via SSO, Breeze follows this logic:

  1. Email lookup. The mapped email attribute is used to find an existing Breeze user (case-insensitive).
  2. Existing user found. The user’s SSO identity link (user_sso_identities table) is created or updated with the latest profile, tokens, and login timestamp. The user is logged in.
  3. No existing user + auto-provision disabled. The login is rejected with a “user not found” error. An administrator must create the Breeze account first.
  4. No existing user + auto-provision enabled. A new Breeze user is created with the mapped email and name. The user is added to the organization with the provider’s defaultRoleId. SSO-provisioned users have no password set.
  5. Organization membership check. The user must have an organization_users record for the provider’s organization. If not, the login is rejected with “no org access”.

Each user-provider combination creates a record in the user_sso_identities table that stores:

  • External ID (sub claim from the IdP)
  • Email from the IdP
  • Profile data (full UserInfo response)
  • Access and refresh tokens (encrypted at rest)
  • Token expiration timestamp
  • Last login timestamp

This link is updated on every SSO login, keeping the profile and tokens current.


When enforceSSO is set to true on a provider, the organization’s login page should disable password-based authentication. The GET /api/v1/sso/check/:orgId endpoint returns enforceSSO: true so the frontend can hide the password form and redirect users directly to the IdP.

// GET /api/v1/sso/check/ORG_UUID
{
"ssoEnabled": true,
"provider": {
"id": "uuid",
"name": "Azure AD",
"type": "oidc"
},
"enforceSSO": true,
"loginUrl": "/api/v1/sso/login/ORG_UUID"
}

Column Type Description
id uuid Primary key.
org_id uuid Organization this provider belongs to (foreign key).
name varchar(255) Display name.
type sso_provider_type enum oidc or saml.
status sso_provider_status enum active, inactive, or testing.
issuer varchar(500) IdP issuer URL.
client_id varchar(255) OAuth Client ID.
client_secret text Encrypted OAuth Client Secret.
authorization_url varchar(500) Authorization endpoint (auto-discovered or manual).
token_url varchar(500) Token endpoint.
userinfo_url varchar(500) UserInfo endpoint.
jwks_url varchar(500) JWKS URI.
scopes varchar(500) Space-separated scopes. Default: openid profile email.
entity_id varchar(500) SAML Entity ID.
sso_url varchar(500) SAML SSO URL.
certificate text SAML signing certificate.
attribute_mapping jsonb Maps IdP claims to Breeze fields.
auto_provision boolean Auto-create users. Default: true.
default_role_id uuid Role for auto-provisioned users.
allowed_domains varchar(1000) Comma-separated allowed email domains.
enforce_sso boolean Disable password login. Default: false.
created_by uuid User who created the provider (foreign key).
created_at timestamp Creation timestamp.
updated_at timestamp Last update timestamp.
Column Type Description
id uuid Primary key.
user_id uuid Breeze user (foreign key).
provider_id uuid SSO provider (foreign key).
external_id varchar(255) User’s sub claim from the IdP.
email varchar(255) Email from the IdP.
profile jsonb Full UserInfo/SAML profile data.
access_token text Encrypted IdP access token.
refresh_token text Encrypted IdP refresh token.
token_expires_at timestamp Token expiration.
last_login_at timestamp Last SSO login timestamp.
created_at timestamp Record creation.
updated_at timestamp Last update.
Column Type Description
id uuid Primary key.
provider_id uuid SSO provider (foreign key).
state varchar(64) Login-flow state parameter (unique). Used to look up and atomically consume the session; CSRF protection is enforced by binding this value to the initiating browser via the signed breeze_sso_state cookie (see SSO Login Flow).
nonce varchar(64) ID token nonce.
code_verifier varchar(128) PKCE code verifier.
redirect_url varchar(500) Post-login redirect path.
expires_at timestamp Session expiration (10 minutes from creation).
created_at timestamp Creation timestamp.

All routes are prefixed with /api/v1/sso.

Method Path Permission Description
GET /sso/presets Authenticated List available provider presets with their default configurations.
GET /sso/providers Authenticated List SSO providers for the organization. Accepts orgId query parameter.
GET /sso/providers/:id Authenticated Get provider details. The clientSecret is not returned; hasClientSecret indicates if one is set.
POST /sso/providers organizations:write + MFA Create a new SSO provider. Returns with status: inactive.
PATCH /sso/providers/:id organizations:write + MFA Update provider configuration.
DELETE /sso/providers/:id organizations:write + MFA Delete a provider and all associated sessions and identity links.
POST /sso/providers/:id/status organizations:write + MFA Set provider status (active, inactive, testing).
POST /sso/providers/:id/test organizations:write + MFA Test OIDC provider configuration by running discovery.
Method Path Auth Description
GET /sso/check/:orgId None Check if SSO is enabled for an organization. Returns provider name, type, enforcement status, and login URL.
GET /sso/login/:orgId None Initiate SSO login. Generates PKCE + state and redirects to the IdP.
GET /sso/callback None IdP callback. Exchanges code for tokens, provisions user, creates session. Redirects to the app with a token exchange code.
POST /sso/exchange None Exchange a one-time SSO code for JWT access and refresh tokens.

SSO operations are recorded in the audit log:

Action Trigger
sso.provider.create Provider created. Includes type and status.
sso.provider.update Provider configuration changed. Includes list of changed fields.
sso.provider.delete Provider deleted.
sso.provider.status.update Provider status changed (active/inactive/testing).
sso.provider.test Provider configuration tested.

Variable Required Description
PUBLIC_URL or PUBLIC_APP_URL or DASHBOARD_URL Yes (one of) Base URL of the Breeze dashboard. Used to construct the SSO callback URI ({base}/api/v1/sso/callback). Falls back to http://localhost:3000.
SECRET_ENCRYPTION_KEY or APP_ENCRYPTION_KEY Yes Used to encrypt/decrypt the clientSecret at rest.

“No active SSO provider for this organization” (404)

Section titled ““No active SSO provider for this organization” (404)”

The GET /sso/login/:orgId endpoint could not find a provider with status: active for the given organization. Verify that:

  • A provider exists for the organization.
  • The provider status has been set to active via POST /sso/providers/:id/status.

The provider is missing one or more of clientId, clientSecret, or issuer. Update the provider via PATCH /sso/providers/:id with the missing values.

Breeze could not fetch the .well-known/openid-configuration document from the issuer URL. Check that:

  • The issuer URL is correct and reachable from the Breeze API server.
  • The IdP’s discovery endpoint returns a valid JSON document.
  • No firewall or proxy is blocking the connection.

The SSO session (state parameter) has expired, or the callback could not be matched to the browser that started the flow. SSO sessions are valid for 10 minutes, and the breeze_sso_state cookie that binds the flow to the originating browser shares that lifetime. The user should retry the login flow in the same browser. If this happens frequently, check for clock skew between the Breeze server and the IdP, and confirm the browser is not stripping the SameSite=Lax callback cookie (for example, when the IdP redirect crosses an unexpected origin).

The user’s email domain is not in the provider’s allowedDomains list. Update the provider to include the domain, or remove the allowedDomains restriction.

The user does not exist in Breeze and autoProvision is disabled on the provider. Either:

  • Enable auto-provisioning on the provider (autoProvision: true).
  • Create the user account manually before they attempt SSO login.

Auto-provisioning is enabled but no defaultRoleId is configured on the provider, or the configured role does not exist as an organization-scoped role. Set a valid defaultRoleId via PATCH /sso/providers/:id.

The user exists in Breeze but does not have an organization_users record for the provider’s organization. Add the user to the organization or enable auto-provisioning with a defaultRoleId.

“Invalid or expired token exchange code” (400)

Section titled ““Invalid or expired token exchange code” (400)”

The one-time SSO code passed to POST /sso/exchange has either:

  • Already been consumed (codes are single-use).
  • Expired (codes are valid for 2 minutes).
  • Been corrupted during the redirect.

The user should retry the SSO login flow.

SSO callback redirects to /login?error=sso_error

Section titled “SSO callback redirects to /login?error=sso_error”

An unhandled error occurred during the callback. Check the Breeze API server logs for the full error message. Common causes include:

  • Network connectivity issues between Breeze and the IdP’s token endpoint.
  • Mismatched clientSecret (the secret in the IdP does not match what was configured in Breeze).
  • ID token verification failure (wrong issuer, audience, or expired token).
  • Nonce mismatch (indicates a replay attack or stale session).