6/ NextAuth: creating a custom login page
Tags: nextauth, nextjs
Published: 2024-04-01 16:19:31

Up until now we've used the default login page `NextAuth` provides us. But we had 2 problems with this:

1. It's ugly and you can't customize it.
2. There is an issue with page reloads.

To solve (some of) these, we will be implementing a custom sign in page. To do this, we will have to:

- Create a sign in page (some sort of form component)
- Create a `NextAuth` handler.
- Do some `NextAuth` settings.

The code for this chapter is available on [github: branch: customlogin](https://github.com/peterlidee/NNAS/tree/customlogin).

## Sign in page

We create a new page:

```tsx
// frontend/src/app/(auth)/signin

import SignIn from '@/components/auth/signIn/SignIn';

export default function SignInPage() {
  return <SignIn />;
}
```

Note that we are using a route group `/(auth)` here. There will be more authentication pages like f.e. register and I want to group them together without this reflecting in the actual route. Meaning that our sign in page will be available at `http://localhost:3000/signin`.

We don't do anything else in here besides import a `<SignIn />` component that we are yet to make. I like to keep my `app` folder for routing only and move everything else to components.

## `<SignIn />` component

Inside the components folder, we create an auth folder to group all our auth components. Here is our `<SignIn />` component:

```jsx
// frontend/src/components/auth/signIn/SignIn.tsx

import Link from 'next/link';

export default function SignIn() {
  return (
    <div className='mx-auto my-8 p-8 max-w-lg bg-zinc-100 rounded-sm'>
      <h2 className='text-center text-2xl text-blue-400 mb-8 font-bold'>
        Sign in
      </h2>
      <div>
        <p className='mb-4'>
          Sign in to your account or{' '}
          <Link href='/register' className='underline'>
            create a new account.
          </Link>
        </p>
        [form]
        <div className='text-center relative my-8 after:content-[""] after:block after:w-full after:h-[1px] after:bg-zinc-300 after:relative after:-top-3 after:z-0'>
          <span className='bg-zinc-100 px-4 relative z-10 text-zinc-400'>
            or
          </span>
        </div>
        <button className='bg-white border border-zinc-300 py-1 rounded-md w-full text-zinc-700'>
          <span className='text-red-700 mr-2'>G</span> Sign in with Google
        </button>
      </div>
    </div>
  );
}
```

It looks like this:

![NextAuth custom signin page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vwop7kpdyu133bmk227a.png)

Couple of notes: I know it's not very stylish but the point is you can customize it yourself. Since we will be using credentials login later I already added some placeholders for a form and a link to a register page that doesn't exist yet. But that is all for later chapters.

## Sign in with Google button

We've already worked with the `signIn` function that `NextAuth` provides us. We used it in our `<SignInButton />` component. Calling `signIn()` just redirected us to the default `NextAuth` _sign in_ page.

But, there is a second use of the `signIn` function. When we call it and we pass it a provider name, it initiates the authentication flow for said provider:

```jsx
signIn('google');
```

As an optional second parameter it takes an options object. More on that later.

We call `signIn` on a button click, so we need a client component. So we move the _sign in with Google_ button into a separate client component, attach our `signIn` function and import it into the `<Login />` component:

```jsx
// frontend/src/components/signIn/GoogleSignInButton.tsx

'use client';

import { signIn } from 'next-auth/react';

export default function GoogleSignInButton() {
  return (
    <button
      className='bg-white border border-zinc-300 py-1 rounded-md w-full text-zinc-700'
      onClick={() => signIn('google')}
    >
      <span className='text-red-700 mr-2'>G</span> Sign in with Google
    </button>
  );
}
```

## Register our new sign in page

We are not ready to test yet. If we were to run our app now (not logged in) and click the navbar login button, it would still take us to the default `NextAuth` login page. We need to tell `NextAuth` we made a custom one. We do this in our `authOptions` object.

We add a new pages property to the `authOptions` object where we set the signin property to our sign in page:

```ts
// frontend/src/app/api/auth/[...nextauth]/authOptions.ts

export const authOptions: NextAuthOptions = {
  //...
  pages: {
    signIn: '/signin',
  },
};
```

Great! Run the app, sign out. Click _sign in_ and we are redirected to our custom sign in page. But, there was a full reload of the page. Also, our url now is: `http://localhost:3000/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F`. So it added a callbackUrl parameter back to our homepage.

Click _Sign in with Google_. What happens:

- Page reloads.
- We are **not** redirected.
- But, we are logged in!

![nextauth custom sign in page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1m46n19y62yiprjbl980.png)

So, it works but we have some problems to solve.

## NextAuth Redirect

The `signIn` function takes as a seconds parameter an options object. One of these options is `callbackUrl`.

```ts
signIn('google', { callbackUrl: 'someUrl' });
```

We've encountered this before. `NextAuth` automatically passes `callbackUrl` as a parameter in the url. To be honest, I expected to be redirected here with our current setup (without the options object). The [NextAuth docs](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl) state:

> The callbackUrl specifies to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.

Based on that I kinda expected to be redirect to the home page by default. It doesn't. So, let's update our `signIn` function, set callbackUrl to home and test it:

```jsx
// frontend/src/components/signIn/GoogleSignInButton.tsx

onClick={() => signIn('google', { callbackUrl: '/' })}
```

It works, we got redirected. A note here, `NextAuth` only accepts either relative paths or absolute urls on the same domain as the app. Always redirecting to the homepage is possible but in our case we want to redirect the user back to the previous page he or she was on. We update the function with the `useSearchParams` hook:

```jsx
// frontend/src/components/signIn/GoogleSignInButton.tsx

'use client';

import { signIn } from 'next-auth/react';
import { useSearchParams } from 'next/navigation';

export default function GoogleSignInButton() {
  const searchParams = useSearchParams();
  const callbackUrl = searchParams.get('callbackUrl') || '/';
  return (
    <button
      className='bg-white border border-zinc-300 py-1 rounded-md w-full text-zinc-700'
      onClick={() => signIn('google', { callbackUrl })}
    >
      <span className='text-red-700 mr-2'>G</span> Sign in with Google
    </button>
  );
}
```

We take callbackUrl from searchParams and simply pass it into `signIn` options. We also provide a fallback `'/'` to our home route. The callbackUrl from searchParams may be empty, f.e. when a user directly surfs to `localhost:3000/signin`.

To test this out, me made a little test page so we can sign in from there:

```jsx
// frontend/src/app/test/page.tsx

export default function page() {
  return <div>test page</div>;
}
```

We open up `localhost:3000/test`, logout, login and it redirects us to `/test` again. Great! I tried adding some searchParams (`/test?foo=bar`) and those were also passed down with no problem. Finally, I directly opened up `/signin`, logged out and in, and as expected, we were sent to the home page.

Note: there is another `signIn` option called `redirect` that takes a boolean as value. But this option is only valid for the email and credentials providers. We will in fact be using this later on.

## Guarded sign in page

One last detail. We are going to update our `<SignIn />` component. If our user is signed in, we will display a message saying "you're signed in" instead of showing the _sign in with Google_ button. Why? To not confuse the user.

```jsx
// frontend/src/components/signIn/SignIn.tsx

import Link from 'next/link';
import GoogleSignInButton from './GoogleSignInButton';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/authOptions';

export default async function SignIn() {
  const session = await getServerSession(authOptions);
  return (
    <div className='mx-auto my-8 p-8 max-w-lg bg-zinc-100 rounded-sm'>
      <h2 className='text-center text-2xl text-blue-400 mb-8 font-bold'>
        Sign in
      </h2>
      {session ? (
        <p className='text-center'>You are already signed in.</p>
      ) : (
        <div>...</div>
      )}
    </div>
  );
}
```

![nextauth already signed in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ike9qvagyt636pyfoefv.png)

## The reloads

Even with our custom login page, we still get the full page reloads when clicking:

- _sign in_ button
- _sign in with Google_ button
- _sign out_ button

Is this bad? Kinda yes. The entire app gets reloaded, you have to download everything again, the screen flickers while components are mounted, and it takes some time. This is not a nice flow. Besides these, not the end of the world. Can we fix them? I spend quite a lot of time on looking this up and trying things out.

The reload when clicking the `sign in with Google` is not fixable. But, here's the thing, every webapp where you sign in with Google reloads on signing in. It's normal. The _sign out_ button is also not _fixable_. There will be a reload but same as the previous button, this is normal behavior. Finally, the _sign in_ button we can and will _fix_ just below.

## Fix the reload issue when clicking the sign in button

You may have already guessed this one. Why don't we just link to our _sign in_ page instead of using the button that calls `signIn`. We can but it leads to some complications.

First complication, we lost the callbackUrl parameter. Earlier, when calling `signIn`, a searchParam `callbackUrl` was automatically added by `NextAuth`. When we remove the button and use a Link that callbackUrl is obviously no longer there. We will have to build that ourselves. So, we will replace `<SignInButton />` with a new component: `<SignInLink />`:

```jsx
// frontend/src/components/header/SignInLink.tsx

'use client';

import Link from 'next/link';
import useCallbackUrl from '@/hooks/useCallbackUrl';

export default function SignInLink() {
  const callbackUrl = useCallbackUrl();
  return (
    <Link
      href={`/signin?callbackUrl=${callbackUrl}`}
      className='bg-sky-400 rounded-md px-4 py-2'
    >
      sign in
    </Link>
  );
}
```

Remember, this is the _sign in_ link for our Navbar component. So, we need to link to `/signin`. But, to this, we need to add a searchParam `callbackUrl`. What is the value of this? Our current url + all of its own searchParams. Here is an example: if we are on page `/test?foo=bar`, we want our `<SignInLink />` to link to: `/signIn` + `?callbackUrl=/test?foo=bar`. To create the callback url value, we made a custom hook so we can reuse it. This is the hook:

```ts
// frontend/src/hooks/useCallbackUrl.ts

import { usePathname, useSearchParams } from 'next/navigation';

export default function useCallbackUrl() {
  const pathname = usePathname();
  const params = useSearchParams().toString();
  // if there are no params, don't add the ?
  const callbackUrl = `${pathname}${params ? '?' : ''}${params}`;
  return callbackUrl;
}
```

Some notes:

- We use a relative url.
- There is no need to `encodeURIComponent` because `Next` covers this for us in the `usePathname` and `useSearchParams` hooks.
- There is a flaw in this. If we were to go from `/test` via our new link to the sign in page, we would have following route: `/signin?callbackUrl=/test`. If we click on the navbar _sign in_ button again, this will be our route: `/signin?callbackUrl=/signin?callbackUrl=%2Ftest`. Our callbackUrl becomes the current page: `/signin` + callbackUrl parameter. It's like a feedback loop. Funnily enough, the `NextAuth` `signIn` function has the same problem. So, I'm going to ignore this.

On the upside: we can run this and it works! Going to the sign in page no longer reloads the page. Testing this further, the callbackUrl works. We are correctly redirected after signing in (but with a full page reload - as expected).

## Conclusion

We made a custom login page. This is an absolute necessity and in itself it's not very difficult. We also learned to use the `signIn` function with a provider and an options argument. This allows us to start the authentication flow with `NextAuth` manually. The options object includes the callbackUrl searchParam.

Finally, we tackled part of the hard page reloads in our _sign in_ and _sign out_ flow. We only managed to _correct_ the navigation to the sign in page. This created a complication in that we had manually pass the callbackUrl parameter to the url.

Is it worth it? The custom login is a must! The hard reloads for me were disruptive. I like that we at least solved one of them. On top of that, we solved it with a minimal amount of code. This chapter is quite long but in the end, the code is manageable and the solution is good.

In the next chapter we will start integrating with `Strapi`.

---

If you want to support my writing, you can [donate with paypal](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).