Next.js 12.1.x NextAuth Custom Login with User Credentials

Overview

If you plan to authenticate users for your Next.js application, then NextAuth.js is an excellent option for authentication. NextAuth.js also has a bit of a learning curve. A single authentication solution that gives users multiple options for authenticating, such as Open ID, Custom Credentials, Auth0, Okta, etc., makes it one of the best authentication libraries for Next.js. Your other option is to use something like BlitzBlitz is a batteries-included framework built on top of Next.js. The following article will go over how to build a custom Login Page that accepts user credentials and a couple of Hard Coded users for Test & Development. This post will not cover how to setup up NextAuth.js for providers like Google or Twitter since there are several articles with suitable examples.

Important! Changes for Middleware in Next.js Version 12.2.x

The example below was written for Next.js 12.1.x. For Next.js 12.2.x and above, the rules for _middleware.tshave changed. _middleware.ts is now middleware.ts without the underscore and it must be placed at the root of the application. If it is anywhere under the ‘/pages folder, you will get a nested middleware error on build time. Otherwise, run the following npm command on your Next.js application to keep the example below from breaking.

npm install next@12.1.6

OR

Make sure the ‘\package.json’ file has the following version.

"next": "12.1.6",

Get the Current Version of Next.js

Use the following CLI command to get the current version of Next.js,

npx next -v

The recorded video of the solution from the GitHub repo.

Related Article

How to Run the Example From GitHub

API

Change into the API directory and start the app. The API is configured to run with ‘npm start

cd API
npm start

Next.js App

Change into the ‘nextjs‘ directory and run the application with ‘npm run dev

cd nextjs
npm run dev

Prerequisites

If you are not using the source files above, get the Next.js template from MUI. You might have to clone the entire repository to get it, but it is worth the effort. The template from MUI has everything configured out of the box and is ready to use. If you are installing Material UI yourself, it can be challenging unless you are comfortable with Next.js.

https://github.com/mui/material-ui/tree/master/examples/nextjs

NextAuth.js library.

npm install next-auth

typescript and @types/node, is required to run this example.

npm install --save-dev typescript @types/node

ovider

From “/pages/_app.js” you will need to wrap the main Component with NextAuth.js‘s <SessionProvider/>.

import { SessionProvider } from "next-auth/react"
...
export default function MyApp(props) {

  return (
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>

      <SessionProvider session={pageProps.session} refetchInterval={0}>                      
          <Component {...pageProps} />                        
      </SessionProvider>
  );
}

Credential Provider.

NextAuth.js has a long list of providers you can view from here. CredentialProvider allows for custom username and password stored in your own database to be used, in addition to providers like Google, Twitter, etc.

Below is the source code for ‘/pages/api/auth/[…nextauth].ts’. There are three Credential Providers defined. First one take an Email and Password from the Login Page and uses hard coded validation. You can replace the hard coded validation with an API call. Example with an API call in the next code block below.
Second and third Credential Providers are hard coded users. This is helpful for testing multiple roles like Admin and Registered Users.

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

export default NextAuth({

  providers: [
    CredentialsProvider({
      
      name: 'Email and Password',
      credentials: {
        email: { label: 'Email', type: 'text'},
        password: { label: 'Password', type: 'password' }
      },
      // This is were you can put your own external API call to validate Email and Password
      authorize: async (credentials) => {
        if (credentials.email === 'user@email.com' && credentials.password === '123') {
          return { id: 11, name: 'User', email: 'user@email.com'} 
        }
            
        return null;
      
      }
    }),

    CredentialsProvider({
      id: "domain-login",
      name: "User I Credentials",
      
      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        const user = { id: 99, name: "Jane Smith", email: "janesmith@example.com" }
          
        if (user) {
          return user
        } else {
          return null  
        }
      }
    }),
    CredentialsProvider({
      id: "user-login",
      name: "User II Credentials",

      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: {  label: "Password", type: "password" }
      },
      async authorize(credentials, req) {

        const user = { id: 26, name: "David Smith", email: "davesmith@example.com" }
  
        if (user) {
          return user
        } else {
          return null
  
        }
      }
    }),
  ],
  theme: {
    colorScheme: "dark",
  },
  pages: {
    signIn: '/login',
//    signOut: '/signout',

  },
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      return true
    },
    async jwt({ token }) {
      
      token.userRole = "regusr"
      return token
    },
  },

})

Real Example of Custom Authentication

Below is a code snippet from an application that calls an external ASP.NET Web API to verify a email and password.
This is not a part of this example but it is commented out so you can just copy and paste this in if you are storing users in a database.

   CredentialsProvider({      
      name: 'Email and Password',
      credentials: {
        email: { label: 'Email', type: 'text'},
        password: { label: 'Password', type: 'password' }
      },
      authorize: async (credentials) => {
   
          const payload = {
            email: credentials.email,
            password: credentials.password,
          };

          const url = process.env.NEXT_API_DOMAIN + `/api/auth/login`
             
          const res = await fetch(url, {
            method: 'POST',
            body: JSON.stringify(payload),
            headers: { "Content-Type": "application/json" }
          })
          
          const user = await res.json()
                  
          
          if (res.ok && user) {
            return user;
          }
        
        
          return null;
      
      }
    }),

Custom Login Page

NextAuth.js  has an option for custom login pages. This allows developers to have Email and Password fields for local credentials with list of Providers at the bottom. The default credentials are ‘user@email.com’ and ‘123’ for the email password combination. In the lower have are a list of all the available providers that are defined in “/page/api/auth/[..nextauth].ts“. There are three providers for this example. 1st one is for user entered credentials. The last two are hard coded users which would be ideal for testing different roles in development. Each provider requires a unique ID since they are all Credentials. Other provider such as Google, Twitter, etc. have their own identifier and require an ID and Secret key.

Source code for login page

Source code for login page “/pages/login/index.js“.

import {useState, useEffect} from 'react';
import { signIn, getSession, getProviders } from "next-auth/react";
import Head from 'next/head';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import FormControl from '@mui/material/FormControl';
import Checkbox from '@mui/material/Checkbox';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import IconButton from '@mui/material/IconButton';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Link from '@mui/material/Link';
import Grid from '@mui/material/Grid';
import FormControlLabel from '@mui/material/FormControlLabel';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import { useRouter } from 'next/router';
import { Stack } from '@mui/material';
import { getToken } from "next-auth/jwt"
import { blue } from '@mui/material/colors';

export default function Signin({ providers, loginError }) { 
  
  const router = useRouter();
  
 
  const [values, setValues] = useState({
    email: 'user@email.com',
    password: '123',    
    showPassword: false,
    rememberMe: false
  });
      
  const [showAlert, setShowAlert] = useState(false);


  useEffect(async () => {
    const session = await getSession()        
    if (session) {
      router.push('/profile')
    }     
  }, [])
  

  const handleRememberMe = (prop) => (event) => {
    setValues({ ...values, rememberMe: !rememberMe });
  };

  const handleChange = (prop) => (event) => {
    setValues({ ...values, [prop]: event.target.value });
  };

  const handleClickShowPassword = (e) => {
    e.preventDefault();
    setValues({ ...values, showPassword: !values.showPassword });
  };

  const handleMouseDownPassword = (event) => {
    event.preventDefault();
  };

 
  const handleLoginUser = async (e) => {    
    e.preventDefault();
    await signIn("credentials", {
      redirect: true,
      email: values.email,
      password: values.password
    });
 
  }

  return (
    <>
    <Head>
    <link rel="shortcut icon" href="/favicon.ico" />
    <title>Full Stack Soup - Next-Auth Demo</title>
    </Head>

    <Container component="main" maxWidth="xs">
      <Grid container align="center">
      <Grid item xs={12}>        
          <Avatar  sx={{ bgcolor: blue[200] }}>
            <LockOutlinedIcon />
          </Avatar>
        </Grid>
        <Grid item xs={12}>
          <Typography component="h1" variant="h5" sx={{mb: 2}}>
            Sign in
          </Typography>
        </Grid>
        {showAlert &&
        <Grid item xs={12}>
          <Typography component="h1" variant="h5" sx={{mb: 2}}>
            <Alert severity="error" onClose={() => {setShowAlert(false)}}>
              <AlertTitle>Warning</AlertTitle>
              Incorrect Email and Password combination
            </Alert>
          </Typography>
        </Grid>
        }

        <Grid item xs={12}>
            <form  noValidate>
            <Grid container spacing={2}>
                
              <Grid item xs={12}>
                <TextField
                    variant="outlined"
                    required
                    tabIndex="1"
                    fullWidth
                    placeholder='user@email.com'
                    onChange={handleChange('email')}
                    value={values.email}
                    id="email"
                    label="Email Address"
                    name="email"
                    autoComplete="email"
                  />
              </Grid>
              <Grid item xs={12}>
                <FormControl variant="outlined" fullWidth>
                    <InputLabel htmlFor="outlined-adornment-password">Password</InputLabel>
                    <OutlinedInput
                        tabIndex="2"
                        required
                        label="Password"
                        id="outlined-adornment-password"
                        type={values.showPassword ? 'text' : 'password'}
                        value={values.password}
                        placeholder='123'
                        onChange={handleChange('password')}
                        endAdornment={
                        <InputAdornment position="end">
                            <IconButton
                              aria-label="toggle password visibility"
                              onClick={handleClickShowPassword}
                              onMouseDown={handleMouseDownPassword}
                              edge="end"
                              size="large">
                            {values.showPassword ? <Visibility /> : <VisibilityOff />}
                            </IconButton>
                        </InputAdornment>
                        }
                        labelWidth={70}
                    />
                  </FormControl>
                
              </Grid>
  
              <Grid item xs={12} align="left">
                  <FormControlLabel
                    control={<Checkbox  color="primary" onChange={handleRememberMe} value={values.rememberMe}/>}
                    label="Remember me"
              />
              </Grid>

              <Grid item xs={12}>

                <Button
                  type="button"
                  tabIndex="3"
                  fullWidth
                  size="large"
                  variant="contained"
                  color="primary"
                  disabled={(values.email.length === 0 || values.password.length === 0 )}
                  onClick={handleLoginUser}
                 
                >
                  Sign In
                </Button>
            </Grid>         
        
            <Grid item xs={12}>
              <Grid container>
                <Grid item xs align="left">
                  <Link href="\auth\forgot-password" variant="body2">
                    Forgot password?
                  </Link>
                </Grid>
                <Grid item align="right">
                  <Link href="\register" variant="body2">
                    {"Don't have an account? Sign Up"}
                  </Link>
                </Grid>
              </Grid>
                
            </Grid>  
                
            </Grid>         
          </form>
        </Grid>         
         
        <Grid item xs={12}>
          <form  noValidate>        
            <Stack spacing={2} sx={{mt: 8}}>
              {Object.values(providers).map((provider) => (
                <>
                {provider.name !== 'Email and Password' &&
                <div key={provider.name}>                  
                  <Button size="large" variant="outlined" fullWidth onClick={() => signIn(provider.id)} >
                    Sign in with {provider.name}
                  </Button>
                </div>
                }
                </>
              ))}
            </Stack>           
          </form>
        </Grid> 
      </Grid>

    </Container>
    </>
  );
}

export async function getServerSideProps(context) {
  const { query, req, res } = context;
  var error = ''
  if(Boolean(query.error)) {
    error = query.error
  }
  
  try {    
    const secret = process.env.NEXTAUTH_SECRET
    const token = await getToken({ req, secret })   
    
    return { props: { providers: await getProviders(), loginError: error } };
  } catch (e) {
    return { props: { providers: await getProviders(), loginError: error } };
  }
  
}

Get Providers from NextAuth.js

All of the providers are loaded on the server side under getServerSideProps()

export async function getServerSideProps(context) {
  const { query, req, res } = context;
  var error = ''
  if(Boolean(query.error)) {
    error = query.error
  }
  
  try {    
    const secret = process.env.NEXTAUTH_SECRET
    const token = await getToken({ req, secret })   
    
    return { props: { providers: await getProviders(), loginError: error } };
  } catch (e) {
    return { props: { providers: await getProviders(), loginError: error } };
  }
  
}

List Providers

The buttons for the providers are generated with the code below.

 <Stack spacing={2} sx={{mt: 8}}>
    {Object.values(providers).map((provider) => (
      <>
      {provider.name !== 'Email and Password' &&
      <div key={provider.name}>                  
        <Button size="large" variant="outlined" fullWidth onClick={() => signIn(provider.id)} >
          Sign in with {provider.name}
        </Button>
      </div>
      }
      </>
    ))}
  </Stack>              

Profile Page – (Optional)

The Profile page is a secure that displays user information from the secure session.

You can try to open the page from the toolbar before you authenticate.

If the user is not authenticated it will show Access Denied

Source for the Profile Page

import React from 'react';
import PropTypes from 'prop-types';
import { createTheme } from '@mui/material/styles';
import { purple } from '@mui/material/colors';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { Container } from '@mui/material';
import Box from '@mui/material/Box';
import moment from 'moment';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import { getToken } from "next-auth/jwt"
import { useSession, getSession } from "next-auth/react"
import { useRouter } from 'next/router';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import KeyOutlinedIcon from '@mui/icons-material/KeyOutlined';
import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined';
import TokenOutlinedIcon from '@mui/icons-material/TokenOutlined';
import PermIdentityOutlinedIcon from '@mui/icons-material/PermIdentityOutlined';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';


export default function UserProfilePage({data}) {
    
  const router = useRouter();
  const { data: session, status } = useSession()
    
  const [ userEmail, setUserEmail ] = React.useState('');
  const [ userName, setUserName ] = React.useState('');
  const [ tokenExpiration, setTokenExpiration ] = React.useState('');

  React.useEffect(async () => {            
              
     if(!Boolean(session)) {
      //router.push('/login')                          
     }   
     else {   
      setUserEmail(session.user.email)
      setUserName(session.user.name)
      setTokenExpiration(session.expires)
       console.log(session)
     }                
  
  }, [])
  
  // R E N D E R   P A G E

  if (session) {
    return (
      <>
      <Container maxWidth="lg" align="center">
            
        <Grid container spacing={2}>
          <Grid item xs={4}>
            <Card sx={{ maxWidth: 305 }}>           
              
                <CardContent>
                <Typography variant="h4" sx={{mb: 5}}>
                  User Profile
                </Typography>

                <ListItemButton>
                  <ListItemIcon>
                  <PermIdentityOutlinedIcon/>
                  </ListItemIcon>
                  <ListItemText primary={userName} />
                </ListItemButton>

                <ListItemButton>
                  <ListItemIcon>
                  <EmailOutlinedIcon/>
                  </ListItemIcon>
                  <ListItemText primary={userEmail} />
                </ListItemButton>

                <ListItemButton>
                  <ListItemIcon>
                  <TokenOutlinedIcon/>
                  </ListItemIcon>
                  <ListItemText primary={moment(tokenExpiration).format('MM/DD/YYYY hh:mm')} />
                </ListItemButton>

      
                </CardContent>
              </Card>

          </Grid>
          <Grid item xs={4}>
            <Card sx={{ maxWidth: 305 }}>              
                <CardContent>
                  <Typography variant="h4" >
                  Password
                  </Typography>

                  <KeyOutlinedIcon sx={{fontSize: '78px'}}/>
                    
                    <Typography variant="body2" align="left">
                    Make your password stronger, or change it if someone else knows it.
                    </Typography>
                </CardContent>
              </Card>
          </Grid>
          <Grid item xs={4}>
            <Card sx={{ maxWidth: 305 }}>           
              <CardContent>
                <Typography variant="h4" >
                  Settings
                </Typography>

                <SettingsOutlinedIcon sx={{fontSize: '78px'}}/>
                  
                <Typography variant="body2" align="left">
                Personalize your account settings and see how your data is used.
                </Typography>
                
              </CardContent>
            </Card>

          </Grid>
          <Grid item xs={8}>

          </Grid>
        </Grid>
                
        </Container>
      </>
    );
  }
  else return <Typography variant="h3" align="center"> Access Denied </Typography>
    
}

export async function getServerSideProps(context) {
  const { req, res } = context;

  try {    
    const secret = process.env.NEXTAUTH_SECRET
    const token = await getToken({ req, secret })   
    setTimeout(() => {
      console.log('token', token)  
    }, 1000);
    
  } catch (e) {
    console.log('getServerSideProps Token Error')
  }
  
  
  return {
    props: {      
      data: []
    },
  };
}
  

Sign Out Page – Optional

Default Sign Out Page

NextAuth.js has an option for adding a custom sign out page. The default page will be similar to what is below, but you can customize it further with a company logo etc.

To sign out simply use something similar to the code below.

import { signOut } from "next-auth/react"

''' 

const handleSignOut = async (e) => {    

  await signOut();
}

...

<Button onClick={handleSignOut}> Sign Out </Button>

Custom Sign Out Page

NextAuth.js provides a way to have a custom sign out page.

You can define custom pages In the “/page/api/auth/[..nextauth].ts” file under pages. The code below is taken from NextAuth.js website here.

 pages: {
    signIn: '/auth/signin',
    signOut: '/auth/signout',
    error: '/auth/error', // Error code passed in query string as ?error=
    verifyRequest: '/auth/verify-request', // (used for check email message)
    newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
  }

Source for the Custom Sign Out Page

Source code for login page “/pages/signout/index.js“.

import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import KeyIcon from '@mui/icons-material/Key';
import { getSession, signOut } from "next-auth/react"
import { useRouter } from 'next/router';

const theme = createTheme();

export default function SignOut() {

  const router = useRouter();
  
  React.useEffect(async () => {
    const session = await getSession()        
    
    if (!session) {
      router.push('/')

    }       
  }, [])

  const handleLogoutUser = async (e) => {    
    
    await signOut();
    router.push('/')
  }

  
  return (
    <ThemeProvider theme={theme}>
      <Grid container component="main" sx={{ height: '70vh' }}>
        <CssBaseline />
       
        <Grid item xs={12} sm={12} md={12}  square>
          <Box
            sx={{
              my: 8,
              mx: 4,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
            }}
          >
            <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
              <KeyIcon />
            </Avatar>
            <Typography component="h1" variant="h4">
              Sign Out
            </Typography>
            <Box component="form" noValidate sx={{ mt: 4 }}>
            <Typography component="h5" variant="h6">
              Are you sure you want to sign out?
            </Typography>
              <Button
                type="submit"
                fullWidth
                size="large"
                color="error"
                variant="contained"                
                sx={{ mt: 3, mb: 2 }}
                onClick={() => handleLogoutUser()}

              >
                Sign out
              </Button>
            </Box>
          </Box>
        </Grid>
      </Grid>
    </ThemeProvider>
  );
}

NextAuth.js Checklist before Deployment.

Make sure you have the following:

Have the following environment variable defined in env.production.

NEXTAUTH_URL and NEXTAUTH_SECRET keys must be defined.

NEXTAUTH_URL=https://YOUR_DOMAIN
NEXTAUTH_SECRET=YOUR_SECRET_KEY_HERE

WARNING! if you installed NextAuth.js but haven’t enabled any of the features yet, it will throw an error while building the application, so put the domain and something value for the secret.

https://randomkeygen.com/ is a website that generates random characters.