Introduction: How does modern authentication work?
Authentication allows users to securely log into a system and verify that they are who they say they are in subsequent requests. On today’s web, this usually involves creating an account using a combination of username and password.
Let’s start with a quick refresher on how modern web authentication works. The first step is always a user submitting their login credentials into a form. Once the form is submitted, the server will determine whether or not the username and password match that of a previously registered user. If they do match, the server will return what is known as a session token. A session token is a unique identifier that authenticates requests as coming from the same user that just logged in. It’s important to note that session tokens usually are temporary and have an expiration, which is determined by the server.
Now that the user has a session token, each request will need to submit the session token to ensure authentication. It might sound tedious to have to submit the session token on each request, but have no fear, the cookie is here! Cookies are small bits of information that the browser stores for a domain. Cookies are also automatically submitted when a request is sent by the browser. Does this sound like a great way to handle the session token or what? Correct! Session tokens are almost always stored in a browser cookie to make authenticating requests a breeze.
Authentication requirements
Headspace has a large number of Single Page Apps, backend services, websites, and iOS and Android applications. We needed a robust solution for authentication that met the following requirements:
- Multiple platform and language support
- Scalable to millions of users
- Reliable with high uptime
- Ability to separate our users into different groups with varying permissions (i.e., admins vs. standard users)
- Ability to restrict user access to certain applications. For example, our Business-to-Business (B2B) users require access to a different application than our Business-to-Consumer (B2C) users.
- Secure Authentication over HTTPS
Instead of rolling our own implementation for auth, we ended up choosing Auth0 as our authentication provider, as they met our requirements. Auth0 comes with tons of features out of the box and great documentation to help developers be productive. Auth0 provided solutions for each of our requirements mentioned above through their extensive dashboard, APIs, and libraries.
I think it’s important to call out one of the best features of Auth0, which is the logging available through the dashboard. Every single authentication action a user attempts is recorded in the logs. You can search and filter on a user, date range, application, or action type. If you need to debug a certain user’s authentication actions, the ability to look at a snapshot of what’s going on quickly and in real time is a lifesaver. It’s personally helped me debug some gnarly login issues multiple times. One example that comes to mind was when a Customer Support ticket came in where a specific user was unable to complete signup. I was able view the user’s login history via the dashboard and noticed that all of the requests were succeeding on Auth0’s side and the issue was caused by a bug on our backend.
User history view on the Auth0 dashboard from a test user.
Technical implementation
Auth0 uses a JSON Web Token (JWT) as a replacement for the classic session token pattern described earlier. The tl;dr of JWTs is they are a standardized method to encode authorization data with optional additional metadata as a token. Auth0’s authorization JWT is submitted with each request in the Authorization header instead of the cookie header. In practice, you can see the token in your browser’s devtools when you inspect the request headers. More information on JWTs can be found on the jwt.io website or in this blog post titled JSON Web Tokens vs. Session Cookies for Authentication.
Example request with a JWT in the authorization header. Note that the full text value has been obscured for security reasons.
Headspace elected to implement Auth0 using an embedded login instead of utilizing Auth0’s universal login page. The main difference is that the user can stay on a *.headspace.com domain without being redirected to the headspace.auth0.com domain for login and signup. Opting for an embedded login has proven to be a less jarring experience for users.
Login and search flow for Headspace’s B2B Dashboard
Our frontend uses the auth0-js v9 package to handle login and authorization with the Auth0 servers. We created an AuthClient wrapper for the auth0-js package to assist with setting up environment configuration and to wrap the built-in APIs with promises. Headspace mainly uses Redux and Redux-Saga to handle global state on the frontend clients. We have built a set of authentication reducers and sagas that employs the AuthClient wrapper. Our React components for login and account creation triggers a saga when a user interacts with either respective form. The form data is sent through the auth0-js library to the auth0 servers and a fresh JWT is returned back to the client. At this point, the user is authenticated and further requests by the browser will submit the JWT in the authorization header.
On the backend side of things, the JWT submitted through the authorization header is used to allow or block users from accessing resources. Each route specifies which token is expected. For instance, routes used by B2B users will expect a JWT coming from our B2B tenant while other routes used by B2C users expect a B2C token. If there is a mismatch between the expected and submitted tokens, we will return a 401 Unauthorized response. Next, the route handler might want to restrict access further through various scopes. For example, user 123 might only have access to the read:users:123 and write:user:123 scopes. If this user attempts to post data to another user, we would throw a 403 forbidden. Depending on the response from the backend, the frontend will either update with the response data or log the user out.
Single Sign-On and Social Login
Headspace offers Single Sign-On through Apple, Facebook, and Spotify
Single Sign-On (SSO) is a method of authentication that allows users to authenticate into multiple independent websites through a single set of credentials. This is great for users as they will not have to create a new account for each service that supports SSO. The most common form that you would have seen would be Social Login buttons (log in with Facebook, Google, etc.) that are ubiquitous in 2021. Another popular form of SSO is through companies that provide SSO as a service such as Okta or OneLogin. These providers enable customers to create accounts for their employees that aren’t tied to their personal social media accounts and can be managed internally.
In the SSO context, Headspace is what is known as a Service Provider. Service Providers are applications or websites that receive SSO authentication from an outside Identity Provider. An Identity Provider is a service that issues authentication verification for SSO profiles. In the above screenshot, Apple, Facebook, and Spotify are acting as Identity Providers.
Web-based SSO generally follows the following pattern for authentication:
- User navigates to the Service Provider’s login page (e.g., headspace.com/login)
- User clicks on a button for a specific Identity Provider (e.g., Facebook, Google, etc.)
- The browser opens a pop-up or redirects to the Identity Provider’s service (If the user is not logged into the Identity Provider, they are prompted to login or sign up)
- The Identity Provider validates the user’s credentials and creates an authentication token
- The browser redirects back to the Service Provider and sends the token to the Service Provider
- Service Provider validates the token (If validation fails, the user does not gain access to the Service Provider)
- User is logged into the Service Provider
For a more detailed explanation, I would recommend checking out Auth0’s SSO documentation and OneLogin’s article on SSO.
Third-party cookies and Custom Domains
One issue we encountered recently had to do with the changing landscape around third-party cookies. A third-party cookie is a cookie set by a different server than the current domain while a first-party cookie is a cookie set by the current domain. First party cookies are only able to be accessed while on the same domain and browsers support them across the board. Up until recently, third-party cookies were supported by default by all major browsers. This change was caused by both security and privacy issues surrounding the use of third-party cookies to track users as they browse the web. If you’re interested in learning more, I would recommend checking out the related blog posts by the Chromium team and Firefox team. Nowadays, a user will have to explicitly enable third-party cookies in their browser settings, which can cause issues especially when a user is not technically inclined. In fact, we received multiple customer support requests caused by this confusion. Requiring users to disable a security setting to even login was a non-intuitive user experience and, honestly, a terrible idea long term. We needed to find a solution.
Headspace uses Auth0’s aforementioned embedded login flow. As a result, when a user submits their login credentials, it is sent to Auth0’s third-party domain. The default setup for embedded login will store a JWT cookie with your default Auth0 subdomain, and in our case, that would be headspace.auth0.com. This JWT cookie is treated as a third-party cookie and will be blocked by the browser causing the login attempt to fail. Luckily, Auth0 has a workaround for this issue: Custom Domains!
Custom Domain tab for Headspace’s B2B production tenant
In the Auth0 context, setting up a Custom Domain is a way to use your website’s domain when interacting with Auth0’s services and setting the domain on JWT cookies. First, you’ll need to go to your Auth0 dashboard and find the Custom Domain tab. On this page, you can enable the feature and specify the domain you want to use for authorization. For example, we use auth.work.headspace.com as our Custom Domain for our B2B tenant. The next step is to set up a Canonical Name record (CNAME record) to point YOUR_DOMAIN.auth0.com to auth.YOUR_DOMAIN.com. Depending on your setup, you will have to accomplish this in different ways. Headspace uses AWS for hosting, so we used Route53 to configure the CNAME record. The final step is to update the configuration of the auth0-js library to use the new Custom Domain as the domain field. Once this is all setup third-party cookie issues should resolve themselves as new cookies will be created with auth.YOUR_DOMAIN.com domain specified in your new CNAME record.
You can verify your setup by clicking on the “TEST” button. If everything is correct, you should see a success message.
Quick tip for checking CNAME records for those who use mac or linux: the “nslookup -q=cname YOUR_DOMAIN” command can be used to verify the values of CNAME records. Here’s what the record looks like for one of Headspace’s auth Custom Domains.
Conclusion
I hope this post has been helpful and informative! Here some key takeaways:
- Most web-based authentication schemes rely on session tokens
- Session tokens are often stored in the browser’s cookies
- JWT is a standardized scheme to securely store data that can be used for authentication
- Third-party cookie support is being phased out by all major browsers (Chrome, Firefox, Safari, and Edge)
- Auth0 provides a workaround for third-party cookie issues through their Custom Domain system.