Detecting a User's Authenticated State Client-Side in Next.js using an HttpOnly Cookie and Static Optimization
Tags: nextjs, authentication, cookie, react
Published: 2020-05-01 22:09:24

In a previous post, I discussed how to [detect a user's authenticated state when using SSR](https://dev.to/justincy/detecting-authentication-client-side-in-next-js-with-an-httponly-cookie-when-using-ssr-4d3e). In this post I propose a similar architecture that works with [static optimization](https://nextjs.org/docs/advanced-features/automatic-static-optimization). The architecture described here also works with SSR, which makes it much more appealing than one which _only_ works with SSR and doesn't work at all with static optimization. 

First, let's review some key points which will inform the requirements of our architecture:

1. The client doesn't validate authentication; the server does validation. The client just needs to know whether or not the user is authenticated. In other words, just a boolean: true or false.
2. The traditional methods for clients to detect authenticated state is through either a) global data populated during render by the server or b) detecting the presence of a session cookie.
3. Statically optimized pages are rendered on the server during the build, but not at runtime. Therefore, we can't have the server populate global data on the page for detecting authenticated state.
4. We want to avoid having our session cookies stolen by 3rd party scripts, so we'll store the session token or ID in an [HttpOnly cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). Unfortunately, that also prevents our client-side JS from reading it.

Therefore, we need something else.

HttpOnly cookies are sent to the server, so we need a server endpoint that tells us whether the user is authenticated. It could be an endpoint for retrieving the user profile: if the profile is returned, the user is authenticated; if we get a 401, the user isn't authenticated. Or it could just be an endpoint built specifically for this (e.g. `/checkAuth`) which returns a 200 or 401.

As before, we will use the [Context API](https://reactjs.org/docs/context.html) to store our authenticated state. It will be initialized when the page loads by making a request to our API, as we just discussed. But until that request returns, the authenticated state is unknown. You might be able to assume false, but if you choose to render or redirect pages based on the authenticated state, then it's best not to make that assumption. So our context will also contain an `isLoading` boolean which we can use to show a loading indicator until the authentication response is returned and we know what to do.

```jsx
import React from 'react';

const AuthContext = React.createContext({
  isAuthenticated: false,
  isLoading: true,
  setAuthenticated: () => {}
});

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setAuthenticated] = React.useState(false);
  const [isLoading, setLoading] = React.useState(true);
  React.useEffect(() => {
    const initializeAuth = async () => {
      const response = await fetch('/api/checkAuth');
      setAuthenticated(response.status === 200);
      setLoading(false);
    };
    initializeAuth();
  }, []);
  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        isLoading,
        setAuthenticated
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export function useAuth() {
  const context = React.useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

export function useIsAuthenticated() {
  const context = useAuth();
  return context.isAuthenticated;
}
```

Now our end goal is to have two HOCs, `withAuth` and `withoutAuth` that will render or redirect pages based on the authenticated state. The majority of their code is shared so we'll create a `withAuthRedirect` HOC that they'll use.

```jsx
import { useRouter } from 'next/router';
import { useAuth } from '../providers/Auth';

function DefaultLoadingFallback() {
  return <p>Loading...</p>;
}

/**
 * Support client-side conditional redirecting based on the user's
 * authenticated state.
 *
 * @param WrappedComponent The component that this functionality
 * will be added to.
 * @param LoadingComponent The component that will be rendered while
 * the auth state is loading.
 * @param expectedAuth Whether the user should be authenticated for
 * the component to be rendered.
 * @param location The location to redirect to.
 */
export default function withAuthRedirect({
  WrappedComponent,
  LoadingComponent = DefaultLoadingFallback,
  expectedAuth,
  location
}) {
  const WithAuthRedirectWrapper = props => {
    const router = useRouter();
    const { isLoading, isAuthenticated } = useAuth();
    if (isLoading) {
      return <LoadingComponent />;
    }
    if (typeof window !== 'undefined' && expectedAuth !== isAuthenticated) {
      router.push(location);
      return <></>;
    }
    return <WrappedComponent {...props} />;
  };
  return WithAuthRedirectWrapper;
}
```

Now we're ready to create the other two HOCs.

```jsx
import withAuthRedirect from './withAuthRedirect';

/**
 * Require the user to be authenticated in order to render the component.
 * If the user isn't authenticated, forward to the given URL.
 */
export default function withAuth(WrappedComponent, location = '/login') {
  return withAuthRedirect({
    WrappedComponent,
    location,
    expectedAuth: true
  });
}
```

```jsx
import withAuthRedirect from './withAuthRedirect';

/**
 * Require the user to be unauthenticated in order to render the component.
 * If the user is authenticated, forward to the given URL.
 */
export default function withoutAuth(WrappedComponent, location = '/profile') {
  return withAuthRedirect({
    WrappedComponent,
    location,
    expectedAuth: false
  });
}
```

Those two HOCs may be used like this:

```jsx
export default withAuth(function ProfilePage() { ... });
```

We've accomplished our goal of an architecture which allows us to detect authentication client-side using an HttpOnly cookie and static optimization.

There is an [example app](https://github.com/justincy/nextjs-client-auth-architectures/tree/master/static) using this pattern. It's also available in [TypeScript](https://github.com/justincy/nextjs-client-auth-architectures/tree/master/static-ts).