The surprising practicality of nested authentication flow

Ontra

June 5, 202513 min read

By Quan Chau, Staff Software Engineer

 

At Ontra, we’re proud to deliver the leading AI-powered technology platform for private markets, trusted by industry giants like Blackstone, Nuveen, AllianceBernstein, and Warburg Pincus. As our customer base has expanded and our product suite has grown, we’ve evolved from a single monolithic application to a dynamic ecosystem of microservices. This evolution has surfaced new challenges, particularly in how we handle authentication. The tried-and-true method of managing sessions with browser cookies worked well when we had a single monolithic app, but after moving to a distributed, service-driven environment, we had to rethink our authentication framework.

In 2024, we migrated to Auth0* to authenticate and manage user sessions across all our applications. Auth0 has a great reputation, documentation, SDK, and provides a wide range of identity management features such as universal login, multifactor authentication, API keys, and threat protection.

However, Auth0 doesn’t provide everything we need, including:

  1. Custom claim in access token
  2. Custom home realm discovery
  3. User impersonation across all microservices in the staging and development environments

In this blog post, we will explain how our authentication is set up with Auth0 and how we overcame challenges to achieve our new objectives.

Our system architecture and old authentication flow

Our software architecture comprises multiple frontend apps using React and multiple backend apps using Ruby on Rails. Users navigate across multiple frontend and backend apps in the same session. We use Auth0 to manage sessions and grant access tokens to frontend apps to make API requests to backend apps.

The flow is simple and works like this:

  1. Frontend apps use Authorization Code Flow with Proof Key for Code Exchange (PKCE) to authenticate and establish sessions with Auth0.
  2. Auth0 grants an access token, which frontend apps store.
  3. Frontend apps use the access token to make API requests to backend apps.

In terms of setup on Auth0, we used Auth0’s Universal Login with “Identifier First” authentication profile.

New objectives and challenges in 2025

1. Custom claim in access token

Ontra maintains our own mapping from user emails to UUID in our monolithic app. As we create more microservices, we need a consistent way to reference users. We want to use UUID instead of email because users occasionally update their emails when their companies rebrand or change their legal names. Ontra-managed UUID is the obvious choice for a consistent reference across multiple apps and databases.

However, Auth0 can only provide the email of the authenticated user because it knows nothing about Ontra’s mapping emails to UUID. Hence, Auth0 cannot insert the user UUID into the access token and then sign it.

Bonus difficulty: Staging Auth0 tenant must handle multiple mappings from email to UUID.

In the staging environment, we have multiple instances of the monolithic app: one per pull request. The same email address can be added to each of those instances independently. This results in different UUID for the same email address depending on which monolithic app instance you interact with.

However, all these instances use the same Auth0 tenant. This creates a complication at login time Auth0 must know which instance the user wants to access and look up the UUID from there.

2. Custom home realm discovery

Auth0 has built-in home realm discovery. However, the logic is quite simple: string comparison of the domain of email input against a mapping of domain to SSO connection. This works for the majority of our use cases, but it would not be usable in the event we wanted to allow sign-ups via passwordless email when users visit certain pages within the application. At the same time, we want existing users to log in via password or SSO like before.

The desired home realm discovery logics look like this:

According to this discussion on Auth0’s forum, this sort of customization is not supported by built-in logics from Auth0. The solutions listed there all point to getting away from the “Identifier First” authentication profile. It’s a difficult trade-off between using built-in Auth0 features and meeting product objectives.

Source: Jake Clark Tumblr

3. User impersonation

Ontra scrambles sensitive information in our production database before copying it to our staging environment for development and debugging purposes. When we receive a bug report that a user encounters an error on a certain page, we want to impersonate that user in the staging environment and reproduce the bug. We wouldn’t know the user’s email or name, only the UUID.

The old method of impersonating a user works like this:

  1. Log in as yourself using SSO.
  2. Use an internal widget and enter the target user UUID. This widget is only available in staging and local development environments.
  3. The widget makes a request to the monolithic app to set a Rails session variable indicating impersonation of another user.

This method fails if you have to traverse other microservices to reproduce the bug, as those services neither receive nor recognize the Rails session from the monolith app. Ideally, we would like to change the access token from Auth0 to include the email and UUID of the impersonating user. Then all microservices will recognize the access token as coming from that user.

According to this discussion on Auth0’s forum, this feature is not possible from Auth0.

The more intuitive solution: Insert the user UUID in post-login actions in Auth0

When we think about how to insert a user UUID into access token, the more intuitive solution is to do it in post-login actions in Auth0. In these actions, we can look up the UUID given the authenticated email address.

Approach 1: Auth0 action makes an API request to the monolithic app

In this approach, post-login actions would use client credentials flow to obtain a Machine-to-Machine access token, then use it to make an API request to obtain the UUID of the user from the given authenticated email address.

Advantages

  • It is simple to implement.
  • It achieves the objective of including a custom UUID claim in the access token for the staging and production environments.

Disadvantages

  • It doesn’t help with Custom Home Realm Discovery or user impersonation in the staging and development environments.
  • It increases the cost on Auth0 of client credentials tokens.
  • It doesn’t work with local development. According to this discussion on Auth0’s forum, the representative from Auth0 suggests deploying the app before testing it.

We thought about ways to deploy the app faster, such as Replit and GitHub Codespaces. These methods will solve the problem, but we didn’t want to impose these tools upon every software engineer.

Approach 2: Keep a cache of mapping email to UUID somewhere between local development and Auth0

With this approach, we will publish changes regarding emails and UUIDs from the monolithic app to a data store that both the monolithic app and Auth0 can reach out to. We look at different databases and connections that Auth0 provides and divide them into two broad categories:

  1. Auth0’s built-in database: This data store is accessible via a built-in users API.
  2. Self-managed PostgreSQL database: Auth0 can connect to the same PostgreSQL database that the monolithic app uses, and look up the user there.

Advantages

  • It includes a custom UUID in the access token in the same way for all environments

Disadvantages

  • It doesn’t help with Custom Home Realm Discovery or User impersonation in the staging and development environments.
  • It requires large-scale cache updates when we spin up and down the app instances in staging environments. These happen every time a new pull request is created or merged. Each new app instance brings thousands of mapping entries from email address to UUID, and each spin down destroys just as many. We predict this would quickly run into the rate limit of Auth0’s built-in users API. In contrast, a self-managed PostgreSQL database would be able to handle bulk updates. We think managing the mapping cache in a self-managed PostgreSQL database is tolerable.

The less intuitive solution: Nested authentication flow

The journey to discover the solution

It’s evident that the more intuitive solution above has significant disadvantages, and it doesn’t help with Objective 2 (Custom home realm discovery) or Objective 3 (User impersonation). We spent a few weeks thinking about this, and one day we hit the lightbulb moment.

Ontra uses SSO via Google Workspace to let employees log in to Ontra apps. And that SSO goes through Auth0 as well. We inspected the payload of the response from Google Workspace to Auth0 and noticed it contains both the user ID of Google and the email address. That got us thinking:

If Google Workspace can return both user ID and email address in the same payload to Auth0, why don’t we do the same with Ontra apps?

So we looked into how to make the monolithic app, which has the authoritative mapping of email address to UUID, an Identity Provider (IdP) just like Google Workspace. There are two main choices of authentication protocol: OpenID Connect and SAML.

  • OpenID Connect requires the monolithic app to store the auth code and must allow Auth0 to make a request to it to obtain an access token. This would run into trouble with local development like Approach 1 above.
  • SAML doesn’t require the monolithic app to store the auth code and works via redirections on the browser. The browser has access to local development, which has a clear advantage because it works in all environments.

Now, to make the monolithic app a true IdP, it must know how to authenticate users. But only Auth0 has all the passwords, MFA, and SSO setups not the monolithic app. To solve this, we would need to make yet another authentication flow from the monolithic app back end to Auth0. In total, the whole flow looks like below:

This diagram reminds me of the 2010 movie Inception. In this movie, the protagonist Cobb (played by Leonardo DiCaprio) was tasked with infiltrating a businessman’s dream and instilling the idea that he should sell his business. During the infiltration, he faced many setbacks, but he doubled down and dived deeper into many layers of dreams. The authentication flow above follows that principle by nesting an authentication flow within another authentication flow.

We named this flow “Auth-ception.”

Compared to the standard setup of a single, regular Auth code flow with PKCE, Auth-ception divides the session management aspect into outer flow (steps 1, 2, 6, and 7) and the authentication aspect into inner flow (steps 3, 4, and 5). Auth0 continues to do both important jobs of managing sessions and authentication, while the monolithic app is a thin layer adding extra information about the user.

How “Auth-ception” fulfills our objectives

Meeting objective 1: Custom claim in access token

At step 6, the monolithic app obtains the authenticated user email, and then includes additional information about the user in the SAML response: UUID, first name, last name. Then in a post-login action in Auth0, we only need to look up this ready-to-use information and insert into access token in step 7. Then all backends can authenticate by user UUID, which is a more reliable and consistent method than authentication by email.

Moreover, Auth-ception has two main advantages over the more intuitive solution:

  1. The SAML flow in steps 2 and 6 works well with local development. The flow uses only redirections on browser, which can access a local host.
  2. There is no cache to maintain. The UUID look up is performed once at login only and uses the latest value.

Meeting objective 2: Custom home realm discovery

At step 1, we can pass an extra parameter from the frontend app to the monolithic backend app to tell it that it can allow sign-ups. Then at step 3, it can decide:

  1. If no sign-up is allowed, start regular Auth code flow on Auth0
  2. If sign-up is allowed, show a copy of the first login screen where the user enters their email. Then, based on email input, the monolithic backend app will follow the decision tree above to trigger regular Auth code flow or use passwordless email flow. We only need to maintain one simple screen where the user enters their email, which is manageable.

Source: Jake Clark Tumblr

Meeting objective 3: User impersonation across all microservices in the staging and development environments

After step 5, the monolithic app can determine if an authenticated user is an Ontra employee, then show a screen that lets the user select which user to impersonate, and then return the information of the impersonated user in step 6. Auth0 will dutifully accept that SAML response as if it comes from the impersonated user. Frontend apps will receive an access token that contains the email and UUID of the impersonated user.

As of now, the user impersonation feature is limited to the staging and development environments only, so we have no need to track impersonating users in audit logs. If the feature gains traction in the future, we may open it up in production to let Ontra’s Customer Success Associates diagnose problems from the user’s perspective. At that point, we can include the impersonating user information in the SAML response of step 6, the access token of step 7, and the audit logs.

Disadvantages

Auth0 considers user login via the connection targeting an external IdP different from the login via the connection targeting a monolithic app and bills them separately. We plan to mitigate this using User Account Linking.

Implementation details

Determine which monolithic app instance to authenticate against

We mentioned above that our staging environment is highly dynamic, with many app instances spinning up and down. As a result, we need to do the following to make Auth-ception work:

  1. On spinning up a new instance, automatically create a new SAML connection with a predictable name. For example, if the app instance is hosted at app-staging-123.ontra.ai, then we will name the SAML connection app-staging-123-saml. This connection will point to that app instance.
  2. On spinning down the instance, automatically destroy the SAML connection above.
  3. When a frontend app wants to authenticate, it must use the above SAML connection in the connection parameter in auth0-spa-js.

Passing parameters from the frontend to Auth-ception

We support a feature called IdP-initiated login that lets users add a tile/bookmark in Okta or equivalent to access Ontra apps. This tile tells Ontra apps to authenticate via the customer’s SSO, which likely already has an established session, and seamlessly logs the user into the Ontra app. This feature depends on setting the connection parameter in auth0-spa-js to the name of the connection pointing to customers’ SSO.

This conflicts directly with the above point, where the connection must point to an instance of the monolithic app. In essence, we would need to find another channel where frontend apps pass an extra parameter to the monolithic app to tell it to use a specific SSO connection in the nested authentication flow. We find that the login_hint parameter in auth0-spa-js is the channel we need.

  1. From the frontend apps, we encode the SSO connection named as JSON and include that in the login_hint parameter in auth0-spa-js.
  2. In Auth0, we configure the SAML request template of the SAML connection targeting the monolithic app to include login_hint as instructed in this discussion.
  3. In the monolithic app, decode the SAML request, decode the login hint attribute as JSON, get the connection name, and then use it as a parameter to a nested Auth code flow.

This method is effective not only for the SSO connection name but also for any customization you want. We use it to let specific React routes indicate that they would allow sign-ups while most routes don’t.

Conclusion

We find Auth-ception to be practical and effective. It solves problems where we find Auth0 inadequate:

  1. Custom claim in access token combined with local development.
  2. Custom home realm discovery combined with Identifier First Authentication Profile.
  3. User impersonation with an audit trail of the impersonating user.

Evidently through the referenced discussion posts, these problems are not unique to Ontra. We hope that by publishing Auth-ception publicly, we contribute back to the community and help others facing these problems in their authentication flow.

 

 

 

*The organizations referenced in this article have no affiliation with Ontra, and neither Ontra nor such organizations promote or endorse the other’s products or services.

Explore Category

Learn more from Ontra by subscribing to our newsletter, Accelerate.

Subscribe Now

Ontra is not a law firm and does not provide any legal services, legal advice, or referral services and, as a result, we do not provide any legal representation to clients, nor do we participate in any legal representation of clients. The contents of this article are for informational purposes only, and are not intended to constitute or be relied upon as legal, tax, accounting, regulatory, or other professional advice, opinion, or recommendation by Ontra or its affiliates. For assistance or guidance regarding the impact or applicability of the topics discussed in this article to your business, please consult your legal or other professional advisers.

Explore our content