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:
- Looks up the
domainFederationsrow for the current subdomain (and optional:idpath param). - Reads
typeand builds the corresponding sub-strategy on the fly:linkedin→auth/modules/federation/linkedin.jssaml(default) →auth/modules/federation/saml.js
- Delegates
authenticate()to that sub-strategy instance, wiringsuccess/fail/error/redirect/passthrough 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:
issuerdefaults toreq.hostnameif 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}/callbackwheneverlegacy_callback_urlis falsy, where{id}is thedomainFederationsrow's ownid. The logout URL follows the same pattern (.../auth/federation/{id}/logout). disableRequestedAuthnContext: true,wantAuthnResponseSigned: falseandwantAssertionsSigned: falseare 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 exceptnameID. profileObject.provider = 'saml'is set, distinguishing SAML logins from LinkedIn logins inusers.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:
- Resolves the
domainFederationsconfig for the subdomain (byfederationId, or the legacy defaultlegacy_callback_urlrow). - 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 afterformatSAMLAttributeKeyslowercasing. - Calls an internal
getOrSetUser(...)helper, which:- Finds an existing user by
name_id, thenemployeeid, thenemail— or creates a new one if none match (unlessdisableAutoCreateUseris set). - Sets
users.federated = trueand stores the full incomingmetapayload verbatim in theusers.metaJSON column — LinkedIn logins setprovider: 'linkedin', SAML logins setprovider: 'saml'. - Assigns any SSO-driven team memberships and reactivates a soft-deleted
userDomainsrecord if the user is rejoining.
- Finds an existing user by
- Loads the full "auth user" record and completes the Passport login via
done(null, authUser), logging aSUCCESSFUL_LOGINactivity 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 be0, so its ACS/logout/login URLs include its owndomainFederations.idto disambiguate.
legacy_callback_url: 1 row per domainSetting 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:
- 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). - API:
GET /api/me/profile(api/routes/me/profile/get.js) readsusers.metaand returnsisLinkedinLogin: booleanin the response —trueonly whenmeta.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 reportisLinkedinLogin: false). - Frontend:
useFetchUserProfile()exposesisLinkedinLoginon theUserProfiletype (frontend/src/common/types/users.ts).useMindsetLocationContext.tsincludes it (stringified, alongsidehasMeetingAnalysisand the other boolean signals) in the context object passed toagentEl.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
- 5app/hub#17406 — added the
isLinkedinLoginsignal for Helix. - @node-saml/passport-saml — the underlying SAML Passport strategy library.
- passport-linkedin-oauth2 — the underlying LinkedIn Passport strategy library.