Server Side Auth with Next.js, Context and Hooks

Use React Context to access auth data server side with Next.js.

When using authentication in a Next.js app you want to have a seamless experience. Doing user authentication server side can prevent unnecessary spinners within your app. We can create server side rendered authentication in Next.js using React Context in an _app.js file. By wrapping our entire application in an auth context, we can validate users server side and have access to the current user within any component.

This method should work for any backend authentication solution you have. The process goes as followed:

  1. Create an AuthContext provider to contain your authentication logic and provide methods for dealing with losing in/out and getting user info
  2. Wrap your application with our Auth.Provider in _app.js.
  3. Use the useContext hook to get user data from within your components.

Auth Context

First create a context for us to store authentication data. React Context gives us access to Provider and Consumer components to set and retrieve data anywhere within our app.


import axios from 'axios';
import { createContext } from 'react';
import router from 'next/router';

const AuthContext = createContext();

export const getUser = async (ctx) => {
  return await axios
    .get(`${process.env.NEXT_PUBLIC_API_URL}/token`, {
      headers: ctx.?req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } : undefined,
      withCredentials: true,
    })
    .then((response) => {
      if (response.data) {
        return { status: 'SIGNED_IN', user: response.data };
      } else {
        return { status: 'SIGNED_OUT', user: null };
      }
    })
    .catch((error) => {
      return { status: 'SIGNED_OUT', user: null };
    });
};

export const AuthProvider = (props) => {
  const auth = props.myAuth || { status: 'SIGNED_OUT', user: null };
  const login = async (email, password) => {
    // Use any auth service methods here
    return await axios({
      method: 'post',
      url: `${process.env.NEXT_PUBLIC_API_URL}/login`,
      data: { email, password },
      withCredentials: true,
    })
      .then(() => {
        router.push('/');
        console.log('user signed in');
      })
      .catch((error) => {
        console.error('Incorrect email or password entered.);
      });
  };
  const register = async (email, password) => {
    return await axios({
      method: 'post',
      url: `${process.env.NEXT_PUBLIC_API_URL}/register`,
      data: { email, password },
      withCredentials: true,
    })
      .then(function (response) {
        router.push('/');
          console.log('user registered');
      })
      .catch(function (error) {
        console.error(error.message);
      });
  };
  const logout = async () => {
    return await axios
      .get(`${process.env.NEXT_PUBLIC_API_URL}/logout`, { withCredentials: true })
      .then(() => {
        router.push('/');
        console.log('user logged out');
      })
      .catch((error) => {
        console.error(error.message);
      });
  };
  return <AuthContext.Provider value={{ auth, logout, register, login }} {...props} />;
};

export const useAuth = () => React.useContext(AuthContext);
export const AuthConsumer = AuthContext.Consumer;

We now have a Context Provider with all the methods needed for handling user authentication.

Wrapping application with _app.js

Create a _app.js in the pages directory. This allows us to have access the root of our react app. Here we can wrap our whole application with our Auth Provider.

We will use getInitialProps to make a call to our /token endpoint to check if a user exists with our current cookies. If there is no valid session user will be set to null and status will be SIGNED_OUT

import App from "next/app"
import { AuthProvider, getUser } from "../components/contexts/AuthContext"

const MyApp = ({ Component, pageProps, auth }) => {
  return (
    <AuthProvider myAuth={auth}>
      <Component {...pageProps} />
    </AuthProvider>
  )
}

MyApp.getInitialProps = async (appContext) => {
  const appProps = await App.getInitialProps(appContext)
  const auth = await getUser(appContext.ctx)
  return { ...appProps, auth: auth }
}

export default MyApp

Use useContext() hook to access AuthContext data

Now we can access our auth data and methods anywhere within our app. The useContext hook makes this especially simple.

// Navigation.jsx
function Navigation() {
  const { auth } = useAuth()
  if (auth.status === "SIGNED_OUT") {
    return <LoggedOut />
  }
  if (auth.status === "SIGNED_IN") {
    return <LoggedIn user={auth.user} />
  }
}

Login/Register

We have access to login and regsiter methods from our context.

const LoginPage = () => {
  const { login, register } = useAuth()
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const handleLogin = async (e) => {
    e.preventDefault()
    // Log in or Register here
    login(email, password)
    // register(email, password)
  }
  return (
    <form onSubmit={handleLogin}>
      <Text as={"h1"} fontSize={28} fontWeight={"bold"}>
        Log in
      </Text>
      <FormControl>
        <FormLabel htmlFor="email">Email</FormLabel>
        <Input
          type="email"
          id="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </FormControl>
      <FormControl>
        <FormLabel htmlFor="email">Password</FormLabel>
        <Input
          type="password"
          id="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </FormControl>
      <Button type={"submit"}>Log In </Button>
    </form>
  )
}

Log out

Logging out is just as easy. We have access to our logout method.

const LoggedIn = () => {
  const { logout } = useAuth()
  return <Button onClick={logout}>Logout</Button>
}

By using React Context in our Next.js app we can get access to user auth not only in the client but server side too.

Have some feedback on this post? .