Skip to content

SSO (Single Sign-On)

SSO allows advisors to log into RightCapital from Vendor systems (or vice versa) without separate authentication. This improves user experience and is often bundled with data integrations.

Code Location: gitlab.rightcapital.io/web-service/api/-/tree/develop/app/Http/Controllers/Sso and app/Sso

The system that authenticates the user and issues a signed identity assertion. Holds the user account, password/MFA, and attributes.

The system the user wants to access. Does not store credentials — it trusts an assertion from the IdP and creates a local session.

The most common protocol for enterprise SSO. The IdP sends a signed XML assertion containing user identity and attributes to the SP’s ACS (Assertion Consumer Service) endpoint.

RightCapital plays both roles depending on direction, but the workload is asymmetric.

DirectionOur roleCounterpartyFrequency
Inbound (vendor → RC)SPVendor / Organization IdPHigh — primary advisor entry path
Outbound (RC → vendor)IdPVendor SPLow — only one SAML target today (Wealthbox)

The code structure mirrors this split. Both abstractions live in app/Sso/Protocols/Saml2/:

FileRoleKey methods
ServiceProvider.phpSPacs(), spMetadata()
IdentityProvider.phpIdPoutbound(), idpMetadata(), generateAssertion()

Underlying library: lightsaml/lightsaml (PHP).

This covers the vast majority of SSO traffic. There are three sub-flavors.

Advisor starts at vendor, clicks a link to RightCapital:

sequenceDiagram
    participant A as Advisor
    participant V as Vendor (IdP)
    participant RC as RightCapital (SP)

    A->>V: Login to Vendor
    A->>V: Click "Open in RightCapital"
    V->>V: Generate signed SAML assertion
    V->>RC: POST SAML assertion to /acs
    RC->>RC: Validate signature & assertion
    RC->>RC: Look up advisor by NameID
    RC->>RC: Create session
    RC->>A: Redirect to application

14 vendor IdPs are supported (see SsoBindingProvider enum):

Commonwealth, Orion, Advyzon, BlackDiamond, A360 (MML/GWN), MassMutual, LPL, Pershing (Net/Wealth), PWC, AssetMark, XYPN, SEI

Each vendor has a directory under app/Sso/Providers/<Vendor>/Saml2/ServiceProvider.php extending the abstract SP.

Advisor starts at RightCapital and is redirected to the vendor IdP to authenticate:

sequenceDiagram
    participant A as Advisor
    participant RC as RightCapital (SP)
    participant V as Vendor (IdP)

    A->>RC: Click "Login with Vendor"
    RC->>V: Redirect with AuthnRequest
    A->>V: Authenticate
    V->>RC: POST signed SAML response to /acs
    RC->>RC: Validate & create session
    RC->>A: Redirect to application

Advisor firms (organizations) want their staff to SSO into RightCapital using their corporate IdP (Okta, Azure AD, ADFS, etc.). Federation SSO is the same pattern at a multi-organization level.

Distinction:

  • Vendor SSO → vendor (Schwab, Pershing, etc.) is the IdP
  • Organization SSO → the advisor’s employer is the IdP

SSO with extra context (e.g., a household identifier in the SAML attributes). RC opens a specific household automatically. Combines authentication with deep-linking.

When receiving a SAML assertion, we validate:

FieldValidation
SignatureVerify XML signature using IdP’s certificate
IssuerMust match expected IdP entity ID
AudienceMust match our SP entity ID
NotBefore / NotOnOrAfterAssertion must be within valid time window
Subject (NameID)Contains the user identifier (email, advisor ID)
ConditionsCheck any additional conditions

A much smaller surface area, governed by the OutboundProvider enum:

ProviderProtocolNotes
WealthboxSAML 2.0Only SAML outbound target today
RedtailProprietaryNot SAML
SEISupports both inbound and outbound
sequenceDiagram
    participant A as Advisor
    participant RC as RightCapital (IdP)
    participant W as Wealthbox (SP)

    A->>RC: Already logged in to RC
    A->>RC: Click "Open in Wealthbox"
    RC->>RC: Generate Assertion (NameID = encrypted user.id)
    RC->>RC: Sign with our private key
    RC->>A: Return {url, method:POST, body:{SAMLResponse}}
    A->>W: Browser auto-POSTs SAMLResponse to Wealthbox ACS
    W->>W: Validate signature against our published cert
    W->>A: Logged in

Concrete implementation: app/Sso/Providers/Wealthbox/Saml2/IdentityProvider.php extending the abstract IdentityProvider.

Key behaviors from Protocols/Saml2/IdentityProvider.php:

  • NameID: bin2hex(Crypt::encryptId($user->id)) with format urn:oasis:names:tc:SAML:2.0:nameid-format:persistent — opaque, persistent, encrypted
  • Validity window: NotBefore = now, NotOnOrAfter = now + 5 minutes
  • Signing key: secrets/<provider>_saml.key
  • Signing cert: certs/sso/idp/<provider>/signing/saml.crt
  • Cert rotation: optional saml_rotation.crt enables zero-downtime rotation — both certs are published in IdP metadata until consumers pick up the new one
  • Metadata endpoint: served via idpMetadata(), valid until the latest cert’s validTo
  • Pre-flight checks: OutboundPremiseCheckers blocks impersonation and requires the integration to be set up
  • Audit trail: every outbound assertion is written to a file log under sso/<level>/outbound/<provider>/<user_id>/

Because we sign the assertion, our infrastructure is the failure point — not the vendor’s:

  • Cert expiry breaks all Wealthbox outbound SSO. Mitigated by NotifyExpiringCerts, which alerts on expiring certs.
  • Private key leak would let an attacker impersonate any RC user to Wealthbox. Keys live in secrets/ (deployment-time secret injection).
  • Clock drift on our servers makes assertions invalid (5-minute window).
  1. Create app/Sso/Providers/<Vendor>/Saml2/ServiceProvider.php extending the abstract SP
  2. Add the provider case to SsoBindingProvider
  3. Configure entity ID, ACS URL, and the vendor’s signing cert
  4. Implement attribute → advisor lookup (NameID format depends on vendor)
  5. Add controller test under tests/Controllers/AsGuest/Sso/Inbound/<Vendor>ControllerTest.php
  6. Verify with the vendor’s QA environment before enabling in production
  1. Add the provider case to OutboundProvider
  2. Create app/Sso/Providers/<Vendor>/Saml2/IdentityProvider.php extending the abstract IdP
  3. Generate a signing keypair, place key in secrets/, cert in certs/sso/idp/<provider>/signing/
  4. Add sso.<provider>.outbound config (entityId, destination, IdP entityId, optional SSO URL)
  5. Publish IdP metadata URL to the vendor; receive their SP metadata
  6. Test in staging — verify signature, NameID format, audience restriction
  7. Add the new cert to the rotation alerting so expiry doesn’t take it down
  • Check if advisor has the integration set up (binding row exists)
  • Verify NameID in SAML matches our records
  • Check for email vs ID mismatch
  • Inbound: vendor’s cert may have rotated — update our copy
  • Outbound: our cert may have rotated without vendor picking up new metadata
  • Check for clock skew between servers
  • Check NTP / clock sync on our servers
  • For outbound, the 5-minute window is tight — slow client redirects can blow it
  • Check that secrets/wealthbox_saml.key and the signing cert are present in the deployed image
  • Inspect the file log under sso/<level>/outbound/wealthbox/<user_id>/ for the actual generated XML

See the full vendor list in Notion: SSO Providers