Published: August 30 2021

Next.js - Redirect to Login Page if Unauthenticated

Tutorial built with Next.js 11.1.0

This is a quick post to show how to redirect users to the login page in a Next.js front-end (React) app. The redirect applies to users that attempt to access a secure/restricted page when they are not logged in.

The below components are part of a Next.js basic authentication tutorial I posted recently that includes a live demo, so to see the code running check out Next.js 11 - Basic HTTP Authentication Tutorial with Example App.

 

Next.js Route Guard Component

Path: /components/RouteGuard.jsx

The route guard component contains the client-side authorization logic for the Next.js app, it wraps the current page component in the Next.js app component.

Client-side authorization is implemented in the authCheck() function which is executed on initial app load and on each route change. If you try to access a secure page (e.g. the home page /) without logging in, the page contents won't be displayed and you'll be redirected to the /login page. The returnUrl is included in the redirect query parameters so the login page can redirect the user back to the page they originally requested after successful login.

The authorized state property is used to prevent the brief display of secure pages before the redirect because I couldn't find a clean way to cancel a route change using the Next.js routeChangeStart event and then redirecting to a new page.

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';

import { userService } from 'services';

export { RouteGuard };

function RouteGuard({ children }) {
    const router = useRouter();
    const [authorized, setAuthorized] = useState(false);

    useEffect(() => {
        // on initial load - run auth check 
        authCheck(router.asPath);

        // on route change start - hide page content by setting authorized to false  
        const hideContent = () => setAuthorized(false);
        router.events.on('routeChangeStart', hideContent);

        // on route change complete - run auth check 
        router.events.on('routeChangeComplete', authCheck)

        // unsubscribe from events in useEffect return function
        return () => {
            router.events.off('routeChangeStart', hideContent);
            router.events.off('routeChangeComplete', authCheck);
        }

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    function authCheck(url) {
        // redirect to login page if accessing a private page and not logged in 
        const publicPaths = ['/login'];
        const path = url.split('?')[0];
        if (!userService.userValue && !publicPaths.includes(path)) {
            setAuthorized(false);
            router.push({
                pathname: '/login',
                query: { returnUrl: router.asPath }
            });
        } else {
            setAuthorized(true);
        }
    }

    return (authorized && children);
}
 

Next.js App Component

Path: /pages/_app.js

The App component is the root component of the example Next.js app, it contains the outer html, main nav, and the component for the current page.

The current page component <Component {...pageProps} /> is wrapped in a route guard component (<RouteGuard>) that implements client-side authorization to prevent unauthenticated users from accessing secure pages.

The Next.js Head component is used to set the default <title> in the html <head> element and add the bootstrap css stylesheet. For more info on the Next.js head component see https://nextjs.org/docs/api-reference/next/head.

The App component overrides the default Next.js App component because it's in a file named /pages/_app.js and supports several features, for more info see https://nextjs.org/docs/advanced-features/custom-app.

import Head from 'next/head';

import 'styles/globals.css';
import { Nav, RouteGuard } from 'components';

export default App;

function App({ Component, pageProps }) {
    return (
        <>
            <Head>
                <title>Next.js 11 - Basic HTTP Authentication Example</title>

                {/* eslint-disable-next-line @next/next/no-css-tags */}
                <link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
            </Head>

            <div className="app-container bg-light">
                <Nav />
                <div className="container pt-4 pb-4">
                    <RouteGuard>
                        <Component {...pageProps} />
                    </RouteGuard>
                </div>
            </div>
        </>
    );
}
 

Next.js Login Page

Path: /pages/login.jsx

The login page contains a form built with the React Hook Form library that contains username and password fields for logging into the Next.js app.

Form validation rules are defined with the Yup schema validation library and passed with the formOptions to the React Hook Form useForm() function, for more info on Yup see https://github.com/jquense/yup.

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/api/useform.

The onSubmit function gets called when the form is submitted and valid, and submits the user credentials to the api by calling userService.login(). On successful login the user is redirected back to the previous page they requested (returnUrl) or to the home page ('/') by default.

The returned JSX template contains the markup for page including the form, input fields and validation messages. The form fields are registered with the React Hook Form by calling the register function with the field name from each input element (e.g. {...register('username')}). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

import { userService } from 'services';

export default Login;

function Login() {
    const router = useRouter();

    useEffect(() => {
        // redirect to home if already logged in
        if (userService.userValue) {
            router.push('/');
        }

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    // form validation rules 
    const validationSchema = Yup.object().shape({
        username: Yup.string().required('Username is required'),
        password: Yup.string().required('Password is required')
    });
    const formOptions = { resolver: yupResolver(validationSchema) };

    // get functions to build form with useForm() hook
    const { register, handleSubmit, setError, formState } = useForm(formOptions);
    const { errors } = formState;

    function onSubmit({ username, password }) {
        return userService.login(username, password)
            .then(() => {
                // get return url from query parameters or default to '/'
                const returnUrl = router.query.returnUrl || '/';
                router.push(returnUrl);
            })
            .catch(error => {
                setError('apiError', { message: error });
            });
    }

    return (
        <div className="col-md-6 offset-md-3 mt-5">
            <div className="alert alert-info">
                Username: test<br />
                Password: test
            </div>
            <div className="card">
                <h4 className="card-header">Next.js Basic Authentication Example</h4>
                <div className="card-body">
                    <form onSubmit={handleSubmit(onSubmit)}>
                        <div className="form-group">
                            <label>Username</label>
                            <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.username?.message}</div>
                        </div>
                        <div className="form-group">
                            <label>Password</label>
                            <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.password?.message}</div>
                        </div>
                        <button disabled={formState.isSubmitting} className="btn btn-primary">
                            {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                            Login
                        </button>
                        {errors.apiError &&
                            <div className="alert alert-danger mt-3 mb-0">{errors.apiError?.message}</div>
                        }
                    </form>
                </div>
            </div>
        </div>
    );
}
 


Need Some NextJS Help?

Search fiverr for freelance NextJS developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by