Skip to main content

LinkedIn Login

LinkedIn Login lets a domain's users sign in to the Hub with their LinkedIn account instead of (or alongside) a Hub password. It is one branch of a shared federation framework that also drives SAML SSO — there is no LinkedIn-specific route or Passport strategy registered up front; the correct provider is resolved dynamically, per request, from the domain's configuration.

Supported Features

  • OAuth2 / OpenID Connect sign-in: uses LinkedIn's openid profile email scopes via passport-linkedin-oauth2.
  • Per-domain configuration: each domain can have its own LinkedIn app credentials (clientID / clientSecret), configured by an admin in Admin → Settings.
  • Find-or-create user matching: on first login, a user is created (or matched to an existing account) using the LinkedIn-provided email address; existing users are updated and reactivated if previously soft-deleted.
  • State passthrough: an incoming RelayState query parameter (the same mechanism used for SAML) is forwarded as the OAuth2 state, so post-login redirects work the same way for both providers.

Implementation Details

Repo: 5app/hub

Configuration

Federation settings live in the domainFederations table (api/db/models/domainFederations.js). The relevant column is type, which is either 'saml' or 'linkedin'. For LinkedIn, clientID and clientSecret hold the LinkedIn app credentials; the SAML-only columns (entryPoint, cert, etc.) are unused.

Admins configure this per domain in Admin → Settings (frontend/src/admin/pages/settings/index.js, settings_sso_* translation keys). 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.

LinkedIn strategy (auth/modules/federation/linkedin.js)

  • Overrides LinkedInStrategy.prototype.userProfile to call LinkedIn's OpenID Connect GET /v2/userinfo endpoint directly (rather than the library's default profile endpoint), mapping sub / given_name / family_name / name / email into a Passport-shaped profile (provider: 'linkedin', id, name, displayName, emails).
  • Overrides authenticate to read req.query.RelayState and forward it as the OAuth2 state param, matching the SAML RelayState convention used elsewhere in the login flow.
  • Requests scopes openid profile email.
  • On a successful callback, its verify function builds:
    {
    nameID: profile.id,
    first_name: profile.name.givenName,
    last_name: profile.name.familyName,
    email: profile.emails.pop().value,
    federationId,
    provider: 'linkedin',
    }
    and hands this off to the shared verify function below. The provider: 'linkedin' marker is what makes LinkedIn logins distinguishable from SAML ones once persisted (see LinkedIn login signal for Helix).

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.
  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.

LinkedIn login signal for Helix

users.federated alone can't identify the provider — it's true for both SAML and LinkedIn logins. 5app/hub#17406 added a dedicated, reliable signal instead:

  1. Persisted marker: users.meta.provider === 'linkedin', set on every LinkedIn login (see Shared verify function above). SAML logins set provider: 'saml' instead, so the two are always distinguishable.
  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'.
  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.

Activating for a hub

The 5app LinkedIn app for SSO is located at LinkedIn Developers app.

To activate LinkedIn login for a hub, add a new entry to the domainFederations DB table:

domain_id: {hub domain id}
identity: linkedin
type: linkedin
config: {"preferred": true, "buttonSubtitle": "LinkedIn", "domainAuthority": ""}
clientID: {LinkedIn app client id}
clientSecret: {LinkedIn app client secret}
signatureAlgorithm: sha256
is_deleted: 0
legacy_callback_url: 1

NB. Currently you must set legacy_callback_url to 0 for any other entries in domainFederations with the same domain_id.

Further Resources