11 “Gotchas” of Implementing Auth0’s New Universal Login Experience

Senior Software Engineer

APrime recently completed an engagement with a new, disruptive FinTech startup that provides banking and financial services specifically catered to the needs of senior citizens. Their offering includes sophisticated, industry-first features related to fraud detection and fine-tuned customer controls on permitted transactions. For more details on that engagement, visit our case study.

The startup integrated Auth0 as their authentication solution before bringing us in, but opted to raise the bar on security before launching a mobile app and responsive mobile site. Upgrading from Auth0’s Classic Universal Login to the New Universal Login Experience (ULE) would enable cutting-edge security features like biometric authentication and a more secure experience for their users — a top priority in supporting their vulnerable senior-aged user base in today’s fraud-laden financial ecosystem.

Our Challenge

Our team was tasked with executing a seamless migration to this new login experience. Our approach focused on automating the configuration management and deployment process, enabling the client team to quickly and reliably push changes to each of their Auth0 environments.

We ran into numerous limitations while working with some of Auth0’s ULE features and spent many hours searching and posting on forums, working with Auth0 support, and ultimately coming up with creative solutions and workarounds to the product’s constraints. 

Essentially, while previous Auth0 login screen templates (login, MFA and password reset) were available as standalone HTML documents with full flexibility in the body structure, styles, fonts and scripts, the New Universal Login Experience limits customization via Liquid templates, general design settings, and prompts. This makes it difficult to customize settings and introduces complexity in properly managing configuration changes.

Going the default route and applying changes through the Auth0 UI would bypass the standard “best practices” change management process and raise the risk of introducing bugs into production. Instead, we found it necessary to set up automated change management using our one-click deployment strategy, allowing for better visibility into logs, proper code review, and ability to rollback any changeset.

With our first Universal Login Experience implementation behind us, we are here to share what we learned and hopefully save you many hours of frustration and experimentation. Beyond the details below, you can also refer to our GitHub repository of Auth0-related scripts — we’d love to hear more questions and feedback over there.

Constraint #1:

Auth0’s new ULE has no out-of-the-box solution for separately managing widget design and styles across multiple applications. 

Our Solution:  APrime designed a solution that configures appearance per application by using application IDs as discriminators for login widgets styling, DOM manipulation, prompt replacement, and more. For example, here’s how you would use Liquid templates together with EJS (Embedded JavaScript) in Auth0’s universal template:  

if ('{{ application.id }}' === '<%= app2 %>')...

In this case, application.id is provided by Auth0 on the widget page, so we can compare it with, for instance, app2 that is defined in the EJS template in the same project. Once the project is built, the latter value will be static in the actual Liquid template that is pushed to Auth0, and during runtime evaluation of the application.id; if there is a match with app2, then you can easily execute customization for a given application

Constraint #2:

While the new ULE template can reference custom messages passed in the URL as query parameters with an “ext-” prefix, it does not support some very commonly used special characters like “,” and “!”. If such characters are used inside a URL parameter, regardless of whether the parameter is URL-encoded, then the login page completely fails to load — without any kind of helpful error message. This initially made displaying feedback to the user such as, “Congratulations, your account has been created!” quite challenging.  

Our Solution: We set the parameter in the initial function using a dummy variable, which is then later replaced by the “real” text in the ULE template’s <script> tag. 

function find_element_with_message_and_replace_text(messageType, newText) {
    if ('{{ transaction.params.ext-message }}' === messageType) {
        document.getElementById('some-message').innerText = newText;

if ('{{ transaction.params.ext-message }}') {
        'Congratulations!\n' +
        'Your account has been created!\n' +
        'For your security, we require you to re-enter your email address and password below.',
        'For your security, you have been logged out due to 10 minutes of inactivity.\n' +
        'You may log in again below.',

Constraint #3

Hiding an element in a ULE widget (ex. Login, Password Reset or MFA) is not available as a straightforward command, and can only be achieved with a nasty hack.

Our Workaround: We set up a function to find an element in the template by iterating through <p> elements in the document, match the text for that given application, and then insert a rule to apply a style with “display: none” for that particular element.

For example, in order to hide a signup button in the log-in widget, we used the following code:

function string_between_strings(startStr, endStr, str) {
    pos = str.indexOf(startStr) + startStr.length;
    return str.substring(pos, str.indexOf(endStr, pos));

function find_element_with_signup_and_hide_it(array) {
    if ('{{ application.id }}' === '<%= app2 %>' ||
        '{{ transaction.params.ext-message }}' === 'account_creation_message') {
        var signUpLinkClassId = '';
        for (var i = 0; i < array.length; i++) {
            if (array[i].innerText === 'Not a customer? Sign up here') {
                signUpLinkClassId = string_between_strings('<p class="', '">Not a customer?', array[i].outerHTML).split(' ').pop();
        if (signUpLinkClassId) {
            var style = document.createElement("style");
            style.sheet.insertRule(`.${signUpLinkClassId} { display: none; }`);


Constraint #4:

The client wanted to change the url on the signup button to direct users to their website, rather than Auth0’s default link.

Our workaround: Add a function to search ‘a’ tag elements for a match of ‘href’ attribute with ‘signup’ text, and replace that with a URLvalue from the EJS file :

function find_element_with_signup_and_replace_link(array) {
    for (var i = 0; i < array.length; i++) {
        if (array[i].getAttribute('href').includes('signup')) {
            if ('{{ application.id }}' !== '<%= partnerAppAuth0ApplicationId %>') {
                array[i].setAttribute('href', '<%= url %>/#footer');

Constraint #5:

Auth0 does not currently support remembering a user’s login ID. In a web browser, setting the browser cache to remember the login/password is an easy way to mitigate this constraint. However, in a mobile app, users are forced to start with an empty login text box even if they’ve already populated that field and logged in.

Our Workaround: We implemented a simple and reliable way of populating the last used login id from the local storage in the new ULE template:

<!-- Get email from local storage -->
    if (localStorage.getItem('email') && document.getElementById('username')) {
        document.getElementById('username').setAttribute('value', localStorage.getItem('email'));
<!-- Save email in local storage -->
    if (document.getElementsByName('action') && document.getElementsByName('action')[0] &&
        document.getElementById('username') && document.getElementById('username').value) {
        document.getElementsByName('action')[0].addEventListener('click', saveEmailInLocalStorage);
    function saveEmailInLocalStorage() {
        localStorage.setItem('email', document.getElementById('username').value);

Constraint #6:

Phone MFA (multi-factor authentication) code verification messages cannot be easily configured separately for SMS and voice verifications. This was problematic, as the client desired different logic for those two channels — namely repeating the OTP code twice during the voice call verification while only showing it once in the text message. 

Our Workaround: It was not immediately obvious, but Auth0 supports the use of Liquid templating code in these configurations! We used the following Liquid templating to repeat the OTP code twice in a voice call, and to not repeat it at all in an SMS text message:

{% if message_type == "sms" %} Hello, Your verification code is {{code}}. Do not share it with anyone. {% endif %} {% if message_type == "voice" %} Hello, Your verification code is {{code}}. Do not share it with anyone. Your verification code is {{code}} {% endif %}

Additional Constraints…aka “Gotchas” #7-11

We ran into a couple additional constraints that you’ll want to be aware of when working with ULE. While we couldn’t find direct solutions for these issues, it was very helpful to identify the problems so that we could work around them.

  • Spaces and new line characters are not supported in custom prompts for any widget.
  • No spacing can be added between any elements, text or buttons on the widget. Only prompt’s text, button text, font and limited button style is supported.
  • There is no way to configure a default phone MFA method. We would have preferred to set Text (rather than Voice) as the default for all of our users, but the template automatically highlights either the Text or Voice radio button based on the MFA option selected by the user during their initial enrollment.
  • There is no way to include the user’s timezone in the password reset email subject line
  • Auth0 provides a testing client for testing ULE templates locally, but it’s not perfect. We found that some cases and features would run fine locally but then that same code would unexpectedly throw errors and fail when deployed and executed in the browser.
    • For example – The Universal Login template doesn’t support comments in <script> tag. Using comments will throw an unexpected end of script runtime error in the browser’s console, and the script fails to execute after the line where the comments are present.

In Conclusion: Successful One-Click Deployment!

While we spent quite a lot of time on experimentation and customization with our first implementation of Auth0’s New Universal Login Experience, we completed it within just a couple of weeks for an on-time and seamless launch, much to the excitement of our client and its users. 

Our one-click deployment for all templates, prompts and settings continues to save the client a ton of time whenever they need to update a widget within the login experience…and now we here at APrime are better equipped to get any client up and running with Auth0 in no time!

If you’re a developer who’s worked with the Auth0’s New Universal Login Experience, we’d love to hear from you in the comments! You’re invited to visit our public repo here. Did you come across the same blockers as we did, and do you find our workarounds helpful? Do you have any of your own to share?

And if your team is looking to launch or improve an implementation of Auth0’s New Universal Login Experience, don’t hesitate to send us a message or schedule a call — we look forward to exploring how we can help.

Let Aprime help you overcome your challenges

and build your core technology

Are you ready to accelerate?