SSO with SAML 2.0
Connect any SAML 2.0 identity provider as your organization's identity provider for member authentication.
SSO with SAML 2.0
Overview
Connect any SAML 2.0 identity provider as your organization's identity provider.
Prerequisites:
- Owner role in the organization (
sso.edit) - Admin access to your IdP to create a SAML application
That's all — the IdP entity ID, SSO URL, and signing certificate below are a complete, working configuration. Verifying your email domain is optional: it makes the login page advertise your IdP to anyone typing an email under that domain, but members can always sign in through the organization sign-in link without it.
Service Provider (SP) Values
When you select SAML in Organization → Authentication, UptimeHunt displays your organization's SP values — before the configuration is first saved, so you can register them at your IdP up front. They are served from the UptimeHunt authentication host and have these forms (always copy the exact values from the settings page):
| Value | Form |
|---|---|
| Assertion Consumer Service (ACS) URL | https://auth.uptimehunt.io/accounts/saml/{org-slug}/acs/ |
| SP metadata URL | https://auth.uptimehunt.io/accounts/saml/{org-slug}/metadata/ |
| SP Entity ID | Shown on the settings page — defaults to the SP metadata URL (also embedded in the SP metadata XML) |
Most IdPs can consume the SP metadata URL directly, which configures the ACS endpoint, entity ID, and bindings in one step.
The same values are available programmatically: GET /api/v1/orgs/{slug}/sso/urls?provider_type=saml (requires sso.view) computes the SP endpoints from the organization slug alone — no saved configuration required, so automation (Terraform, Ansible, scripts) can create the IdP application before the first PUT:
curl "https://app.uptimehunt.io/api/v1/orgs/acme/sso/urls?provider_type=saml" \
-H "Authorization: Bearer {access_token}"{
"data": {
"provider_type": "saml",
"urls": {
"login": "https://auth.uptimehunt.io/accounts/saml/acme/login/?process=login",
"acs": "https://auth.uptimehunt.io/accounts/saml/acme/acs/",
"sp_metadata": "https://auth.uptimehunt.io/accounts/saml/acme/metadata/",
"sp_entity_id": "https://auth.uptimehunt.io/accounts/saml/acme/metadata/"
}
}
}(sp_entity_id here is the pre-save default — the SP metadata URL.)
Step 1 — Create the SAML Application in Your IdP
Configure your IdP application with the SP values above, and set:
-
NameID format:
persistent(a stable, opaque identifier). The persistent NameID is the identity anchor — do not use email as the NameID, since email changes would otherwise look like new users. -
Attribute mapping: assert the user's email (and optionally first/last name) as attributes. UptimeHunt reads these attribute names by default (first match wins):
Field Accepted attribute names Email urn:oid:0.9.2342.19200300.100.1.3·http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddressFirst name http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname·urn:oid:2.5.4.42Last name urn:oid:2.5.4.4Subject ID urn:oasis:names:tc:SAML:attribute:subject-id(falls back to the persistent NameID when absent)Different names can be configured via the API-only
config.attribute_mappingobject (see Step 2). -
Assertion signing: assertions must be signed. Unsigned or tampered assertions are rejected.
Collect from your IdP: the IdP entity ID, SSO URL, and X.509 signing certificate. Most IdPs publish all three in their SAML metadata XML — you can paste that XML directly into UptimeHunt and it will fill the three fields for you (see Paste IdP metadata XML below); UptimeHunt does not fetch IdP metadata URLs automatically.
Step 2 — Configure the Organization
UI: Organization → Authentication → SAML — enter the IdP entity ID, SSO URL, and signing certificate (or paste the metadata XML to fill them automatically).
Paste IdP metadata XML
The SAML credentials form has an Import IdP metadata box: paste your IdP's metadata XML and UptimeHunt extracts the three values into the fields below it —
- IdP entity ID ← the
<EntityDescriptor entityID="…">, - IdP SSO URL ← the
<SingleSignOnService>Location(the HTTP-POST binding, falling back to HTTP-Redirect), - IdP X.509 certificate ← the signing
<X509Certificate>, with all whitespace stripped.
The parsing happens entirely in your browser — nothing is sent to UptimeHunt until you Save, so importing only pre-fills the form; it is not itself a save. Review the filled-in values and then save as usual. Invalid XML, or metadata missing any of the three elements, is reported inline and fills nothing.
API:
curl -X PUT https://app.uptimehunt.io/api/v1/orgs/acme/sso \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"provider_type": "saml",
"config": {
"idp_entity_id": "https://idp.acme.com/saml/metadata",
"idp_sso_url": "https://idp.acme.com/saml/sso",
"idp_x509cert": "MIIC..."
},
"jit_provisioning": true,
"default_role": "member",
"enabled": true
}'The IdP values go inside the config object; the top-level fields are the organization-side toggles.
| Field | Description |
|---|---|
config.idp_entity_id | Required. Your IdP's entity ID |
config.idp_sso_url | Required. Your IdP's SSO (sign-in) URL |
config.idp_x509cert | Required. Your IdP's X.509 signing certificate (this is the IdP's public certificate) |
config.idp_slo_url | Optional single-logout URL |
config.attribute_mapping | Optional object overriding the default attribute names — keys are email, first_name, last_name, uid; values are lists of attribute names to try in order. API-only: it is not editable in the UI |
config.role_mapping | Optional {source, rules, unmapped_policy, overage_action} object mapping SAML group attribute values to roles (Role Mapping). source is the SAML attribute Name; overage_action (default deny_new) governs an Entra ID group overage; {"rules": []} clears it |
jit_provisioning | Auto-create accounts on first sign-in (details) |
jit_requires_invitation | Only JIT-provision users with a matching pending invitation (default false) |
allowed_email_domains | Optional declared email-domain allowlist for JIT: when non-empty, sign-ins asserting an email whose domain is not listed are refused at provisioning. Plain declared list — never DNS-verified. Default [] (no filtering) |
default_role | Role granted to JIT-provisioned members (default member) |
skip_role_sync | When true, config.role_mapping runs only at account creation, never on later sign-ins — so a manual role override is not reverted (default false; no effect without a configured role_mapping) |
enabled | Activates the IdP for login routing |
enforced_sso | Require every member to sign in through this IdP. Enabling it must pass the readiness guards and immediately signs all members out |
Step 3 — Test
Validate the stored configuration:
curl -X POST https://app.uptimehunt.io/api/v1/orgs/acme/sso/test \
-H "Authorization: Bearer {access_token}"This checks that the IdP SSO URL answers HTTP and that the stored signing certificate parses as X.509 and is inside its validity window — response shape and check details in Testing the Configuration.
Then sign in from a private browser window with a corporate email: enter the email on the login page and choose the organization route.
Role Mapping (attribute → role)
Map the groups your IdP asserts in the SAML assertion to UptimeHunt roles, instead of giving every member the same default_role. The shared mechanics — ordered rules, first-match-wins, the unmapped-members policy (including Refuse sign-in), the Owner warning, the last-Owner guard, re-sync on every sign-in, and the audit events — are documented once under SSO → Role Mapping. This section covers only the SAML-specific part: where the group values come from.
- The
sourceis the SAML attribute Name (the exact, case-sensitiveName="…"on the<Attribute>element — not its FriendlyName) that carries group membership. Groups always travel in anAttributeStatement<Attribute>with one<AttributeValue>per group — never in the NameID. - Each rule
valueis matched verbatim against the attribute values your IdP emits. Configure your IdP to send one<AttributeValue>per group, not a single comma-joined value.
UI: Organization → Authentication → SAML → Role mapping — set the SAML attribute name (source), then add rules pairing an attribute value with a role, ordered most-privileged first.
`unmapped_policy: deny` blocks new JIT users whose groups aren't mapped
If you set the unmapped policy to Refuse sign-in (deny) and rely on JIT provisioning to onboard people, a brand-new user whose asserted group attribute matches no rule is refused on their first sign-in — JIT does not save them, because deny is evaluated before the account is created. Either keep every legitimate group in a rule, or use a non-deny unmapped policy (a concrete role, or '' for the default role). Full interaction details.
API (inside the SAML config):
curl -X PUT https://app.uptimehunt.io/api/v1/orgs/acme/sso \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"provider_type": "saml",
"config": {
"idp_entity_id": "https://idp.acme.com/saml/metadata",
"idp_sso_url": "https://idp.acme.com/saml/sso",
"idp_x509cert": "MIIC...",
"role_mapping": {
"source": "groups",
"rules": [
{"value": "platform-admins", "role": "owner"},
{"value": "sre", "role": "admin"},
{"value": "engineering", "role": "member"}
],
"unmapped_policy": "member"
}
}
}'Per-IdP attribute guidance
The attribute Name and the value format differ per IdP — set source to the exact Name your IdP emits, and write each rule value in the format that IdP sends:
| IdP | source (attribute Name) | Value format · setup |
|---|---|---|
| Okta | a name you choose (convention: groups) | Group display names. Apps → Sign On → SAML Settings → Group Attribute Statements; set a filter (e.g. Matches regex .* for all groups) |
| Microsoft Entra ID | http://schemas.microsoft.com/ws/2008/06/identity/claims/groups | Object-ID GUIDs by default — your rule values must be GUIDs unless you emit names (see caveat below) |
| Keycloak | a name you choose (convention: groups or member_of) | The Group List mapper emits the full group path with a leading slash by default (/parent/child) — include the slash in rule values, or disable Full group path in the mapper |
| authentik | http://schemas.xmlsoap.org/claims/Group | Group display names (renaming a group breaks the mapping); this is the default Groups SAML property mapping (note the capital Group and the trailing s in claims) |
| Google Workspace SAML | whatever you typed in the Google App attribute field (use groups) | Group email addresses (e.g. engineers@acme.com), not display names — renaming a group changes its email and breaks the mapping until you update both the Admin-console attribute mapping and the UptimeHunt rules — see Google Workspace |
Microsoft Entra ID emits GUIDs, and goes silent past 150 groups
Two Entra-specific gotchas:
- Default values are Object-ID GUIDs, not names. To emit display names instead, add a group claim with Source attribute = "Cloud-only group display names" (requires
groupMembershipClaims=ApplicationGroupwith the groups assigned to the app), or — for on-prem-synced groups —sam_account_name. Otherwise your rule values must be the security groups' GUIDs. - Group overage at 150 groups (SAML). When a user is in more than 150 groups, Entra omits the groups attribute entirely and replaces it with a
_claim_names/_claim_sourcesindirection pointing at the Microsoft Graph API. UptimeHunt detects this and treats it as a lookup failure (sso.role_mapping.lookup_failed), not an empty group set, and resolves it with the mapping'soverage_action(defaultdeny_new): existing members keep their role, and a brand-new just-in-time sign-in is refused — lookup failures block new provisioning regardless ofunmapped_policy. Setoverage_action: allow_existingto provision such users atdefault_roleinstead, orignoreto treat the unreadable attribute as an empty group set (sounmapped_policygoverns). Heavy-group-membership tenants should keep such users in a small set of role-granting groups, or assign roles manually.
Security Properties
- SP-initiated flow only. IdP-initiated SSO (unsolicited assertions posted to the ACS) is disabled — sign-ins always start at the UptimeHunt login page.
- Assertions are validated per the SAML 2.0 spec: signature, audience restriction,
NotBefore/NotOnOrAftervalidity window, and replay protection (each assertion ID is accepted once). - The persistent NameID is scoped to your organization's IdP (identity realm) — the same person arriving via another organization's IdP is a separate account.
- There is no email-domain gate by default: whoever your IdP authenticates is admitted (JIT gating) — asserted emails are display attributes. Use
jit_requires_invitationfor invitation-gated provisioning, or the declaredallowed_email_domainsallowlist to filter JIT by asserted email domain.
Troubleshooting
| Symptom | Likely cause |
|---|---|
sso/test fails on idp_x509cert_valid | The stored certificate doesn't parse as X.509, hasn't become valid yet, or has expired — paste the IdP's current signing certificate into config.idp_x509cert |
| Assertion rejected: signature | IdP signing certificate in UptimeHunt doesn't match the one the IdP uses (rotated cert?) — update config.idp_x509cert with the IdP's current certificate |
| Assertion rejected: expired | Clock skew between IdP and UptimeHunt, or assertion NotOnOrAfter too tight — check IdP clock/NTP |