IAnswerable

Adding SSO to a Small SaaS

08 May 2026

An enterprise buyer asks “do you support SSO” before they ask about price. Saying no loses the deal regardless of how good your product is — their IT policy requires it, and that’s not negotiable from their side. Here’s the smallest implementation that actually satisfies procurement.

What “SSO” means to the buyer

They want their employees logging into your app through their identity provider (Okta, Azure AD, Google Workspace) using SAML or OIDC, so access is centrally managed — when someone’s offboarded in the IdP, they lose access to your app automatically, without your involvement. That automatic deprovisioning is usually the actual requirement behind “we need SSO,” more than the login UX itself.

SAML vs OIDC: which to build

SAML OIDC
Age / ubiquity Older, universally supported by enterprise IdPs Newer, growing support
Format XML JSON / JWT
Implementation effort Higher — XML signing is fiddly Lower — it’s OAuth with an identity layer
What enterprise buyers expect Still the default ask Increasingly accepted

If you can only build one, build SAML — it’s what most enterprise procurement checklists still name explicitly, even though OIDC is technically simpler to implement. Building OIDC-only will lose you deals with buyers whose IdP config only offers SAML for third-party apps.

Don’t hand-roll SAML

SAML’s XML signature validation is a well-documented source of real vulnerabilities (signature wrapping attacks are the classic one) when implemented from scratch. Use a maintained library:

The integration shape is the same regardless of library:

const saml = new SAML({
  entryPoint: 'https://customer.okta.com/app/xxx/sso/saml',
  issuer: 'https://yourapp.com',
  cert: customerIdpCertificate, // from their metadata
  callbackUrl: 'https://yourapp.com/sso/saml/callback',
})

app.get('/sso/saml/login', (req, res) => {
  saml.getAuthorizeUrl(req).then(url => res.redirect(url))
})

app.post('/sso/saml/callback', async (req, res) => {
  const profile = await saml.validatePostResponse(req.body)
  const user = await findOrCreateUser(profile.email, profile.nameID)
  req.session.userId = user.id
  res.redirect('/dashboard')
})

Multi-tenant: the part people get wrong first

Every customer has their own IdP, own certificate, own entity ID. You need per-tenant SAML config, not one global setting:

CREATE TABLE sso_configs (
  organization_id INT REFERENCES organizations(id),
  idp_entry_point TEXT,
  idp_cert TEXT,
  idp_issuer TEXT
);

Look up the tenant’s config by a slug or email domain before building the SAML request — yourapp.com/sso/login?org=acme-corp, or infer it from the email domain the user typed on your login page.

Just-in-time provisioning

Most enterprise buyers expect a user’s first SSO login to create their account automatically, matched by email, rather than requiring you to pre-provision every employee manually:

async function findOrCreateUser(email, nameId) {
  let user = await db.users.findByEmail(email)
  if (!user) {
    user = await db.users.create({ email, ssoNameId: nameId, role: 'member' })
  }
  return user
}

What enterprise buyers actually check during procurement

You don’t need all three to close deals, but “SSO-only enforcement” is the one that comes up most — a customer’s security team often requires that password login be disabled org-wide once SSO is configured.

The concrete next action

If you’re building this for your first enterprise deal, scope it to SAML with ruby-saml or node-saml, per-tenant config, and JIT provisioning. Skip building an admin UI for it until a second customer asks — for one customer, a manual config row in your database is enough.