UptimeHunt Docs
OrganizationsSingle Sign-On

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):

ValueForm
Assertion Consumer Service (ACS) URLhttps://auth.uptimehunt.io/accounts/saml/{org-slug}/acs/
SP metadata URLhttps://auth.uptimehunt.io/accounts/saml/{org-slug}/metadata/
SP Entity IDShown 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):

    FieldAccepted attribute names
    Emailurn:oid:0.9.2342.19200300.100.1.3 · http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
    First namehttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname · urn:oid:2.5.4.42
    Last nameurn:oid:2.5.4.4
    Subject IDurn: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_mapping object (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.

FieldDescription
config.idp_entity_idRequired. Your IdP's entity ID
config.idp_sso_urlRequired. Your IdP's SSO (sign-in) URL
config.idp_x509certRequired. Your IdP's X.509 signing certificate (this is the IdP's public certificate)
config.idp_slo_urlOptional single-logout URL
config.attribute_mappingOptional 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_mappingOptional {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_provisioningAuto-create accounts on first sign-in (details)
jit_requires_invitationOnly JIT-provision users with a matching pending invitation (default false)
allowed_email_domainsOptional 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_roleRole granted to JIT-provisioned members (default member)
skip_role_syncWhen 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)
enabledActivates the IdP for login routing
enforced_ssoRequire 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 source is the SAML attribute Name (the exact, case-sensitive Name="…" on the <Attribute> element — not its FriendlyName) that carries group membership. Groups always travel in an AttributeStatement <Attribute> with one <AttributeValue> per group — never in the NameID.
  • Each rule value is 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:

IdPsource (attribute Name)Value format · setup
Oktaa name you choose (convention: groups)Group display names. Apps → Sign OnSAML SettingsGroup Attribute Statements; set a filter (e.g. Matches regex .* for all groups)
Microsoft Entra IDhttp://schemas.microsoft.com/ws/2008/06/identity/claims/groupsObject-ID GUIDs by default — your rule values must be GUIDs unless you emit names (see caveat below)
Keycloaka 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
authentikhttp://schemas.xmlsoap.org/claims/GroupGroup 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 SAMLwhatever 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=ApplicationGroup with 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_sources indirection 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's overage_action (default deny_new): existing members keep their role, and a brand-new just-in-time sign-in is refused — lookup failures block new provisioning regardless of unmapped_policy. Set overage_action: allow_existing to provision such users at default_role instead, or ignore to treat the unreadable attribute as an empty group set (so unmapped_policy governs). 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/NotOnOrAfter validity 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_invitation for invitation-gated provisioning, or the declared allowed_email_domains allowlist to filter JIT by asserted email domain.

Troubleshooting

SymptomLikely cause
sso/test fails on idp_x509cert_validThe 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: signatureIdP 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: expiredClock skew between IdP and UptimeHunt, or assertion NotOnOrAfter too tight — check IdP clock/NTP

On this page