This post covers the process we went through while developing Sym’s integration with Vanta, a compliance automation application that helps streamline audits for common security frameworks like SOC, HIPAA, and more. Specifically, it discusses the decisions we made around authentication and user credentials.
OAuth is a common authentication protocol that enables a third-party application to act on behalf of an individual user via that user’s identity from another application. Consciously or not, you’re probably using OAuth every day – any time you see a screen that asks you to “allow” an application to read data from another (say, using your Google identity on any other site), that’s probably OAuth at work.
Here at Sym we use OAuth to authenticate our users when they log into our CLI using `symflow login`. This allows us to grant the correct permissions to the correct user in the correct organization.
When building our integration between Sym and Vanta we wanted our users to be able to start their connection journey from the Vanta platform which would in turn direct the user to Sym's OAuth login flow. Vanta also uses a similar solution to authenticate users in their system as we do, so this required accounting for a double OAuth workflow for the user to authenticate within Vanta itself, and then within Sym's system – effectively granting identity credentials first to Vanta, and then to Sym to complete the integrated connection.
When thinking about this problem we considered three different competing goals.
So let’s start with the “why” – why do we need to do a double OAuth handshake at all?
Both Vanta and Sym have a concept of Users and Organizations.Users belong to Organizations and have specific roles in those Organizations, and in order to effectively connect the two accounts, we need to align those concepts – which means getting two rounds of context about a single user.
When a user begins to connect Sym to their Vanta account, we have access to Vanta's user context, meaning we know which Vanta user is requesting the action on behalf of which Vanta organization. We then need to bring that context into the Sym login, verify the Sym user has admin privileges, and then map the Vanta organization to the Sym organization. Once this is all confirmed and configured, we start sending our logs to Vanta in an hourly sync – the actual meat of the integration.
Our major constraint was that at the time of implementation we did not have a web UI for our product. If we had a web UI it would have been much simpler to direct the user to our already-built authentication flow after authenticating with Vanta to create a seamless user experience. At the time, however, all Sym auth ran through CLI – so that was the first of several options we debated.
Option one: Require OAuth to be triggered from the `symflow` CLI.
We could require the user to authenticate in our CLI before going to Vanta to connect the two accounts. On the one hand, we had precedence for this in our previous Slack install method; on the other, we wanted to move away from this as a general antipattern because bouncing between web and CLI requires context switching and imposes cognitive load on the user. While this would satisfy our requirements for security because the user is authenticated on both ends before making the connection, it failed our second requirement for usability: clicking the “Connect Sym” button in Vanta would need to redirect the user to documentation, which is at best, a medium-frustrating user experience.
Option two: Connect the accounts using Terraform
Sym uses Terraform to define all of our 3rd party integrations. Because of this, regardless of how we choose to implement the OAuth flow the user would need to define a Vanta terraform resource to begin sending data from Sym to Vanta. The existing pattern there made this an appealing option. The user would still be authenticated with Vanta and Sym separately, but the mapping would essentially be done manually. This would require the user to go into their Vanta account, pull out their Vanta account ID and add it to their `sym_integration` Terraform module. Technically, though, if you made a bad copypasta mistake you could accidentally send your Sym data to someone else’s Vanta account, provided that Vanta account has been connected to Sym previously. This option opened up too much of a security risk to be considered even though it felt lower friction to the user and would be easier for us to implement.
Option three: Connect the accounts as part of a CLI-authenticated OAuth flow.
This is a combination of some of the other options, where the user would generate a “one time install token” of sorts via symflow CLI that proves they are an admin of their particular Sym organization. They can input that install token in the Vanta webpage which would kick off the OAuth flow in the browser. In this case, the user can securely complete installation via the web, without us building true web authentication. However, it would mean that the user would need to go back and forth between the CLI and web to complete the installation This got close to most of our goals, but still asked a little too much back-and-forth from the implementing user.
Option four: Connect the accounts as part of an authenticated OAuth flow.
This is the option where we actually have to build a UI to redirect the user to. The user would be logged into their Vanta account where they would click a button to connect to Sym. This would route the user to a webpage that asked for their Sym organization ID – something we already require for our CLI login. The organization ID would direct to the newly built login page where we authenticate the user and verify that they are an admin for the specific organization that they requested. This is secure and keeps the user in one context. This did require us to build out an OAuth web login page which did prolong the project as a whole, but we can't complain about the end result: a simple user friendly authentication flow between Sym and Vanta
The next question we needed to ask ourselves was what do we do with the Vanta refresh token once the user has successfully authenticated.
Option one: Store the token in the customer’s infrastructure
We always prefer to store tokens on our customer’s infrastructure. This is how we handle all of our other integrations aside from Slack. That said, our usual practice of fetching a token from a customer’s Secrets Manager assumes that the secret is something static and available to the user in a UI, like an API token used for Basic Auth.
To replicate this workflow with an OAuth refresh token, we considered asking the user to define a secret with no value in their AWS account before going through the OAuth flow. In this case the user must also add permissions for Sym to update secrets with the /sym/ path. Then, when OAuth succeeds, Sym will save the secret to the customer’s pre-defined secret path. The pros of this approach are that the customer owns their own secret and Sym cannot create arbitrary new secrets. On the other hand, this requires more steps for the customer, more context switching, and more room for errors as OAuth will fail if any of these steps are missed or misapplied.
We of course could remove one step in the above option and just ask our customers to give us access to create and update any secrets with the path `/sym/`. This option removes one potential hurdle, but at the cost of requiring that the user give us even more permissions to their AWS secrets manager.
Option two: Store the token in our infrastructure
The only integration we currently have where we store the token in our infrastructure is our Slack integration – for all others we fetch from the customer’s secrets manager. On the plus side of this approach, if we store the token in our infrastructure we are guaranteed to have all the access we need, with no extra configuration by the customer, and nowhere for them to miss a step. It should Just Work™. On the other side we have to own the secret, which we generally try to avoid. That said, because this token would be capable only of reversible write operations, and would not be associated with conferring any elevated access to users, it felt (like Slack) like an acceptable compromise.
In the end we chose to implement the options that created the smoothest user experience, following our token storage pattern for Slack instead of our pattern for access integration. That did come with the tradeoff of a prolonged development cycle, but the end result is easily understood and maintainable.
These kinds of technical decisions are rarely clear cut and different teams will face this same problem and choose different options that best serve their goals. We may have even chosen a different path in the past, and would likely choose a different path in the future. Our hope is that walking you through how our team thought about this decision was helpful in guiding any conversations you may be having with your teams as you navigate similar problems that bridge technical requirements, security, and user experience.
If you are interested in reading more about our Vanta integration you can view our docs here.