Using React to Customize Your Auth0 User Experience
As our implementation with Auth0’s branding and styling grew more complex, our engineering team looked for a solution to simplify our implementation and accelerate our development. Our goal was to leverage existing tools and frameworks we were already using. As a result, we leveraged React for our Auth0 Universal Login pages.
In this blog, we’ll take a look at how we implemented React with Auth0’s Universal Login, but check out our repo with a working code example to learn more.
Challenges while Scaling Universal Login
Auth0 offers extensive, out-of-the-box styling options for their Universal Login. Their built-in editor is capable of styling most aspects of Auth0’s default sign-up and sign-in pages; however, you may require more extensive customization options such as animations, transitions, and layouts. Auth0 provides a way to accomplish this by customizing all of the HTML, CSS, and JavaScript via their templates.
This solution presented our team with some challenges as we began to scale:
- Hard to Organize - Our implementation quickly grew to over several thousands of lines of HTML, CSS, and JavaScript. It became difficult to organize the page content and state.
- Context Switching - All of our other frontend applications were written in React, so our team members had to context switch when working with Universal Login.
- Vanilla JavaScript - We are a TypeScript shop, so we lost most of our syntactic sugar when switching back to JavaScript.
- Redo Theme - Our team had implemented custom styling with Material UI components, forcing us to re-implement all of our custom stylings.
- Hard to Test - We no longer could use React Testing Library and Cypress for unit and acceptance testing. This created a slow feedback loop to verify sign-up and sign-in behavior until we deployed the latest changes.
Due to these challenges with Universal Login, our engineers avoided working on the changes. This was problematic, so we looked for a solution.
How to manage Auth0 Universal Login with React
Since our team was already using React for all of our existing applications, we decided to leverage React to manage our Universal Login experience. To do this, you will need to perform the following four steps:
- Create your form components
- Add Auth0 Context Provider
- Add module to login.html
- Build the static page
Let’s jump in.
1. Create your form components
With React, we can break up the HTML in the Universal Login page into three components: 1) Sign-up, 2) Sign-in, and 3) Reset password. We benefit from using out-of-the-box components, such as Material UI’s TextField component:
<TextField
id="email"
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
sx=
type="email"
value={email}
/>
In this example, we can set things, such as onChange events, set value from state, and set styling on our component.
2. Add Auth0 Context Provider
This step adds most of the magic to the solution.
We need a way to manage state within our React application. The default Universal Login template injects and configures an instance of the `WebAuth` client. We need to replicate this behavior.
First, we need to import the auth0-js dependency to give us access to WebAuth and a way to initialize the WebAuth client:
import type { DbSignUpOptions, WebAuth } from 'auth0-js';
const createAuthClient = (): WebAuth => {
const leeway = window.auth0Config.internalOptions.leeway;
if (leeway) {
const convertedLeeway = Number.parseInt(leeway);
if (!Number.isNaN(convertedLeeway)) {
window.auth0Config.internalOptions.leeway = convertedLeeway;
}
}
const params = {
overrides: {
__tenant: window.auth0Config.auth0Tenant,
__token_issuer: window.auth0Config.authorizationServer.issuer,
},
domain: window.auth0Config.auth0Domain,
clientID: window.auth0Config.clientID,
redirectUri: window.auth0Config.callbackURL,
responseType: 'code',
...window.auth0Config.internalOptions,
};
return new WebAuth(params);
};
This code is copied directly from the default Universal Login template. Auth0 will automatically inject the auth0Config property into the page when it loads, giving us access to client, tenant, and callback settings.
Next, we need to add a context that will hold the state of our Universal Login application:
type Mode = 'signIn' | 'signUp' | 'resetPassword';
type Provider = 'google-oauth2' | 'github';
type Login = (attrs: { username: string; password: string }) => void;
type LoginWithSocial = (provider: Provider) => (e: SyntheticEvent) => void;
type SignUp = (attrs: { email: string; password: string; givenName: string; familyName: string; }) => void;
type ChangePassword = ({ email }) => void;
type WebAuthValue = {
webAuth: WebAuth;
login: Login;
loginWithSocial: LoginWithSocial;
signUp: SignUp;
changePassword: ChangePassword;
mode: Mode;
setMode: (mode: Mode) => void;
};
const WebAuthContext = createContext<WebAuthValue>(null);
This will hold the state of our application including ability to login, social login, password reset, and sign-up. We also are adding a custom Mode state to toggle between the different views of our application.
Now we need to create our WebAuthProvider and initialize the Auth0 WebAuth instance:
const WebAuthProvider: FC = ({ children }) => {
const [webAuth, setWebAuth] = useState<WebAuth>();
const [mode, setModeDispatch] = useState<Mode>(getInitialMode());
useEffect(() => {
if (!webAuth) {
setWebAuth(createAuthClient());
}
}, [webAuth]);
};
Notice this is calling createAuthClient which will initialize the WebAuth client from the injected Auth0 settings.
Within our WebAuthProvider we can implement the rest of our methods from our state. We can copy most of this JavaScript directly from the default Universal Login template.
Here is login for reference:
const login: Login = ({ username, password }) => {
return webAuth?.login(
{
realm: 'Username-Password-Authentication',
username,
password,
}
);
};
Finally, we need to export our context:
const WebAuthProvider: FC = ({ children }) => {
// Rest of WebAuthProvider
const value: WebAuthValue = {
webAuth,
login,
loginWithSocial,
signUp,
changePassword,
mode,
setMode,
};
return (
<WebAuthContext.Provider value={value}>{children}</WebAuthContext.Provider>
);
};
const useWebAuth = (): WebAuthValue => useContext(WebAuthContext);
export { WebAuthProvider, useWebAuth };
With our WebAuthProvider complete, we just need to wrap our App with it to make the state available to all of our components:
const App = (): JSX.Element => (
<WebAuthProvider> <ThemeProvider theme={theme}> <CssBaseline /> <Content /> </ThemeProvider> </WebAuthProvider>
);
At this point, we can change the mode and call all methods on the WebAuth client from any of our form components. You can see the completed WebAuthProvide here.
3. Add module to login.html
We have our form components and our WebAuthProvider, but now we need to wire it up and build our application as a module. Inside our login.html page, we simply replace all of the JavaScript, HTML, and CSS with a simple script tag:
<div id="app"></div>
<script type="module"> import "./index.tsx" </script>
When our React application is built, it will bundle and minify the application as a module and inject the JavaScript directly into the HTML file. This will include the Auth0 dependency, React framework, and other UI components such as Material UI. You can see the completed page here.
3. Build the static page
With the React application done, we need to build and deliver it to Auth0. To do this, we utilize Parcel and build it with the command:
parcel build
This will compile our application.The resulting login.html will hold a single file containing our compiled React application.
To deploy the application, it is highly recommended you look at Auth0’s deployment CLI. We can copy the resulting login.html file to pages directory and easily deploy it to our Auth0 tenant:
cp dist/login.html ../infrastructure/pages/login.html
a0deploy import --input_file ../infrastructure
Conclusion
Replacing the default Universal Login template with React gives our team numerous benefits:
- Reduces context switching - Our team can now work exclusively with React
- Breaks up the monolith form - We separated our application into individual components
- Faster page load times - Since our page was compiled, we reduced the payload size
- Faster feedback loop - Our engineering team gained the benefits of testing and hot reloading
- Reuse components and styles - We could copy components and our company theme over with minimal changes
More importantly, our team now no longer avoids working with the Universal Login forms. We gain all of the efficiencies of reusing our existing tools, frameworks, and workflows to build and deliver our login experience.