Back to all

Building an auth checker

Catching unauthenticated users with a login modal efficiently.

Challenge

While building a full stack web application where users should be able to trigger various authenticated actions, I not only wanted to protect the API routes on the backend, but also trigger client-side UI based on the users authentication side, i.e. whether they have an active session or not.An example would be a user attempting to message another user. They should only be able to do so once signed in, so that the application can identify the user. There are usually countless other occasions in web applications where users should only be able to trigger a certain action once they have an active session.To be clear: We want to protect the action on the backend anyways. However, the frontend should render UI that guides the user towards their action in case they are not logged in. In this case this is the triggering of a login modal.In summary: The user attempts to make an auth protected action, e.g. sending a message.

  • IF the user is not signed in, the application should show a login modal.

  • IF the user is signed in, the application should show a message modal.

Planning the components

Hence, I wanted to build this logic out in the most reusable way. I figured I would need 2 core components to do so:

  • Global Login Modal: My login modal should be controlled on a global app-level instead of managing itself on component-level state.

  • AuthChecker: I need a component that wraps any existing component in the app and checks the users authentication state before firing the default onClick action of the element.

Global Login Modal

To manage the login modal globally, I first created a Global Context that holds the value for an isOpen boolean and an onOpen and onClose function to toggle the modal.

GlobalModalContext.ts


type GlobalModalContextType = {
  onOpen: () => void;
  onClose: () => void;
  isOpen: boolean;
};

const initialState: GlobalModalContextType = {
  isOpen: false,
  onOpen: () => {},
  onClose: () => {},
};

export const GlobalModalContext = createContext({ ...initialState });

export const useGlobalModalContext = () => {
  const value = useContext(GlobalModalContext);
  return value;
};

I then created a Global Modal component that consumes the context. It renders all elements that my global login modal needs and passes the content of the modal from a separate component.

GlobalModal.tsx


const GlobalModalComponent = () => {
  const { isOpen, onClose } = useGlobalModalContext();
  return (
    <Modal onClose={onClose} isOpen={isOpen} isCentered size='xl'>
      <ModalOverlay />
      <ModalContent>
        <LoginModal onClose={onClose} />
      </ModalContent>
    </Modal>
  );
};

This way, all my login component needs to care about, is its own functionality. It does not need to anything about modal behaviour. In fact the component itself doesnt know about its existence as a modal.

LoginModal.tsx


const LoginModal = ({ onClose }) => { return (
  <form>...</form>
)}

I then pass the Global Modal Component into a Context Provider wrapper which uses the useDisclousre hook from chakra to populate the context with its respective values. (Here, we could have also built our own global modal state and respective toggle functions but as I am a big chakra fan, I used their prebuilt functions.)

GlobalModal.tsx


export const GlobalModal = ({ children }) => {
  const { isOpen, onOpen, onClose } = useDisclosure();

  return (
    <GlobalModalContext.Provider value={{ isOpen, onOpen, onClose }}>
      <GlobalModalComponent />
      {children}
    </GlobalModalContext.Provider>
  );
};

Then, wrap the application with the provider.

_app.tsx
export const App = ({   Component, pageProps }) => {
  return (
    ...
    <GlobalModal>
      ...
      <Component {...pageProps} />
      ...
    </GlobalModal>
    ...
  );
};

The auth checker

The AuthCheck component should be independent from any child component. So, to not only pass the child as props but also be able to use its onClick event handler where the default action is called, we use React.clone and call the onClick.


AuthCheck.tsx

const AuthCheck = ({ children }) => {
  const handleClick = (e) => {
    if (session) {
      children.props.onClick();
    }
  };
  return (
    <>{React.cloneElement(children, { onClick: (e) => handleClick(e) })}</>
  );
};

export default AuthCheck;
Now, consume the session. Trigger the default element action if user has a session and trigger the modal function from the context if user has no session.

AuthCheck.tsx


const AuthCheck = ({ children }) => {
  const { data: session, status } = useSession();
  const { onOpen } = useGlobalModalContext();
  const handleClick = (e) => {
    if (!session) {
      onOpen();
    }
    if (session) {
      children.props.onClick();
    }
  };
  return (
    <>{React.cloneElement(children, { onClick: (e) => handleClick(e) })}</>
  );
};

export default AuthCheck;

Finally, use the AuthCheck component to wrap the element invoking the action you want to protect. That's it! You can now enjoy protecting UI client-side with only two lines of code.

SendMessageButton.tsx

<AuthCheck>
  <Button onClick={() => openMessageModal()}>
    Message
  </Button>
</AuthCheck>