Skip to main content

Overview

The Hub supports single sign-on via a shared federation framework (5app/hub, auth/modules/federation/). Every provider — LinkedIn OAuth2/OIDC, or any SAML 2.0 identity provider — is resolved dynamically, per request, from the domain's configuration; there is no per-provider route or Passport strategy registered up front. This page documents the mechanics shared by every provider. Provider-specific setup lives on its own page:

Repo: 5app/hub

Configuration

Federation settings live in the domainFederations table (api/db/models/domainFederations.js). The relevant column is type, either 'saml' or 'linkedin' — see each provider's page for the columns it actually uses.

Admins configure this per domain in Admin → Settings (frontend/src/admin/pages/settings/index.js, settings_sso_* translation keys), where each federation row gets its own options card: buttonSubtitle (the button label), domainAuthority, disableAutoCreateUser (invert of "auto-create users"), saveNameIdAsEmployeeId, saveNameIdAsEmail — plus, for type: 'saml' rows only, a "Download SP metadata" button linking to /auth/federation/{id}/metadata (generates SP metadata XML via generateServiceProviderMetadata, auth/routes/auth/federation/metadata/get.js). That metadata file can be uploaded directly into most IdP consoles instead of entering fields by hand.

The frontend login page (frontend/src/main/components/pages/auth/login.js) reads the resulting config.federatedLogins list and renders an SSO button per entry, linking to:

/auth/federation/{id}/login?RelayState=<base64-encoded state>

Routing (auth service)

auth/routes/index.js mounts auth/routes/federation.js at /auth/federation, wiring login, callback, logout, and metadata handlers (auth/routes/auth/federation/*). Each of these calls passport.authenticate('federation', ...).

Dynamic strategy resolution

'federation' is not a normal, statically-configured Passport strategy — it's a custom Strategy (auth/modules/federation/index.js) whose authenticate() method:

  1. Looks up the domainFederations row for the current subdomain (and optional :id path param).
  2. Reads type and builds the corresponding sub-strategy on the fly:
    • linkedinauth/modules/federation/linkedin.js
    • saml (default) → auth/modules/federation/saml.js
  3. Delegates authenticate() to that sub-strategy instance, wiring success / fail / error / redirect / pass through from the outer Passport context so the sub-strategy behaves as if it were registered directly.

SAML strategy (auth/modules/federation/saml.js)

This single module handles every SAML IdP — there is no per-vendor SAML code:

  • issuer defaults to req.hostname if not set in the DB — this is the SP Entity ID the IdP must be configured with, and it must match exactly (no protocol prefix).
  • The ACS (callback) URL is built as https://{hostname}/auth/federation/{id}/callback whenever legacy_callback_url is falsy, where {id} is the domainFederations row's own id. The logout URL follows the same pattern (.../auth/federation/{id}/logout).
  • disableRequestedAuthnContext: true, wantAuthnResponseSigned: false and wantAssertionsSigned: false are hardcoded — the Hub doesn't require the IdP to sign the top-level response or individual assertions, only the overall SAML response.
  • Incoming SAML attributes are passed through formatSAMLAttributeKeys(), which strips any URI/namespace prefix (splits on /, keeps the last segment) and lowercases every key except nameID.
  • profileObject.provider = 'saml' is set, distinguishing SAML logins from LinkedIn logins in users.meta.provider (see Federation provider signal for Helix below).

Shared verify function (auth/modules/auth.js)

Both SAML and LinkedIn logins funnel into the same auth.federation(req, meta, done) function:

  1. Resolves the domainFederations config for the subdomain (by federationId, or the legacy default legacy_callback_url row).
  2. Derives the user's email from meta.emailaddress / meta.email / (optionally) meta.nameID, in that fallback order — whatever attribute name an IdP sends must resolve to one of these after formatSAMLAttributeKeys lowercasing.
  3. Calls an internal getOrSetUser(...) helper, which:
    • Finds an existing user by name_id, then employeeid, then email — or creates a new one if none match (unless disableAutoCreateUser is set).
    • Sets users.federated = true and stores the full incoming meta payload verbatim in the users.meta JSON column — LinkedIn logins set provider: 'linkedin', SAML logins set provider: 'saml'.
    • Assigns any SSO-driven team memberships and reactivates a soft-deleted userDomains record if the user is rejoining.
  4. Loads the full "auth user" record and completes the Passport login via done(null, authUser), logging a SUCCESSFUL_LOGIN activity event.

Session persistence (serializeUser / deserializeUser, same module) is identical to local username/password login — Redis-backed via the shared modules/ session middleware.

legacy_callback_url rules

legacy_callback_url controls whether a domainFederations row is the fallback picked when a login URL has no explicit id in the path (_getService's query falls back to {legacy_callback_url: true} when no :id param is present).

  • If a domain has only one federation provider, that row can set legacy_callback_url: 1, giving it clean URLs without an id (.../auth/federation/callback, .../auth/federation/login, etc).
  • If a domain has multiple federation providers (e.g. LinkedIn and a SAML IdP, or two SAML IdPs), only one row may be 1 — every other row must be 0, so its ACS/logout/login URLs include its own domainFederations.id to disambiguate.
Only one legacy_callback_url: 1 row per domain

Setting more than one row to 1 for the same domain makes the "no id in the URL" lookup ambiguous. When adding a second (or third) federation provider to a domain that already has one, double check the existing row(s) and flip any that should no longer be 1 to 0 — and update their IdP-side callback/ACS URLs to include the correct id.

Federation provider signal for Helix

users.federated alone can't identify which provider a user logged in with — it's true for every federation provider. 5app/hub#17406 added a dedicated, reliable signal instead:

  1. Persisted marker: users.meta.provider, set on every federation login (see Shared verify function above) — 'linkedin' for LinkedIn, 'saml' for any SAML IdP (Google, Microsoft, or otherwise).
  2. API: GET /api/me/profile (api/routes/me/profile/get.js) reads users.meta and returns isLinkedinLogin: boolean in the response — true only when meta.provider === 'linkedin'. There is currently no equivalent per-SAML-vendor boolean (a Google login and a Microsoft login are indistinguishable from this API alone; both just report isLinkedinLogin: false).
  3. Frontend: useFetchUserProfile() exposes isLinkedinLogin on the UserProfile type (frontend/src/common/types/users.ts). useMindsetLocationContext.ts includes it (stringified, alongside hasMeetingAnalysis and the other boolean signals) in the context object passed to agentEl.setSituationalAwareness(...), so the Helix/Mindset agent widget can see whether the current user is logged in via LinkedIn.

Note: this signal reflects the user's most recent federation login, not necessarily their current session — users.meta is overwritten (not merged) on every federation login, and isn't touched by local password logins, so a user who has ever logged in via LinkedIn keeps isLinkedinLogin: true until they log in via SAML or another federated method.

Further Resources