We shipped SSO support in a day, how?
Last week, RunReveal shipped Single Sign-On support with the help of SSOReady.com! 🎉 It took us less than 8 hours from project kick-off to working in production, which is faster than I get a load of laundry done and folded much of the time.
While that's impressive for any change involving authentication, we realized some of the challenges that we imagine many companies encounter that can really gum up the works when attempting to add SSO to your app so we decided to share how we did it so that other folks may benefit when adding SSO to their enterprise applications.
Even if you federate the hard work of managing the SAML connection to a third party provider like SSOReady, WorkOS, Auth0, or another you still have the task of figuring out how to integrate the authentication mechanism to your user data model in your application. This is often harder than it seems. If you're dealing with well established applications having complicated data models, good luck!
Here are some questions we asked ourselves when getting this started:
- How do we associate a user with a workspace (or other resources) when they first sign up?
- Do we need automatic association or can we rely on workspace invites?
- When implementing SSO for a domain, do we prevent logging in with OAuth for users in that domain?
- How do we productize this and onboard our customers?
Adding SSO to our existing data model
RunReveal's website data-model has Users, Organizations, and Workspaces. All workspaces belong to exactly one organization, and users can be a member of any number of workspaces via the workspace members table, which maintains a mapping between users and workspaces. Resources like sources, destinations, notifications, etc all belong to exactly one workspace.
When a user first creates an account we allocate them a new workspace and organization, and all resources they create belong to workspace. This data model has a few major benefits:
- Users can work together easily and create multiple workspaces.
- Collections of workspaces can manage configuration centrally at the Organization level
- All resources have a clear owner (the workspace), which simplifies enforcing permissions across the control plane and the data plane.
We needed to extend this data model to enforce SSO for a customer's domain even if they have users with multiple workspaces in RunReveal. To do this, we added nullable SSO configuration columns to the organizations table.
BEGIN;
ALTER TABLE organizations ADD COLUMN ssoready_org_id TEXT;
ALTER TABLE organizations ADD COLUMN domain TEXT;
COMMIT;
The migration was incredibly simple. It's worth noting that both domain
and ssoready_org_id
are nullable, so if they're not present then the org isn't requiring that users belonging to their domain log in with SSO, and if they are then SSO is required across all organizations and all workspaces for the claimed domain.
After adding those columns and the corresponding database function, we can then check to see if any given user has an email which has been registered in RunReveal as requiring Single Sign On by a domain administrator upon login using the following HTTP Handler function:
func (s *Server) BrowserSSOLogin(w http.ResponseWriter, r *http.Request) {
// ... Form parsing to get email domain omitted for brevity
org, err := s.db.GetSSOConfig(r.Context(), domain)
if err != nil {
HandleErrUIRedirect(w, r, err, "/login")
return
}
var response struct {
RedirectURL string `json:"redirectUrl"`
}
// Lookup SAML Authorization endpoint
err = requests.
URL("https://api.ssoready.com/v1/saml/redirect").
Header("Content-Type", "application/json").
Bearer(ssoReadyKey).BodyJSON(map[string]string{
"organizationId": org.SSOReadyOrgID,
}).ToJSON(&response).Fetch(r.Context())
if err != nil {
HandleErrUIRedirect(w, r, err, "/login")
return
}
http.Redirect(w, r, response.RedirectURL, http.StatusFound)
}
The above code delegates the rest of the SSO workflow to the Identity Provider (IDP) and SSOReady. After they're done with their exchange, the user's browser gets redirected back to the SSO completion route on our application which is handled by the code below to complete the handshake with a token exchange between RunReveal and SSOReady.
func (s *Server) BrowserSSOCallback(w http.ResponseWriter, r *http.Request) {
accessCode := r.URL.Query().Get("saml_access_code")
var resp samlRedeemResponse
err := requests.
URL("https://api.ssoready.com/v1/saml/redeem").
Method("POST").
Header("Content-Type", "application/json").
Bearer(ssoReadyKey).
BodyJSON(map[string]string{
"samlAccessCode": accessCode,
}).ToJSON(&resp).Fetch(r.Context())
if err != nil {
HandleErrUIRedirect(w, r, err, s.mktBaseURL+"/login")
return
}
// ... User creation, login and session management code omitted for brevity
}
And that's it! We have a few lines of code in the OAuth code paths to prevent users of a domain from logging in using the other methods, but that's really all. Preventing users having emails belonging to the domain from logging in using other methods was the number one reason why administrators wanted SSO support. There's no explicit mapping between a user and the organization, but the organization is still enforcing the login method for the user.
Onboarding Customers
To onboard customers to SSO, we send them a single-use SSOReady link where they can fill out some forms with their SAML endpoints and to share the URLs that the IDP needs for the authentication flow.
The way we architected our SSO alongside our existing data model means that regardless of workspace or organization, SSO will be enforced whenever someone from an SSO configured domain logs in. This decision is a trade-off. Making SSO fully self-service is a nice feature, but it also means that SSO must be enforced at a per-workspace level and not across the entire platform. There's nothing stopping a malicious actor from setting up their own IDP and impersonating ownership of a domain, so the way SSO fits into your product needs to account for this.
What's next
RunReveal understands the risks and difficulties associated with building secure products, and we want to be our customer's long term partner for detection and security data. SSO is just one small step towards that goal and we have a lot of exciting plans for the near future. This project came together so quickly because our data-model was designed to be added to, we knew the pros and cons of the different design decisions we had to make, and we worked closely with our friends at SSO Ready.
Over the next few weeks you will see more from content on our blog, and we'll be attending SINET New York in October. If you'd like to learn more about the product you can reach out and set up a demo, or give the product a try yourself.