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
Key Concepts
Section titled “Key Concepts”Identity Provider (IdP)
Section titled “Identity Provider (IdP)”The system that authenticates the user and issues a signed identity assertion. Holds the user account, password/MFA, and attributes.
Service Provider (SP)
Section titled “Service Provider (SP)”The system the user wants to access. Does not store credentials — it trusts an assertion from the IdP and creates a local session.
SAML 2.0
Section titled “SAML 2.0”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’s Roles
Section titled “RightCapital’s Roles”RightCapital plays both roles depending on direction, but the workload is asymmetric.
| Direction | Our role | Counterparty | Frequency |
|---|---|---|---|
| Inbound (vendor → RC) | SP | Vendor / Organization IdP | High — primary advisor entry path |
| Outbound (RC → vendor) | IdP | Vendor SP | Low — only one SAML target today (Wealthbox) |
The code structure mirrors this split. Both abstractions live in app/Sso/Protocols/Saml2/:
| File | Role | Key methods |
|---|---|---|
ServiceProvider.php | SP | acs(), spMetadata() |
IdentityProvider.php | IdP | outbound(), idpMetadata(), generateAssertion() |
Underlying library: lightsaml/lightsaml (PHP).
Inbound SSO — RightCapital as SP
Section titled “Inbound SSO — RightCapital as SP”This covers the vast majority of SSO traffic. There are three sub-flavors.
Vendor IdP-initiated SSO (most common)
Section titled “Vendor IdP-initiated SSO (most common)”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.
SP-initiated SSO
Section titled “SP-initiated SSO”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
Organization / Federation SSO
Section titled “Organization / Federation SSO”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.
- Controller:
OrganizationSamlController,FederationSamlController - Routes:
organizations.sso.saml2.{metadata,login,acs},federations.sso.saml2.{metadata,login,acs} - Setup: exchange metadata XML with the org’s IdP, then enable via
ConfigureOrganizationSsoSettings
Distinction:
- Vendor SSO → vendor (Schwab, Pershing, etc.) is the IdP
- Organization SSO → the advisor’s employer is the IdP
Contextual SSO
Section titled “Contextual SSO”SSO with extra context (e.g., a household identifier in the SAML attributes). RC opens a specific household automatically. Combines authentication with deep-linking.
SAML response validation (SP side)
Section titled “SAML response validation (SP side)”When receiving a SAML assertion, we validate:
| Field | Validation |
|---|---|
| Signature | Verify XML signature using IdP’s certificate |
| Issuer | Must match expected IdP entity ID |
| Audience | Must match our SP entity ID |
| NotBefore / NotOnOrAfter | Assertion must be within valid time window |
| Subject (NameID) | Contains the user identifier (email, advisor ID) |
| Conditions | Check any additional conditions |
Outbound SSO — RightCapital as IdP
Section titled “Outbound SSO — RightCapital as IdP”A much smaller surface area, governed by the OutboundProvider enum:
| Provider | Protocol | Notes |
|---|---|---|
| Wealthbox | SAML 2.0 | Only SAML outbound target today |
| Redtail | Proprietary | Not SAML |
| SEI | — | Supports both inbound and outbound |
Wealthbox flow (RC as IdP)
Section titled “Wealthbox flow (RC as IdP)”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
Implementation details
Section titled “Implementation details”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 formaturn: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.crtenables 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’svalidTo - Pre-flight checks:
OutboundPremiseCheckersblocks 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>/
Operational risks (IdP side)
Section titled “Operational risks (IdP side)”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).
Adding a new SSO integration
Section titled “Adding a new SSO integration”New inbound vendor (RC as SP)
Section titled “New inbound vendor (RC as SP)”- Create
app/Sso/Providers/<Vendor>/Saml2/ServiceProvider.phpextending the abstract SP - Add the provider case to
SsoBindingProvider - Configure entity ID, ACS URL, and the vendor’s signing cert
- Implement attribute → advisor lookup (NameID format depends on vendor)
- Add controller test under
tests/Controllers/AsGuest/Sso/Inbound/<Vendor>ControllerTest.php - Verify with the vendor’s QA environment before enabling in production
New outbound vendor (RC as IdP)
Section titled “New outbound vendor (RC as IdP)”- Add the provider case to
OutboundProvider - Create
app/Sso/Providers/<Vendor>/Saml2/IdentityProvider.phpextending the abstract IdP - Generate a signing keypair, place key in
secrets/, cert incerts/sso/idp/<provider>/signing/ - Add
sso.<provider>.outboundconfig (entityId, destination, IdP entityId, optional SSO URL) - Publish IdP metadata URL to the vendor; receive their SP metadata
- Test in staging — verify signature, NameID format, audience restriction
- Add the new cert to the rotation alerting so expiry doesn’t take it down
Troubleshooting
Section titled “Troubleshooting”SSO failed — user not found
Section titled “SSO failed — user not found”- Check if advisor has the integration set up (binding row exists)
- Verify NameID in SAML matches our records
- Check for email vs ID mismatch
SSO failed — invalid signature
Section titled “SSO failed — invalid signature”- 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
SSO failed — expired assertion
Section titled “SSO failed — expired assertion”- Check NTP / clock sync on our servers
- For outbound, the 5-minute window is tight — slow client redirects can blow it
Outbound to Wealthbox returns 500
Section titled “Outbound to Wealthbox returns 500”- Check that
secrets/wealthbox_saml.keyand 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
SSO Providers reference
Section titled “SSO Providers reference”See the full vendor list in Notion: SSO Providers
Related
Section titled “Related”- Architecture — overall system design
- API Integrations — often paired with SSO