Chaoverse Lab

Identity & Auth

Auth0: Classic Login vs Universal Login vs ACUL

seed created 2026-10-09

When someone at Auth0 says “login page”, they mean the page where the user types their password. Auth0 has three ways to produce that page: Classic Login, Universal Login, and ACUL (Advanced Customizations for Universal Login). Everything else (tokens, sessions, security checks) works the same way underneath. This page explains the difference from zero, then walks the migration your team is doing: Classic → Universal → ACUL.

The 30-second answer

legacy · frozen

Classic Login

Auth0 hosts the page, but it is built from Auth0's older JavaScript widget, Lock (or the auth0.js library). You may have hand-edited its HTML in the dashboard.

Auth0: “Classic Login no longer receives updates.” If you customize it, you maintain it.

default · modern

Universal Login

Auth0 hosts and draws the page: lightweight HTML rendered on Auth0's servers. You style it with a theme, change the text, and optionally wrap it in your own HTML template.

Gets every new feature (passkeys, Google One Tap, Organizations…) and security fixes automatically.

mode of Universal Login

ACUL

Still Universal Login, but for the screens you choose, your own JS/CSS bundle draws the UI. Auth0 still runs the login logic and passes your code the data it needs.

Generally available since 2025-12-10. You now own a CDN, a build pipeline and the UI code.

Classic: frozen. Universal: Auth0 draws, you style. ACUL: Auth0 decides, you draw.

1. Foundations: the words you'll keep hearing

If you're new to identity, these are the terms the rest of this page relies on.

Identity provider (IdP) / authorization server
A service that stores and checks user identities, then hands out tokens. Auth0 plays this role: your app sends the user to Auth0, Auth0 proves who they are, and Auth0 returns tokens.
Tenant
Your own isolated Auth0 account space: its users, applications, settings and domain (for example your-tenant.auth0.com). Teams often keep separate dev / test / prod tenants.
OAuth 2.0
An open standard for letting an app get permission to act for a user, in the form of an access token the app sends to an API (“what the app may call”).
OIDC (OpenID Connect)
A thin layer on top of OAuth 2.0 that adds who the user is: an ID token with the user's identity. “Log in with…” buttons are usually OIDC.
Authorization code flow + PKCE
The standard login dance (diagram below). Auth0 returns a short-lived code, which the app swaps for tokens. PKCE (Proof Key for Code Exchange) adds a secret code_verifier so a stolen code is useless. It's the flow for single-page and native apps, which can't keep a client secret.
Redirect (hosted) login
The app redirects the browser to the IdP's own login page, and the IdP sends the user back afterwards. Classic, Universal and ACUL are all hosted login.
Embedded login
The opposite: your app shows the login form and posts the password to the IdP. Possible with Lock or auth0.js, but harder to get right (see cookies below). Not what this page is about.
Custom domain
Serving Auth0 under your own name, such as login.example.com, instead of tenant.auth0.com. It's required for Universal Login page templates, partials and ACUL.
SSO (single sign-on) cookie
After login, Auth0 sets a session cookie on its domain. Every app that redirects there sees the same cookie, so the user logs in once for all apps. Because the browser visits Auth0's domain directly, it's a first-party cookie, which browsers don't block.
Why hosted login matters. With redirect login, users always type passwords on one recognizable domain, and only that origin ever sees them. Embedded login on a web app needs “cross-origin authentication”, which depends on third-party cookies that Firefox, Safari and Chromium browsers restrict or block by default. Auth0's fix is a custom domain on the same parent domain as your app (example.com + login.example.com), so the cookies are no longer third-party.

A redirect login, step by step

All three options use this exact flow. The dashed box is the only part where they differ.

Browserthe user Your apphas a client_id Auth0your tenant 1. click “Log in” 2. redirect to /authorize + state, code_challenge 3. GET /authorize Auth0 looks for its SSO cookie ★ Login page: who renders it? 4. no session → login page HTML served from Auth0's domain 5. user submits credentials 6. redirect to /callback ?code=…&state=… 7. GET /callback?code 8. POST /oauth/token code + code_verifier 9. ID + access tokens
Steps 1–3 and 6–9 are identical in every option. The login page in step 4 is drawn by Lock (Classic), by Auth0's server (Universal) or by your bundle (ACUL). In every case it is served from Auth0's domain, so SSO and password safety work the same. state protects against CSRF (cross-site request forgery); PKCE is the code_challenge / code_verifier pair.

2. Classic Login

What it is. An Auth0-hosted page (so it still uses redirects) built on Auth0's JavaScript libraries: Lock (a drop-in login widget), auth0.js (a lower-level library for building your own form), the MFA widget (MFA = multi-factor authentication, e.g. a code from an app) and Password Reset.

Basic customization

Logo, primary color, background color.

Advanced customization

Flip Customize Page in the dashboard and edit raw HTML for exactly three pages: Login, Password Reset, MFA. Starting templates: Lock, Lock (Passwordless Mode), Auth0.js (custom form), Authentication API.

Constraints

CSS “must be inline, as you cannot host a separate CSS file in your Auth0 tenant”. The page is JavaScript: no JS, no login form.

What you own

  • Auth0: “When you enable customization for a specific page, you assume responsibility for maintaining that page, as it will no longer receive automatic updates from Auth0.” That includes bumping Lock and auth0.js versions.
  • Watching for XSS (cross-site scripting: attacker script running on your page) and being careful with third-party JS on the login page.
  • With home realm discovery (routing users to their company's login by email domain), your list of connections is public in Auth0.setClient.

Status

Auth0's comparison page: “Auth0's active development efforts are focused on Universal Login, and Classic Login no longer receives updates.” The Classic customization page itself now says Auth0 recommends ACUL for customizing login pages. No formal end-of-life date was found.

Classic is missing passkeys, Google One Tap, WebAuthn (device biometrics / security keys), Organizations, Email and Voice MFA, MFA driven by Actions (your own code that Auth0 runs during login), WCAG (Web Content Accessibility Guidelines) compliance, and full localization (it only localizes the login page). It has two features Universal doesn't: email magic link and Kerberos for AD/LDAP (Windows-domain single sign-on against Active Directory / Lightweight Directory Access Protocol servers).

Don't confuse Classic Login with embedded Lock. The same Lock library can run inside Classic's hosted page or be embedded directly in your app. Switching the tenant's login experience only affects the hosted page.

3. Universal Login

Universal Login (sometimes called “New Universal Login”, abbreviated UL) is Auth0's default. Auth0 renders lightweight server-side HTML that doesn't need JavaScript, and ships updates and fixes automatically.

Prompts and screens

Universal Login thinks of login as a series of steps:

  • A prompt is a step in the flow, e.g. login, signup, mfa.
  • Each prompt has one or more screens, e.g. login-id (just the email box), login-password, mfa-login-options.
  • Auth0 decides which screen comes next. You customize screens; you never script the order.

This model matters for ACUL: you switch screens to your own UI one at a time.

Ways to customize, least to most work

  1. Theme / no-code editor. Colors, fonts, logo. Also settable via the Management API (Auth0's admin HTTP API for configuring your tenant) with PATCH /api/v2/branding, which is how you set the favicon.
  2. Text customization. Change any page's wording in the dashboard or Management API, per supported language.
  3. Page template (Liquid). Your own HTML around Auth0's form. Liquid is a small templating language: {{ variable }} and {% tag %}. Needs a custom domain and can only be set via the Management API.
  4. Partials. Small HTML/CSS/JS/Liquid snippets injected at fixed slots inside login and signup screens (form-content-start/end, form-footer-start/end, secondary-actions-start/end). Max 10,000 characters each; needs a custom domain and a page template.
  5. ACUL. Replace the screen's UI entirely with your own bundle (next section).

The smallest valid page template

Copied from Auth0's docs. auth0:head and auth0:widget are required: they inject Auth0's styles/scripts and the login form.

<!DOCTYPE html>
{% assign resolved_dir = dir | default: "auto" %}
<html lang="{{locale}}" dir="{{resolved_dir}}">
  <head>
    {%- auth0:head -%}
  </head>
  <body class="_widget-auto-layout">
    {%- auth0:widget -%}
  </body>
</html>

Upload it with the Management API (also GET and DELETE on the same path, or auth0 api put "branding/templates/universal-login" from the Auth0 CLI, its command-line tool):

curl --request PUT \
  --url 'https://{yourDomain}/api/v2/branding/templates/universal-login' \
  --header 'authorization: Bearer MGMT_API_ACCESS_TOKEN' \
  --header 'content-type: text/html' \
  --data '<!DOCTYPE html><html><head>{%- auth0:head -%}</head><body>{%- auth0:widget -%}</body></html>'

Templates can read application.*, branding.*, tenant.*, organization.*, custom_domain.*, user.* (only after authentication), prompt.name, prompt.screen.name, locale, and your own ext--prefixed /authorize parameters. So one template can vary per app, per organization or per screen.

Docs warning: the widget's CSS class names change with every Auth0 build, and its HTML structure can change. Style around it, never target its internals. (This is a big reason ACUL exists.)

Handy behaviors

  • screen_hint=signup on /authorize opens signup directly; prompt=login forces the login page.
  • login_hint pre-fills the email or username.
  • Going straight to /login errors unless you've configured a default login route.

Only in Universal Login

Passkeys, Google One Tap, WebAuthn/device biometrics, Organizations, Email and Voice MFA, MFA selection via Actions, the post-challenge password-reset Action trigger, buttons for enterprise connections, customizable localized text on every page, the no-code editor, and WCAG 2.2 AA accessibility.

4. ACUL: Advanced Customizations for Universal Login

ACUL lets you “replace the hosted login UI with your own React or Vanilla JS app while Auth0 still drives the authentication state machine.” A useful picture: Auth0 is still the referee and keeps the score; you only repaint the stadium. Bot detection, MFA, passkeys, sessions and tokens all stay Auth0's job.

Timeline

Early Access 2025-02-06 → MFA screens added 2025-04-01 → General Availability (GA) 2025-12-10, with “full screen parity”, React and TypeScript SDKs (SDK = software development kit) v1.1.0, a dashboard visual editor and auth0 acul CLI (command-line interface) tooling.

Older doc mirrors (e.g. dev.auth0.com) still show an “Early Access” banner. They're stale.

Requirements

A tenant on Universal Login with a custom domain, a first-party application, and “a CDN with an included CI/CD pipeline” (CDN = content delivery network; CI/CD = automated build and deploy). The Quickstart also needs Identifier First login (email first, password on a later screen), Node.js 22 and the Auth0 CLI.

Per screen, not per tenant

Each screen has its own rendering config. Screens you don't switch stay on standard Universal Login. You can migrate one screen at a time.

How it works

Build timeYour CI compiles your app into static JS + CSS and uploads it to your CDN.
→
Auth0 host pageFor an ACUL screen, Auth0 serves a minimal page whose <head> points at your files.
→
Browser checks SRISubresource Integrity: the browser refuses files whose hash doesn't match the one configured.
→
Your code runsIt reads the injected universal_login_context, draws the screen, and submits via the ACUL SDK.

Rendering mode and per-screen settings

Every screen has rendering_mode: standard (Auth0's normal UI) or advanced (your bundle). In advanced mode these fields matter:

FieldWhat it does
head_tagsRequired in advanced mode, with at least one script. Allowed tags: script, link, style, meta, title, base, noscript, template. Max 10 attributes per tag; content max 250 characters. integrity (an SRI hash) is required on scripts and stylesheets.
context_configurationRequired in advanced mode. An allow-list of the data Auth0 sends to the browser (branding, tenant, client, organization, screen.texts, user metadata, untrusted data…). Empty or null values are stripped.
filtersOnly use advanced mode for some requests: match_type is includes_any or excludes_any, over clients, organizations or domains (by id or metadata key/value). Non-matching requests fall back to standard.
use_page_templateRender the ACUL screen inside your tenant's Liquid page template.
default_head_tags_disabledDrop Universal Login's default head tags (e.g. the favicon).

Asset URLs may contain Liquid variables ({{ screen.name }}, {{ prompt.name }}, {{ locale }}, {{ client.name }}, organization fields…). If one URL can resolve to several bundles, list one hash per variant, space-separated. Example from the docs:

{
  "head_tags": [
    {
      "tag": "script",
      "attributes": {
        "src": "https://cdn.example.com/assets/{{ client.name }}/bundle.js",
        "defer": true,
        "integrity": "sha256-hashForClientA sha256-hashForClientB"
      }
    },
    {
      "tag": "link",
      "attributes": {
        "rel": "stylesheet",
        "href": "https://cdn.example.com/assets/{{ client.name }}/styles.css",
        "integrity": "sha256-cssHashForClientA sha256-cssHashForClientB"
      }
    }
  ]
}
Anything under untrusted_data.* (submitted form data, login_hint, screen_hint, ui_locales, your ext- parameters) comes from the user's URL or form. Escape it before rendering. And keep context_configuration minimal: think twice before exposing user.app_metadata / user.user_metadata keys to the browser.

The SDK

Two npm packages (not loadable from a CDN): @auth0/auth0-acul-js (framework-agnostic TypeScript) and @auth0/auth0-acul-react (React hooks, React ≥ 18.3.1). Each screen is its own import. Example copied from the README:

import LoginId from '@auth0/auth0-acul-js/login-id';

const loginIdManager = new LoginId();

// Trigger the login method on button click
loginIdManager.login({
  username: <USERNAME_FIELD_VALUE>
});

// Get list of mandatory fields
const { transaction } = loginIdManager
const requiredFields = transaction.getActiveIdentifiers();

// Social login
loginIdManager.continueWithFederatedLogin({
  connection: selectedConnection.name,
})

Errors come from getErrors() (or useErrors in React). The React SDK also has context hooks (useScreen, useTransaction, useBranding, useUntrustedData…) and helpers (usePasswordValidation, useMfaPolling, useResend). Its README warns that root imports load every screen, so import per screen.

The React SDK README lists 77 prompt/screen pairs: login and identifier screens, signup, passkey enrollment, captcha, password reset, every MFA family, organizations and invitations, device flow, logout, consent and more.

Configuring it: CLI, API, dashboard, Terraform

# scaffold a project: choose React or JS, choose screens
auth0 acul init "App"
auth0 acul screen add SCREEN_NAME

# local dev on port 55444 with mock context (Universal Login Context Inspector)
auth0 acul dev
# end-to-end against your real tenant; every reload restarts the login flow
auth0 acul dev --connected
auth0 test login

# per-screen rendering config
auth0 acul config generate | get | set | list | docs
Deprecated on 2026-06-15: auth0 ul customize --rendering-mode advanced and auth0 ul switch (auth0 universal-login switch). Replace them with auth0 acul config …. Old blog posts and scripts still use them.
  • Dashboard: Branding → Universal Login → Customize authentication screens → pick a screen → Rendering mode = Advanced → set Head Tags and Additional Data → Save & Publish.
  • Management API: GET/PATCH /api/v2/prompts/{prompt}/screen/{screen}/rendering for one screen, GET/PATCH /api/v2/prompts/rendering in bulk.
  • Infrastructure as code: Terraform resource auth0_prompt_screen_renderer, or the Deploy CLI prompts resource.

5. Who renders the page, and what do you own?

Pick a setup. Each row is one layer of the login page; the color says who is responsible for it.

Auth0 Auth0 builds and maintains it You your team builds and maintains it Shared split Frozen nobody: no updates

    6. Side by side

    DimensionClassic LoginUniversal Login (theme / template / partials)ACUL (advanced screens)
    Who hosts the pageAuth0 (redirect)Auth0 (redirect)Auth0 serves a host page; your assets come from your CDN
    Who draws the formLock / auth0.js / MFA widget in the browserAuth0, as lightweight server-side HTML; JS not requiredYour React or vanilla JS bundle
    Customization depthLogo and colors, then full HTML of Login, Password Reset and MFA pages onlyTheme → text → Liquid template → partialsFull UI per screen; any framework
    CSSInline onlyTheme settings; your CSS around the widget (don't target widget classes)Your stylesheet, SRI-pinned
    Maintenance & securityOnce customized: yours (SDK bumps, XSS)Auth0 ships updates automaticallyAuth0 runs the flow; you own CDN, CI/CD, SRI hashes, escaping untrusted data, SDK upgrades
    New features“No longer receives updates”Ongoing developmentBuilt on Universal Login; new screens need your UI
    Passkeys / One Tap / WebAuthn / OrganizationsNoYesYes (SDK has passkey, WebAuthn and organization screens)
    MFAMFA widget page; no Email/Voice MFA; Duo supportedFactor picker, Email/Voice MFA, Actions-driven MFA; Duo only if it's the sole factorSame flow as Universal; you build the MFA screens
    LocalizationLogin page onlyFull localization, customizable text on all pagesYour job, using screen.texts and tenant.enabled_locales or your own translations
    AccessibilityNot WCAG compliantWCAG 2.2 AA + EN 301 549Your job inferred
    PerformanceHeavier JS bundle (“Lightweight HTML: No”)“Lightweight HTML”, “simpler, faster experience”Depends on your bundle; import per screen
    SSOAuth0 session; SSO with social dev keys “not supported”Auth0 session cookie on tenant / custom domain; silent auth worksSame Auth0 session as Universal inferred
    Only hereEmail magic link, Kerberos for AD/LDAPPasskeys, One Tap, Orgs, Email/Voice MFA, no-code editor…Your own design system, multi-brand, A/B tests, custom UI logic
    Custom domain needed?Not for the HTML editor inferredFor templates and partialsYes
    Partialsn/aYes (login and signup screens)Ignored on advanced screens
    How to turn onBranding → Universal Login → Advanced Options → choose experienceSame togglePer screen: dashboard, Management API, auth0 acul config, Terraform

    7. Migration path: Classic → Universal → ACUL

    ACUL only extends Universal Login, so the move is two hops. Classic HTML and Lock customizations don't carry over; they get rebuilt.

    Hop 1: Classic → Universal Login

    1. Inventory Classic. Which of Login, Password Reset and MFA pages are customized, and which Lock options are in use (additionalSignupFields, showTerms, forgotPasswordLink, allowForgotPassword…). Each needs a Universal equivalent: setting, text, template or partial.
    2. Check Classic-only features. Email magic link and Kerberos don't exist in Universal. Duo must be the only MFA factor.
    3. Set up a custom domain on the same parent domain as your apps. Hop 2 needs it anyway.
    4. Fix routes. Set an Application Login URI: Universal ignores the password-reset email's redirect URL and sends users to the default login route, and hitting /login directly errors without one.
    5. Turn off “Customize Login Page” (the signup-prompt customization docs require it off), then rebuild with theme, text, template and partials.
    6. Switch in Branding → Universal Login → Advanced Options, first in a dev tenant. Test login, signup, reset and MFA end to end.

    Hop 2: Universal → ACUL

    1. Prerequisites: custom domain, first-party app, Identifier First enabled, separate dev/test/prod tenants, CDN + CI/CD, Node.js 22, Auth0 CLI.
    2. Scaffold: auth0 acul init "App", auth0 acul screen add …, or start from the official samples / React boilerplate.
    3. Develop locally: auth0 acul dev with mock context (JSON in public/screens/{prompt}/{screen}/, registered in public/manifest.json). Switch to --connected late, since every reload restarts the flow.
    4. Rebuild each advanced screen: forms, error display, social buttons, identifier types, localized text, keyboard and screen-reader behavior. Partials and the built-in widget no longer apply.
    5. Deploy: Auth0's reference workflow builds with Vite in GitHub Actions, uploads to a private S3 bucket behind CloudFront, and then the Auth0 CLI (using an M2M, machine-to-machine, app with read:branding, update:branding, read:prompts, update:prompts, read:custom_domains) points each screen at the new assets and hashes.
    6. Roll out gradually with filters: one client, organization or domain first. Everyone else stays on standard rendering.

    Checklist

    Tick as you go. Nothing is saved; it resets when you reload.

    Classic → Universal

    Universal → ACUL

    Gotchas

    • New bundle, new hash. Every build changes the SRI hash, so CI must update head_tags in the same run as the upload, or the browser rejects the file. Content-hashed filenames help inferred.
    • Liquid in asset URLs means listing the hash of every possible variant in integrity.
    • Assets must be on a public CDN. A private bucket behind a public CloudFront distribution satisfies this.
    • Partials are ignored on advanced screens. Move that logic into your bundle.
    • Don't target Universal widget internals in templates; class names and structure change between builds.
    • Classic “Customize Page” toggle: if you turn it on just to look at the code, turn it off again, or Auth0 stops rendering the default page.
    • Check the date on docs you read. 2025 copies still say “Early Access”; the CLI changed in 2026.
    • Server-side logic is separate. Rules, Hooks and Actions run in Auth0's pipeline regardless of which login UI you use inferred.

    8. Which level do you actually need?

    Answer four questions. The rule of thumb: changing how the page looks → standard Universal Login; changing how the screens are built or behave → ACUL.

    Do you need content or layout around the login box (header, footer, side panel), or different looks per app or organization?

    Do you need a few extra fields or bits of markup inside the login or signup form?

    Do you need your own design system or components, pixel-level control of the form, UI A/B tests or analytics, languages Auth0 doesn't ship, or custom UI logic?

    Can your team own a CDN, a CI/CD pipeline, SRI hash rotation and SDK upgrades for the long term?

    9. Things I couldn't verify

    • Which Auth0 plans include ACUL. Not stated in the docs or on the pricing page. Ask your Auth0 account team.
    • Official CSP guidance for the ACUL host page. (CSP = Content Security Policy, the header that limits which scripts a page may load.) No primary doc found.
    • Which screens, if any, ACUL can't render. GA claims parity with all Universal Login screens, but there's no current list of gaps. Check each production prompt against the SDK's screen list before cutting over.
    • An end-of-life date for Classic Login or Lock. None found; the docs only say “no longer receives updates”.
    • Whether Auth0 Forms works with Classic. It isn't in the comparison table. Probably not, since Classic gets no new features, but that's a guess.
    • The exact Management API path for custom text (likely under /api/v2/prompts/…/custom-text/…) wasn't opened. Check the API reference.
    • Inferences marked inferred above: ACUL accessibility and localization being your job; the ACUL SSO session matching Universal's; Classic's HTML editor not needing a custom domain; Rules/Hooks/Actions being independent of the login UI; apps that embed Lock being unaffected by the Classic/Universal toggle.

    Sources

    All read on 2026-10-09.

    Classic vs Universal, and login basics

    Universal Login customization

    ACUL