Enterprise SSO

Live Last updated

Connect your identity provider — SAML or OIDC — so your people sign in with your IdP instead of an emailed link, verify your email domains, and (optionally) make the IdP the only way in.

Live. Every step on this page works today. The examples call this deployment's API at https://app.skipthesell.com; the same screens live in the console under Settings → SSO. The vocabulary lives in core concepts.

If your organization's policy says your people authenticate through your own identity provider (IdP) — Okta, Microsoft Entra ID, Google Workspace, Ping, anything that speaks SAML 2.0 or OIDC — an SSO connection is how your Skip the Sell tenant honors it. Your members sign in through your IdP instead of an emailed magic link, and the session they land in is an ordinary member session: same scopes, same audit trail, same revocation.

This page is written for the two people who share the work: the tenant's owner (who configures the connection here) and your IdP administrator (who configures the application on the IdP's side). Each half needs one thing from the other, listed below.

What an SSO connection is#

An SSO connection is your tenant's configured link to one IdP. It is operational policy — a tenant setting, like your seller handle — not part of your versioned configuration. One tenant usually holds one connection, but may hold several (a migration window, say).

A connection is created as a draft, configured, then activated by a human, and can be deactivated again. Drafts never appear in sign-in routing. Per connection you configure:

Setting Meaning Default
Protocol saml or oidc — a field of one resource, not two kinds —
Trust material The IdP's metadata and keys (SAML), or the issuer, client id and secret (OIDC) —
Claimed domains The email domains the connection claims for routing and provisioning, each verified by a DNS TXT record before it does anything none
Enforcement When on, every non-SSO sign-in path is refused for your members on the connection's verified claimed domains — the IdP becomes the only way in off
Provisioning policy invite-only (a first sign-in by an unknown IdP subject is refused) or auto (it creates a member at the connection's default role) invite-only
Default role The role auto provisioning creates members at — viewer, deal-manager or configurator, never owner viewer

Setting up a SAML connection#

You give the IdP admin: the service-provider (SP) metadata — we are the SP, your IdP is the assertive side. Once the draft connection exists it has stable URLs derived from this deployment's base URL and the connection id:

  • SP metadata (download and hand over, or point the IdP at it): https://app.skipthesell.com/auth/sso/{sso_connection_id}/metadata
  • Entity ID (audience): https://app.skipthesell.com/auth/sso/{sso_connection_id}
  • Assertion consumer service (ACS / recipient URL): https://app.skipthesell.com/auth/sso/{sso_connection_id}/acs

The IdP admin gives you: the IdP's metadata XML (entity ID, sign-in URL, X.509 signing certificate). It goes into the connection's config as idp_metadata. If your IdP asserts the email somewhere other than the NameID, name the attribute in email_attribute — by default we read the NameID.

Assertions are validated strictly: the signature (including XML signature wrapping), the audience and recipient, the validity window against a small clock skew, and single-use within that window. Encrypted assertions are optional: put the SP's RSA private key PEM in sp_private_key, together with its X.509 certificate PEM in the same value — the SP metadata advertises an encryption key only when the certificate is present, so an IdP cannot turn encryption on without it. The member is sealed at rest and served only masked.

Setting up an OIDC connection#

You give the IdP admin: the redirect URI to register the client with:

https://app.skipthesell.com/auth/sso/{sso_connection_id}/callback

The IdP admin gives you: the issuer URL, the client id and the client secret. They go into the connection's config as issuer, client_id and client_secret (the secret is sealed at rest and served only masked). The email is read from the standard email claim, or whichever claim you name in email_claim.

The sign-in uses authorization-code flow with PKCE and a single-use nonce; the issuer's discovery document and JWKS are fetched from the issuer URL you configured.

Verifying your email domains#

A claimed domain is inert until verified: before a domain may route sign-ins, before enforcement applies to it, and before auto provisioning may activate, the connection must prove control of the domain with a DNS TXT record.

  1. Claim the domain on the connection. The response carries a check_token.

  2. Publish a TXT record at the domain's zone apex containing that token — for acme.example:

    acme.example.  IN  TXT  "<the claim's check token>"
    
  3. Re-check the claim (the console's button, or verify_domains below). One TXT record containing the token is the proof.

Domains are canonicalized (lowercase, IDNA A-label, no trailing dot) — DNS is case-insensitive, so raw spellings would defeat both uniqueness and routing. A verified claim is unique across tenants: an email domain routes to at most one tenant's connection, so one tenant can never intercept another's people.

Turning the connection on, and what members experience#

Activation is a human decision — an owner (or a member holding sso:write) activates from the console; an API key cannot. From then on:

  • The sign-in page asks for email first, as always. For an address whose domain sits on an active connection's verified claimed domains, it offers Sign in with SSO — alongside the magic link unless enforcement is on. The lookup says "SSO is available for this domain"; it never reveals whether a particular address is a member.
  • The first sign-in links the member: an existing member whose asserted email matches is linked and signed in (the migration path — nobody re-enrolls). Under auto provisioning, an unknown subject is created as a member at the default role. Under invite-only (the default), they are told to ask their administrator.
  • Your IdP's unsolicited (IdP-initiated) sign-ins are accepted and validated identically.

With enforcement on, magic-link sign-in (and its sign-in code), platform OAuth, and the invitation email's link are refused for members on the verified claimed domains — every refusal keeps its anti-enumeration shape and lands in your audit feed. Members holding the owner role always keep email sign-in — that is the escape hatch that keeps a dead IdP from locking your tenant out permanently.

The API surface#

Everything above is API, like every console action (scopes sso:read / sso:write; owner-only in permission schema v1):

# operationId: create_sso_connection
curl -X POST https://app.skipthesell.com/api/v1/sso-connections \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <generate a fresh value>' \
  -d '{"protocol": "saml"}'

# operationId: update_sso_connection — config, policy and domains,
# If-Match-guarded on the revision ETag the reads carry
curl -X PATCH https://app.skipthesell.com/api/v1/sso-connections/{sso_connection_id} \
  -H 'If-Match: "r1"' \
  -H 'Content-Type: application/json' \
  -d '{"config": {"idp_metadata": "<the IdP'"'"'s metadata XML>"},
       "claim_domains": ["acme.example"]}'

# operationId: activate_sso_connection — the human decision
curl -X POST https://app.skipthesell.com/api/v1/sso-connections/{sso_connection_id}/activate

Activation and every security-relevant change to a live connection — trust material, provisioning policy, default role, domain claims, turning enforcement off — require a human member session even for an API key holding the scope: an API key may draft and configure freely, and may turn enforcement on, but cannot re-point a live connection at an IdP it controls or downgrade it. Members list and remove their own linked identities on the sessions screen; owners audit who is linked per member.

Every connection change, every link and unlink, every SSO sign-in and every refused sign-in is an audit-feed event — see your console's audit page (scope audit-event:read).

What to tell your security reviewer#

  • Sign-in through the connection lands in the same session model as the magic link (seller setup walks the email path): hashed secret cookie, idle and absolute expiry, revocation, and a per-session auth_method your members can see.
  • The asserted email must fall on one of the connection's verified claimed domains — a tenant cannot provision "verified" members for domains it does not control, and a member is never moved between tenants or given a new email by an IdP assertion.
  • auto provisioning is your explicit opt-in; the default is invite-only. The default role is never owner.
  • Rejected assertions are audited with their reason, and a source that keeps failing is throttled.