Uber has operations in over 10,000 cities worldwide and its services include ridesharing, food delivery, package delivery, couriers, freight transportation, electric bicycle and motorized scooter rental, and ferry transport.
Every year we have millions of users going through signup and login on our various apps. Over the years we’ve built independent signup and login experiences for each of our lines of business which allowed us to innovate and move a lot quicker. However, as we scaled and added additional lines of business, our experiences began to diverge leading to some of these inconsistencies being amplified. USL (unified signup and login) enables the vision of “One Uber Identity” through a unified signup and login experience across all Uber apps.
Background and Existing Challenges
Screenshots below depict the growing inconsistencies in the signup and login experience for users over time, which led to difficulties in maintaining them from a product and engineering perspective.
Here are some of the challenges faced by our users and internal stakeholders:
- Inconsistency: Vast differences between signup and login experiences across Uber apps lead to unnecessary cognitive overhead for users interacting with multiple Uber apps, which in turns causes frustration and drop-offs. These inconsistencies also resulted in forcing users to create duplicate accounts. For example, in the past, Rides allowed users to create their account with social identities, however the login experience for Uber Eats didn’t allow users to log in using a social identity. Such incompatibilities between onboarding experiences lead to millions of users being blocked during login, or forced to create duplicate accounts, leading to a high volume of support tickets.
- Duplication of effort: Each team owning the signup and login experience for their product ended up spending a large amount of engineering, product, and data science bandwidth building, maintaining, and optimizing their stack on an ongoing basis. Most growth features which help enable frictionless onboarding experiences are common across Uber apps, yet teams are forced to duplicate these features for their apps.
- Slow rollout of common security policies and fixes: At times we need to roll out certain security patches and updates to security policies (e.g., minimum password length) related to sign up and login across all Uber apps. Native implementations require separate rollouts for each of the mobile apps, leading to long rollout cycles that require large coordination efforts.
- High engineering complexity and maintenance overhead: Separate signup and login experiences across various apps resulted in thousands of flows that engineering teams need to maintain, monitor, and test on an ongoing basis. Ensuring that any changes don’t cause regressions in any of these flows is very difficult due to the sheer number of flows.
The Solution (USL)
As mentioned earlier, the USL (unified signup and login) project was launched with a vision to provide a unified signup and login experience for all Uber apps. Our framework is designed with several goals in mind:
- Maximize support for all clients: We want USL to support all signup and login use-cases at Uber. To maximize support, we made the decision to go web-based, so that USL would be supported by all clients that can open a web browser.
- Easy to maintain and developer velocity: Going web-based also ensures we can roll out any changes to flows or screens almost instantly to all clients. Only a subset of features which need mobile code changes (e.g., the native SMS auto read feature) would need a mobile app version upgrade by users.
- Good performance: Uber operates globally, which includes areas with sub-optimal networks. Users with poor network conditions and slow phones should be able to quickly load the signup and login experience. We’re aware that as compared to a web based solution a native experience would be faster, so we’re constantly looking at ways to close this performance gap to the point where it’s unnoticeable to our customers.
- Increase growth and security across the platform: In the past, whenever we wanted to push security or growth features, 15+ product teams had to implement the necessary changes to close these opportunities. This wasn’t a scalable approach and required a huge amount of time and effort coordinating with these teams. With USL, we simply build and deploy once to push security patches and growth features to all apps in a fraction of the time.
Using a web-based signup and login experience that is embedded by mobile apps and doubles down as the desktop login app, allowed us to achieve all these goals. We maximized support for all clients by making it extremely easy to integrate with. Being web-based, any security update or growth feature was instantly deployed to all users and didn’t require any mobile app upgrades, adding to developer velocity.
Many products have separate entry points for login and signup. This forces a user to switch between the two if they don’t recollect whether they have an existing account or not. With USL we have combined signup and login flows in order to avoid forcing users to switch between them. As the first step, the user provides a unique identifier for their Uber account which may be an email, phone number, or social login identifier. Based on this account identifier, we determine whether the user already has an Uber account or not, and guide them to the login or signup flow accordingly. This also removes the burden from the user to remember which identifier they used to register with Uber.
We also have inbuilt deduplication and linking flows. Let’s take for example a user who signed up by providing a mobile number, email, and password. If the same user tries to login again using Facebook, we look up their Uber account using email and phone number provided by Facebook and route the user to the login flow. Once the user is logged in, we seamlessly link Facebook to their Uber account, so next time they’ll be able to do single step login using Facebook, which also prevents creating duplicate accounts.
Most of the new features are rolled out as experiments in a staggered way. We keep a close eye on primary (signup/login rate) and secondary (first trip/order rate, total trip/order rate, total support tickets, etc.) metrics during any rollout. A typical rollout plan starts with 2% (engineering validation), which moves to 20%, 50%, and 100%, depending on how the metrics are faring.
We are not just in many cities around the world. Uber also has a diverse customer base including drivers, couriers, consumers, riders, restaurants, restaurant chains, etc. As we strive to improve our customers’ ability to sign up and log in, our platform evolves to support diverse customer preferences while keeping our codebase clean and maintainable. Additionally, we are also mindful of not overwhelming the customers by keeping our experiences clean and highly optimized.
USL’s web application is embedded within mobile applications and opened using specific platform technologies on iOS and Android. The user then goes through the web flow using the backend API and is finally logged into the app. Lets deep dive into the client (mobile and web) and server side aspects.
Our Android clients open USL using a trusted web activity. Trusted web activities (TWAs) extend Chrome custom tabs (CCTs), which help make transitions between native and web content more seamless without having to resort to a WebView. Apart from sharing the benefits of using CCTs (like sharing of cookies, ability to leverage latest web APIs, etc.), TWAs open the browser tab without any browser UI and are recommended for mobile apps that want to show web apps in fullscreen. On Android clients that don’t have the Chrome browser installed we fall back to loading USL in a WebView.
Our iOS clients open USL in a WKWebView, which is a platform-native view that you use to incorporate web content seamlessly into your app’s UI.
USL itself is a web-based single-page application built using FusionJS: a plugin-based web framework for building universal React applications. We’ll assume the reader is aware of some of the FusionJS concepts and won’t deep dive into the framework in this post (please refer to this blog post for that).
When the user opens signup and login, a HTTP request is received by the fusion-core (i.e., part of the FusionJS code running on the Node server), which processes the request by running it through each middleware in the plugin middleware chain. FusionJS’ middleware is built on top of Koa.js middleware and utilizes its downstream and upstream abstraction.
We have a bunch of server-side plugins that run on our FusionJS middleware:
- Enabling Redux: FusionJS provides us with a plugin to integrate with Redux, which we use extensively for state management
- Enabling single sign-on for our apps: We have middleware to check if the user is already logged into any of the Uber sites and log them in directly
- Customized flows: USL supports some customized flows that are implemented using plugins
- Getting dynamic property values and experiment values: We have a bunch of plugins related to getting (dynamic) property values and experiment configurations, which are run when any request hits our servers
After running through all the plugins, the HTML content is generated and sent down to the browser. The JS and CSS is loaded from a CDN and the React Application is hydrated in the browser.
USL has a bunch of code that need not be included in the initial page load. For example, there are some screen components that are rarely seen by users, which can be lazily loaded. We use JS bundle splitting to make sure we download a fraction of the entire JS code on the initial page load. Since the app is used in various low-bandwidth areas, it is critical that the web app loads in a reasonable time on slower networks and older devices, and bundle splitting is critical in helping us meet this goal.
All of the logic related to flows (i.e., which screens/factors the users should see) resides on the backend. The client application is responsible for just the presentation of the current state. It contains a main <Controller /> component, which reads from the Redux state and loads the current screen component on the UI. Here is the step-wise process of how our backend API processes requests, from receipt through updated UI in response:
- The (un-authenticated) user opens an Uber app, which in-turn will open USL in a browser
- The request goes to the USL node server, which runs through all the plugins and sends back rendered HTML to the browser
- The user waits for the client application to load in the browser
- The user enters details on the screen and submits the form, which sends a request to our backend API
- The backend API response updates the Redux store
- The Controller component (which subscribes to updates in the Redux store) in turn mounts the component for the next screen
As we were building custom flows for each Uber app using native flows, this meant we could never deprecate an old flow. The backend systems became very complex and slowed us down when developing new features. As USL is on the web, we do not have to maintain backward compatibility for any old flow, which reduces a lot of complexity on the backend. The number of nodes in the graph were reduced by 75% and the number of transitions by 85%. We are now able to quickly ship new growth and security features across all the apps.
Because of complex product requirements that are also regionally optimized, we have to support 100+ signup and login flows, which are controlled by the backend. On the backend, a user’s login and signup journey is represented as a graph, which is traversed by a state machine. A node in the graph is the screen the user sees. A node can offer one or more challenges to the user. The edges of the graph are functions which return a boolean value.
Based on the user’s input for a given challenge, the challenge is validated. As a result, the rules get executed and a decision is taken to determine the user’s next node (screen).
A challenge is a reusable, Lego™-like building block that can be placed at different nodes. It is an interface with methods ShowChallenge, which returns the given challenge, and HandleAnswer, which validates the answer for the challenge.
Below is a sample config. Here the user starts by entering their phone number, and the backend throws a VerifyPassword challenge. If the user enters an incorrect password, the state does not move and the user is prompted for their password again. Once the user responds with the correct password, the rules are executed that determine if the user needs to do a two-factor authentication or not. If the user did not need a 2FA, they’re logged in. Otherwise, the user is prompted for a 2FA challenge.
Below is the high-level architecture of the signup and login service:
Let’s walk through the architecture using an example of a login flow. Once the user enters a phone number, the presentation layer will convert that to an internal request and forward the request to the state machine. As this is the first request, the state machine creates a new session and then executes the challenge—in this case it is the Identifier challenge. If there’s an error (say the phone number is invalid), an error is returned back. State machine uses the config graph to continue the user journey graph traversal and gets back the next user state (screen). The next challenge is returned to the user.
In this way, a user keeps solving challenges and the backend continues to present the next challenge to the user until the user solves sufficient challenges to be authenticated or provides enough information for a signup. In the final request, backend returns the auth tokens back to the user.
The 100+ flows on USL are monitored in various dimensions such as the mobile app, website, device OS, etc. This exponentially increases the number of combinations of flows we need to monitor. We’ve built a custom tool that generates these alerts (in the hundreds). It internally uses Uber’s anomaly detection tool to automatically determine the thresholds based on historical data. We have integration tests for all of the flows and blackbox tests for the critical flows, which give us the confidence to deploy backend/web code multiple times a day.
Over the last two years, the team has been hard at work in rolling out USL across Uber Rides, Uber Eats, and Uber Driver. Currently more than 78% of traffic has adopted USL, and given we started at 0% just 1 year ago, this is a huge milestone. Here is a quick time-lapse of USL adoption across the world (in the year 2021):
This is only the start and we still have a lot of exciting challenges on the roadmap ahead of us:
- As mentioned earlier, our app might be accessed in low-bandwidth areas and on older devices. One of the focus areas is going to be various performance optimizations to help USL load even faster.
2. We’re planning several growth features on USL targeted towards increasing our signup and login rates:
- We’re exploring the webauthn browser API, which allows us to authenticate using built-in authenticators like Windows Hello or Apple’s Touch ID. Most newer devices come with platform authenticators built into them and we see this as having huge potential in onboarding users smoothly.
- We’ve been looking into potentially using QR codes to transfer already-authenticated sessions between mobile and desktop devices.
- We’re planning to run experiments tweaking flows to take into account various regional differences (e.g., SMS OTP has deliverability issues in some regions and we’ve been looking at WhatsApp OTP codes as a backup factor).
3. We also plan to look into platformization of other aspects of our identity stack across the company. USL could eventually be used as a pluggable re-authentication framework for any activity that needs explicit re-authentication. An example would be asking the user to re-authenticate just before payment or updating their profiles. Just like we’ve created the authentication stack to be plug-and-play for apps at Uber, we’d like to do something similar with the identity/profile management stack. This is the UI used to update your profile, change passwords, enable/disable 2FAs, etc., that can be embedded by mobile apps or used as a standalone desktop app.
4. With the diverse set of products at Uber, from a customer POV the experience of logging into these apps happens in silo and with high friction. By recognizing and honoring the logged-in state of the user on the same device, we’re looking into implementation of a One-Tap SSO across all Uber Apps.
Want to Learn More?
As we continue to innovate and grow, we’re looking for talented folks who share our passion for making the world a better place.
The customer identity platform team deals with security concerns that cut across Uber (like API token management, OAuth, signup and login stack, identity management, etc.). If you’re interested in taking on some of these challenges with our team and Uber Engineering, check out our open engineering positions and reach out to firstname.lastname@example.org. We’d love to hear from you!