Auth0: Classic Login vs Universal Login vs ACUL
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
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.
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.
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 secretcode_verifierso a stolencodeis 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 oftenant.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.
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.
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).
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
- 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. - Text customization. Change any page's wording in the dashboard or Management API, per supported language.
- 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. - 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. - 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.
Handy behaviors
screen_hint=signupon/authorizeopens signup directly;prompt=loginforces the login page.login_hintpre-fills the email or username.- Going straight to
/loginerrors 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
<head> points at your files.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:
| Field | What it does |
|---|---|
head_tags | Required 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_configuration | Required 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. |
filters | Only 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_template | Render the ACUL screen inside your tenant's Liquid page template. |
default_head_tags_disabled | Drop 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"
}
}
]
}
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
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}/renderingfor one screen,GET/PATCH /api/v2/prompts/renderingin bulk. - Infrastructure as code: Terraform resource
auth0_prompt_screen_renderer, or the Deploy CLIpromptsresource.
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.
6. Side by side
| Dimension | Classic Login | Universal Login (theme / template / partials) | ACUL (advanced screens) |
|---|---|---|---|
| Who hosts the page | Auth0 (redirect) | Auth0 (redirect) | Auth0 serves a host page; your assets come from your CDN |
| Who draws the form | Lock / auth0.js / MFA widget in the browser | Auth0, as lightweight server-side HTML; JS not required | Your React or vanilla JS bundle |
| Customization depth | Logo and colors, then full HTML of Login, Password Reset and MFA pages only | Theme → text → Liquid template → partials | Full UI per screen; any framework |
| CSS | Inline only | Theme settings; your CSS around the widget (don't target widget classes) | Your stylesheet, SRI-pinned |
| Maintenance & security | Once customized: yours (SDK bumps, XSS) | Auth0 ships updates automatically | Auth0 runs the flow; you own CDN, CI/CD, SRI hashes, escaping untrusted data, SDK upgrades |
| New features | “No longer receives updates” | Ongoing development | Built on Universal Login; new screens need your UI |
| Passkeys / One Tap / WebAuthn / Organizations | No | Yes | Yes (SDK has passkey, WebAuthn and organization screens) |
| MFA | MFA widget page; no Email/Voice MFA; Duo supported | Factor picker, Email/Voice MFA, Actions-driven MFA; Duo only if it's the sole factor | Same flow as Universal; you build the MFA screens |
| Localization | Login page only | Full localization, customizable text on all pages | Your job, using screen.texts and tenant.enabled_locales or your own translations |
| Accessibility | Not WCAG compliant | WCAG 2.2 AA + EN 301 549 | Your job inferred |
| Performance | Heavier JS bundle (“Lightweight HTML: No”) | “Lightweight HTML”, “simpler, faster experience” | Depends on your bundle; import per screen |
| SSO | Auth0 session; SSO with social dev keys “not supported” | Auth0 session cookie on tenant / custom domain; silent auth works | Same Auth0 session as Universal inferred |
| Only here | Email magic link, Kerberos for AD/LDAP | Passkeys, 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 inferred | For templates and partials | Yes |
| Partials | n/a | Yes (login and signup screens) | Ignored on advanced screens |
| How to turn on | Branding → Universal Login → Advanced Options → choose experience | Same toggle | Per 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
- 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. - Check Classic-only features. Email magic link and Kerberos don't exist in Universal. Duo must be the only MFA factor.
- Set up a custom domain on the same parent domain as your apps. Hop 2 needs it anyway.
- 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
/logindirectly errors without one. - Turn off “Customize Login Page” (the signup-prompt customization docs require it off), then rebuild with theme, text, template and partials.
- 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
- Prerequisites: custom domain, first-party app, Identifier First enabled, separate dev/test/prod tenants, CDN + CI/CD, Node.js 22, Auth0 CLI.
- Scaffold:
auth0 acul init "App",auth0 acul screen add …, or start from the official samples / React boilerplate. - Develop locally:
auth0 acul devwith mock context (JSON inpublic/screens/{prompt}/{screen}/, registered inpublic/manifest.json). Switch to--connectedlate, since every reload restarts the flow. - 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.
- 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. - 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_tagsin 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 vs Classic Login
- Classic Login experience
- Universal Login experience
- Auth0 Universal Login
- Universal vs embedded login
- Cross-origin authentication
- Customize Classic Login pages
- Lock library
- Authorization code flow with PKCE
- Authentication API: authorize with PKCE
Universal Login customization
- Customize Universal Login
- Customize Universal Login page templates
- Customize signup and login prompts (partials)
- Customize text elements
ACUL
- Advanced Customizations for Universal Login (stale mirror with Early Access banner)
- Configuration reference and configure overview
- Quickstart, development workflow, deployment workflow
- SDK reference
- Auth0 changelog: GA entry v202549, v202505 (Early Access), v202508 (MFA screens)
- auth0/universal-login (SDK monorepo README), React SDK README, JS SDK FAQ
- ACUL samples, React boilerplate, Universal Login Context Inspector
- Auth0 CLI: universal-login docs (deprecations), auth0 acul
- Terraform auth0_prompt_screen_renderer
- Auth0 pricing (checked; no mention of ACUL)