It has been a one-week challenge for me to create a neat implementation of auth0 authentication in our Next.js project.The @auth0/nextjs-auth0 SDK was published in September 2019, and there have been multiple nice tutorials online that runs through the work from the beginning, but they all happily finish after implementing one Profile page and do not offer further suggestions on good practices when multiple pages need the user authentication information.
After many trial-and-errors, I am now (hopefully) settled with an implementation utilising api endpoints and _app.js.
Here’s a review of what I have done.
Auth0 Registration
I first went to auth0.com and signed up for an account, which comes with an auth0 domain.
I then created a Regular Web App.
The fields that are required in the Settings are:
Allowed Callback URLs, Allowed Web Origins, and Allowed Logout URLs.
For local development and testing, I set:
- Allowed Callback URLs: http://localhost:3000/api/callback
- Allowed Web Origins: http://localhost:3000
- Allowed Logout URLs: http://localhost:3000
Once the application gets deployed, the deployed URLs can be added to corresponding fields, separated with other URLs by a comma(,).
Save the changes to settings, and now scroll up to note down three pieces of basic information:
- Domain (something.auth0.com),
- Client ID
- Client Secret
They will be needed for the auth0 configuration.
nextjs-auth0 Configuration
The installation and configuration of @auth0/nextjs-auth0 are fully explained on their github page. The only thing to be noted is that, while they put the clientId, clientSecret etc. straight into the /utils/auth0.js file, We should actually put them into the .env file for the security of credentials and for the flexibility among multiple deployments. I also learnt from my team mate that although the .env file is never committed, a .env.example file could be shared through git to present the required .env variables. dotenv module is installed to load environment variables from a .env file into process.env.
Creating API Routes
I copied and pasted the code from the @auth0/nextjs-auth0 github page for these api endpoints:
- login, to handle user log in. This will redirect user to auth0.
- callback, this gets called once user comes back from auth0. It will create a cookie that contains the user session, encrypted with the CookieSecret provided in the configuration step.
- logout, to handle user log out. This removes the cookie.
- me, to return the user profile to the client.
Accessing user info from client
Attempt 1: MyApp.getInitialProps
According to this tutorial, the Profile page accesses the user session in the getInitialProps function, which will set the user
to Profile’s props
:
1 | Profile.getInitialProps = async ({ req, res }) => { |
I went to learn more about getInitialProps
from the next.js documentation, and learnt that:
getInitialProps
enables server-side rendering in a page and allows you to do initial data population, it means sending the page with the data already populated from the server.getInitialProps
is an async function that can be added to any page as a static method.getInitialProps
will disable Automatic Static Optimization, which means with the blocking data requirement, Next.js will render the page withgetInitialProps
on-demand, per-request (meaning Server-Side Rendering), instead of pre-rendering the page to static HTML.getInitialProps
can not be used in children components, only in the default export of every page.
Therefore, for the current project implementation, there are two issues that stop me from using getInitialProps
for each of my pages:
getInitialProps
could only be called by a page component, and therefore components likeTopBar
, and page components that are exported in wrappers likewithLayout(Map)
could not usegetInitialProps
to fetch the data.- There will be many pages in our website protected by authentication, and including this piece of logic in every pages will not look good (In the end I still included the same logic in each page, but not as naked as this piece).
With these two problems in mind, my first idea was to make the user property accessible to all pages through customising _app.js
. I migrated the Profile.getInitialProps
to MyApp.getInitialProps
, and pass the user property to the Layout and Component:
1 | const { Component, pageProps, user } = this.props; |
For some time I thought this was a perfect solution, until I found out that I had to refresh each page to have the current login status. It looks like when I was browsing through Next.js Link
s, MyApp.getInitialProps
does not get invoked again until I refresh the page. So this is how the first attempt failed.
Attempt 2: React Hooks stuff
I will not go through this implementation in detail as it is largely based on this tutorial. Up to this moment I don’t know what React.useEffect
, React.useContext
and React.useState
do. I’ll come back to learn more about that later.
Attempt 3: componentDidMount
When I was talking about the first attempt with my team mates, they suggested that if I did not pass the user
object, but instead pass a getUser
function to be called by components, the user information will be forced to be loaded for each component. Based on this idea, here’s the new \pages\_app.js
:
1 | import '../css/global.css'; |
As can be seen, _app.js defines a function setUser
that returns an async
function. This async
function takes in a component, fetches the user information, and sets the user information into the component’s state. The setUser
function is passed on to both MainLayout
and Component
as a property.
Now where do I call the setUser
method?
According to react documentation, render
is not the place to call setState
:
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser.
Not constructor
either:
Typically, in React constructors are only used for two purposes:
Initializing local state by assigning an object to this.state.
Binding event handler methods to an instance.
You should not call setState() in the constructor(). Instead, if your component needs to use local state, assign the initial state to this.state directly in the constructor:
Down on the React Component lifecycle, now I have componentDidMount
:
componentDidMount() is invoked immediately after a component is mounted (inserted into the tree). Initialization that requires DOM nodes should go here.
You may call setState() immediately in componentDidMount(). It will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state.
So here’s my third version of implementation, using Dashboard
as an example:
1 | import React from 'react'; |
My next steps
- Passing the function around gives me a weird happy thrill. JavaScript is a fun language and I’ll learn more about it!
- What on hell is React Hook and what did the second version do with it remains a mystery to me at this moment. I’ll go further into that as soon as possible.
- The login handler of auth0 seems to provide the option to store state, so that user can come back after they login, instead of being redirected to the profile page all the time. It’s an extra nice thing to do when the main features of the website are achieved.
Coding on!