To safeguard our users’ accounts and prevent fraud, we sometimes ask users to verify their identity or confirm a transaction by completing a “user friction” such as two-factor authentication. User frictions, or verification steps designed to prevent fraud, are essential in combating fraudulent activity but are not always easy to implement quickly. At DoorDash, we sometimes find our suite of applications being abused and attacked due to unforeseen loopholes. In such scenarios, introducing a friction feature to those workflows is effective in preventing further damage.
Our initial implementation of frictions focused only on the DoorDash consumer application. While that was effective in the short run, we soon realized the use cases expanded beyond our consumer application and we needed to design the frictions with modularity and scalability at their core. To implement a more long-term solution, we opted to build a common library for all our frictions. This helps us to easily integrate to different workflows and applications as we grow.
Why are frictions important and why a library for frictions?
User frictions are similar to checkpoints on a road and are assigned to users that our risk engine deems risky and effectively prevent fraud in applications before it even happens. Most frictions, such as multi-factor authentication (MFA) or 3-D Secure (3DS), collect and verify user details to ensure authenticity. The information we collect might be known or additional.
For example, we have a credit card re-entry friction that requires users to re-enter their saved card information at checkout when our risk engine detects a potentially fraudulent transaction. If there is an unauthorized account takeover, the fraudster (who does not have access to the original credit card) will not be able to proceed with a transaction.
As DoorDash rapidly grows, it has become increasingly hard for us to implement and integrate new frictions quickly enough in response to fraud attacks. We determined we needed a common library for all our frictions so that different applications could be integrated quickly. This approach lets us to:
- Introduce friction to different workflows and applications
- Increase ownership of the logic to render the frictions
- Avoid the risk of bugs or disparity with multiple implementations
- Have a common point of failure to debug and triage issues
- Prevent DRY (don’t repeat yourself) code
Creating a library for risk frictions
We created our web library component-risk
with interoperability and modularity in mind, to reduce the time needed to integrate risk frictions in any workflow. Some example of our frictions are:
- Multi-factor authentication using SMS/Email (MFA)
- Second card verification
- Card re-entry verification
- Phone number/Email verification
- 3DS
These frictions are used across different workflows, including login, checkout, and edit profile, in different DoorDash and Caviar applications--consumer, Dasher (our name for delivery driver), and merchant. Some of the workflows, such as login, are integrated web views by then also serving mobile workflows.
Stay Informed with Weekly Updates
Subscribe to our Engineering blog to get regular updates on all the coolest projects our team is working on
Please enter a valid email address.
Thank you for Subscribing!
Architecture of component-risk
Component-risk
is a TypeScript library using React.js. As depicted in Figure 1, most of DoorDash’s web React applications have a BFF (backend for frontend) service that is used to communicate with our backend microservices. The applications consume the library component-risk
and the library can be configured to leverage each application’s BFF for frictions. For all applications without a BFF we use old monolith DSJ or our in-house service called risk-bff
.
The tech stack for component-risk
is:
- React.js
- TypeScript
- Styled components
- Apollo GraphQL
We use React Contexts for state management. Opting for an in-built state management solution like React Contexts allows us to keep the code simple and limit the bundle size. At its core, component-risk
uses multiple contexts as described in Figure 2 to maintain separation of concern and manages state across components that serve different frictions,
- Risk context: Externally exposed context that manages the state of
component-risk
at the application level. This is where states such as apollo client, URL proxy are maintained. - Layout context: Internal context that manages the design elements of the frictions. This includes setting up the CTA behavior for the modal, controlling the dismissible nature of the modal, and so on.
- Verify account context: Internal context for frictions like MFA and phone/Email verify.
- Verify payment card context: Internal context for some of our card challenge frictions.
- ThreeDSecure context: Internal context for maintaining the state variables for the 3DS friction.
Roadblocks and considerations with building the library
Even though building a library seemed to be the solution for quickly implementing and rolling out frictions, it had its share of challenges with design and implementation. Most of these challenges were around supporting tech stack customizations by different teams and addressing and embracing the disparities that arise due to such customizations.
Supporting a fragmented backend
The web packages in DoorDash are maintained by every team individually and the team has the liberty to design them as they see fit. In addition to each team adopting different approaches, we also had a huge migration effort that saw our backend shift from a Django monolith to Kotlin-based microservices. All these together cause a disparity in the way each application communicates with the backend and was one of the primary roadblocks for our library. We needed to support the following:
- Applications using GraphQL-based BFF
- Applications that use REST-based services (includes our Doorstep Django - DSJ monolith)
- Applications that do not have a BFF and need to communicate with gRPC-based services
We had to use the same frontend code with the flexibility to interact with our varied backend infrastructure. After several considerations, we solved this problem by using the REST link from Apollo GraphQL. REST link allows us to call endpoints inside GQL queries to both GraphQL and a RESTful service and have all the data managed by Apollo Client. This is done by configuring Apollo to support multiple links such as:
new ApolloClient({
link: ApolloLink.from([
onError(({ networkError }) => {
if (networkError) {
console.log(`[Network error]: ${networkError}`)
}
}),
new RestLink({
headers,
uri: getBaseUrl(proxyOverride),
credentials: 'include',
fieldNameNormalizer: (key: string) => camelCase(key),
fieldNameDenormalizer: (key: string) => snakeCase(key),
}),
new HttpLink({
headers,
uri: getBffUrl(riskBffConfig),
credentials: 'include',
}),
]),
And then when constructing the GQL, we specify whether it needs to be the one for GraphQL-based service or the one for the RESTful service. This is done using the @rest directive.
1/ Mutation used by HttpLink:
gql`
mutation verifyPasscodeBFF($code: String!, $action: String!) {
verifyPasscode(code: $code, action: $action) {
id
}
}
`
2/ Mutation used by RestLink:
gql`
mutation verifyPasscodeRest($code: String!, $action: String!) {
verifyPasscode(input: { code: $code, action: $action })
@rest(method: "POST", path: "/v1/mfa/verify", type: "VerifyPasscode") {
id
}
}
`
Landing on a common design paradigm
Being a library of multiple frictions, we had use cases for supporting interchangeable frictions in a single workflow. For example, we have MFA, card re-entry, second card challenge, and 3DS all at checkout, and any user might get one or a combination of these based on their risk evaluations.
To achieve this, we had to decide upon a common design paradigm for all our frictions, which allows us to reuse a lot of the boilerplate code and just change the context of each one. We went with the approach of using a modal for all the frictions. This allows us to:
- Retain the state of the workflow since the friction is only a hard overlay
- Not take the user away from what they’re working on
- Keep the friction quick enough so the user experience isn’t hampered too much
Adapting to different themes
We have different theming requirements for different applications such as consumer, merchant, etc. We wanted our frictions to adopt the theming as can be seen in Figure 4, based on the application that’s rendering it. We achieved this by making all DLS (Design Language System) dependencies to be peer dependencies so that we could use the same theming context across the applications and the library.
Minimizing bundle size
One of the technical challenges with our library was to ensure that we kept our bundle sizes as small as possible. Since libraries do not have webpack bundling code splitting and tree-shaking wasn’t really an option, we opted to use as much native JavaScript as possible and to use external libraries that are commonly being used in DoorDash applications. For example, we used the same analytics.js loader and created a separate i18n instance from the application. This was achieved by defining the respective dependencies as a peer dependency.
Enabling local development
The goal of component-risk
is to allow seamless integration into our applications and improve developer velocity. When developing locally, it is important for component-risk
to follow the same environment as that of the application. It would be hard to develop with the application hitting production and component-risk
relying on staging. We got over this by leveraging the application’s webpack proxies and allowing the application to configure the library’s URL based on the proxies.
The future: Looking beyond the web
As part of the fraud team, we wanted to develop solutions that transcend our current infrastructure and make sure risk mitigation is baked into every life cycle of software development. Having a common library such as component-risk
helps us achieve it since ease of integration prevents risk mitigation from being an afterthought.
We have had some of DoorDash’s most critical workflows such as login, checkout protected using our library. So far we have almost 15 workflows across consumer, logistics, and merchant applications that use component-risk
in production for almost two years with no major breakdowns.
After creation of our library we have seen our development time reduce from months to weeks for most major initiatives. As an example, we were able to protect our identity workflows such as login, signup, and forgot password with MFA friction in a few months’ worth of effort. However, we still have some scope for improvements for our library and we are ambitious to extend its use cases. Here’s a list of compiled items for our library’s North Star:
- Consolidate all our backend code in a single BFF, which will let us own and develop
component-risk
as an end-to-end solution. - Integrate with our identity service to authenticate backend calls without having the respective application’s BFFs as a middle man.
- Split up each category of frictions into its own sub library to improve bundle size and also let our sub teams own the respective sub library.
- Most frictions are extremely simple in their user experience and don't necessarily require native solutions for mobile platforms. We see a huge opportunity in creating hybrid frictions that can also be used as webviews for iOS and Android platforms.
If you are interested in building solutions like these and in fighting fraud with us, consider joining our team!