Posts > Login / Logout / User Authentication

Login / Logout / User Authentication

My first time adding User Authentication / Login Logout integration to my client project was an awesome experience. I’ve always been just doing the CRUD, Component before and giving me the opportunity to do the Login credentials was pretty cool.

Anyway, these “how to’s” are what I’m basing my code from now on ( until I learn a better approach to do it ) going forward.

So the important thing about doing any kind of challenging task is to cut them down first into little pieces and then do them one at a time.

Here’s how the login authentication is built based on what I gather on the web:

  1. Login Page ( UI ) - you have to have a small UI component to put down your username and password
  2. Login Logic for forms - this includes the state management / or putting your login information and passing it to the API
  3. Tokens & Session storage - when API sees your login information, it’ll usually respond with something that confirms that you have the correct details. Depending on your project, mine returns the user details and tokens. Which we need to access data and pages that require tokens or authentication to open.
  4. Protect Routes - if we have multiple pages, we might need to protect some pages that require users' information and authentication before accessing. Protecting them with some kind of redirect is one that I have used on my current project.
  5. Logout Function - removing any traces of user information on our browser.

So let’s start:

Login page ( UI )

Just a simple login form will do for this how to’s:

export default function Login() {
  return (
    <>
      <form>
        <p>
          <label htmlFor='username'>Username</label>
          <input name='username' id='username' type='text' />
        </p>
        <p>
          <label htmlFor='password'>Password</label>
          <input name='password' id='password' type='password' />
        </p>
        <button type='submit'>Login</button>
      </form>
    </>
  );
}

We added a username and password field for the form and submit button for the form.

Login Logic

  1. Let’s start creating a handleChange function for our form to be able to change the input fields, then handleSubmit to ready our API request later.

import { ChangeEvent, FormEvent, useState } from 'react';

interface LoginForm {
  username: string;
  password: string;
}

export default function Login() {
  const [payload, setPayload] = useState<LoginForm>({
    username: '',
    password: '',
  });

	// `handleChange` is here to make our dynamic as react will
	// have an issue on form that are not properly handled
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setPayload((prevState) => ({
      ...prevState,
      [name]: value,
    }));
  };
	
	// handleSubmit will get our payload/credentials and pass to
  // our api to check if the user exist
  const handleSubmit = (e: FormEvent<HTMLFormElement>, payload: LoginForm) => {
    e.preventDefault();
    console.log(payload); // <-- {username: '', password: ''}
  };

  return (
    <>
      <form onSubmit={(e) => handleSubmit(e, payload)}>
        <p>
          //<label ...
          <input
            //...
            onChange={handleChange}
          />
        </p>
        <p>
          //<label ...
          <input
            //...
            onChange={handleChange}
          />
        </p>
        <button type='submit'>Login</button>
      </form>
    </>
  );
}

2. Interacting with the API with our credentials. We needed to know if we entered the correct or incorrect credentials and check if there’s a return data from our API. Back in our handleSubmit function, we just wanted to see in our console what data our API is going to give to us.

const handleSubmit = (e: FormEvent<HTMLFormElement>, payload: LoginForm) => {
    e.preventDefault();

    const requestOptions = {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify(payload),
    };

		// using the fetch API to get our data fetch('url', [method, headers, body])
    fetch('[your API url]', requestOptions)
      .then((res) => {
        if (res.ok) {
          // Login successful
          return res.json();
        } else {
          // Login failed
          throw new Error('Login failed');
        }
      })
      .then((data) => {
        // Access the response data
        console.log('Response:', data);
      })
      .catch((error) => {
        // An error occurred during the request
        console.error('Error:', error);
        // Handle the error appropriately
      });
  };
👌requestOptions this object will depends on the server or API you are using, at this moment the API I’m using requires this headers and the credentials are required to be passed in the body.

Token and Session Storage

We now have a basic Login page that responds to our entered credentials. We should have been able to see what kind of message/response it has and use that response to our application.

👌Usually, in any login authentication, the server will respond with a token, token expiration, and user details. With this information, we can store them in our browser's local storage to be used for application.

Such as:

  • Displaying the user details ( name ) in the frontend
  • Using the tokens to get other API information we might need
  • Logging off the app automatically when the token expires

Back on the handleSubmit function, we can store our login API response in the local storage.

const handleSubmit = (e: FormEvent<HTMLFormElement>, payload: LoginForm) => {
    e.preventDefault();

    const requestOptions = {
      //...
    };
	 
		fetch('[your API url]', requestOptions)
      .then((res) => {
        //...
      })
      .then((data) => {
        // Add your data to the local storage.
        localStorage.setItem('user', data.result.userDetails);
        localStorage.setItem('token', data.result.token);

				// When logged in, redirect to the `/` page
        router.push('/');
      })
      .catch((error) => {
        // An error occurred during the request
        console.error('Error:', error);
      });
  };
👌Again depending on your API returned data, we can add them to our localStorage to be used on different parts of our site. Now we have user and token that we can use later on our Frontend.

Protect Routes

This one is where I struggled a lot when I was doing the user authentication as I was not really familiar with HOC ( High Order Component - more on that soon ) back then and still right now 😅 and Context API. We need to create some kind of a way to create restrictions/protection on some pages/routes that we wanted only logged-in users to view.

In this case, I’m just going to make sure the user can’t see the home page (/) that’s why in token and session storage, we redirect the user after logging in. Now we’ll create a way to make sure non-logged-in users can’t see our home page (/).

Let’s start by making sure that when a valid user logged in, we can access his data everywhere in our App. We’re going to use Context API here.

// AuthContext.js
import { useState, useContext, createContext, ReactNode } from 'react';
import { useRouter } from 'next/router';

interface AuthContextType {
  currentUser: string | null;
  login: (payload: Payload) => Promise<void | undefined>;
}

interface ResponseData {
  result: {
    email: string;
    token: string;
  };
}

type Payload = {
  email: string;
  password: string;
};

interface AuthProviderProps {
  children: ReactNode;
}

const AuthContext = createContext<AuthContextType>({
  currentUser: null,
  login: async () => undefined,
});

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }: AuthProviderProps) {
	// created a state for our currentUser where we can use in our components later
  const [currentUser, setCurrentUser] = useState<string | null>(null);
  const router = useRouter();

	//moving the login() here in the AuthProvider so we can use them anywhere
	//made it async await as well.
  async function login(payload: Payload): Promise<void | undefined> {
    try {
      const requestOptions = {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*',
        },
        body: JSON.stringify(payload),
      };

      const response = await fetch(
        'https://demo.cardid.app/api/login/',
        requestOptions
      );
      if (response.ok) {
        // Login successful
        const data: ResponseData = await response.json();
        localStorage.setItem('user', data.result.email);
        localStorage.setItem('token', data.result.token);

        //
        setCurrentUser(data.result.email);

        // when successfully logged in redirect to home page
        router.push('/');
      } else {
        // Login failed
        throw new Error('Login failed');
      }
    } catch (error) {
      console.error('Error:', error);
    }
  }

  const value = {
    currentUser,
    login,
  };

	// we can now call the `currentUser` state and `login` function anywhere in our app
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
👌I move the login() function here as well, so we can use it anywhere in our application in the future.

Don’t forget to add our AuthProvider in our main app component

//_app.tsx
//...
import { AuthProvider } from '@/components/context/AuthContext';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
}

2. Create our High Order Component to make sure that if a non-logged in user cannot access our home page, and redirects to our login page. Basically, what we’re doing is creating a component that takes a component as an argument and returns a new component. With the new component, we’re checking first if the user has logged-in information and is valid before rendering the component. ( too much use of the word component here 🤣 ).

//withAuth.jsx
import { useState, useEffect, ComponentType } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '@/components/context/AuthContext';

interface AuthProps {
  isAuthenticated: boolean;
}

const withAuth = <P extends object>(Component: ComponentType<P>) => {
  const AuthenticatedComponent = (props: P & AuthProps) => {
    const router = useRouter();
    const { currentUser: initialCurrentUser } = useAuth();
    const [currentUser, setCurrentUser] = useState(initialCurrentUser);

    useEffect(() => {
      if (typeof window !== 'undefined' && !initialCurrentUser) {
        // Fetch the authentication state on the client-side
        // This can be an API request or reading from local storage
        const storedUser = localStorage.getItem('currentUser');
        if (storedUser) {
          setCurrentUser(storedUser);
        } else {
          router.replace('/login');
        }
      }
    }, [initialCurrentUser, router]);

    if (!currentUser) {
      return null;
    }
    return <Component {...props} />;
  };

  return AuthenticatedComponent;
};

export default withAuth;

We just need to wrap this component in any pages we wanted to ask/require authentication.

function Homepage(){
	//...
}

export default withAuth(Homepage);

Logout function

Now let’s create our logout function. This will be easy as we just need to clear out the localStorage that we created earlier when logging in.

// AuthContext.tsx
import { useState, useContext, createContext, ReactNode } from 'react';
import { useRouter } from 'next/router';

interface AuthContextType {
  //...
}

interface ResponseData {
  //...
}

type Payload = {
  //...
};

interface AuthProviderProps {
  //...
}

const AuthContext = createContext<AuthContextType>({
  //...
});

export function useAuth() {
  //...}

export function AuthProvider({ children }: AuthProviderProps) {
	async function login(payload: Payload): Promise<void | undefined> {
    ///....
  }


	function logout(): void {
    // Remove user and token from localStorage
    localStorage.removeItem('user');
    localStorage.removeItem('token'); 

    // Clear the user state
    setCurrentUser(null);

    // Navigate back to the login page
    router.push('/login');
  }


  const value = {
    //...
		logout, // don't forget to add to your provider
  };

	
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Then just use your logout function into the component you needed it.

//Homepage.tsx
//...
import { useAuth } from '@/components/context/AuthContext';

const inter = Inter({ subsets: ['latin'] });

function Home() {
  const { logout } = useAuth();
  return (
    <main
      className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
    >
      <button onClick={() => logout()}>Logout</button>
      <h1>This the HOME PAGE</h1>
    </main>
  );
}

export default withAuth(Home);

Congratulations! 🥳 Now you have a working user authentication component.

This is how I did my first ever User Authentication task, and there is for sure a better approach than what I have, and there’s a better tool as well to use for easier user authentication.

👌You can also check the next-auth where I see lot’s of developer is currently using for thier next.js authentication.

Check out the whole code in my GitHub Repository: https://github.com/owaaquino/Login-Authentication