Follow Us:
How to Implement Login Authentication without a backend?
As a developer, you know the importance of implementing secure login authentication for your web applications. In this tutorial, we’ll be discussing how to implement login authentication without the need for a backend server. We’ll be using the React framework for the front end and JSON Web Tokens (JWTs) for storing and validating user credentials.
By the end of this tutorial, you’ll know how to set up a login form, hash, and store user passwords, generate JWTs and protect routes and pages from unauthorized access. This approach has its pros and cons, which we’ll discuss at the end of the tutorial. Let’s dive in and learn how to implement authentication without a backend!
In this article, I aimed to provide a solution for individuals or small projects that may not have the resources or expertise to implement a full backend login authentication system. I understand that this approach may not be suitable for larger, more sensitive websites and applications. This is is just a guide to implement login authentication using JWTs aka JSON Web tokens. However I would like to remind readers that it is always important to consider the security implications of any implementation and to use the appropriate measures to protect sensitive information.
Setting up the Frontend:
Before we can start implementing authentication, we need to set up the front end of our application. If you’re using React for your front end, you’ll need to install the necessary packages and dependencies. First, make sure you have Node.js and npm installed on your machine. Then, create a new React project using the following command:
npx create-react-app my-app
This will create a new directory called “my-app” with all the necessary files and dependencies for a React application. Next, we’ll need to install some additional packages that we’ll be using for authentication. In your terminal, navigate to the root directory of your React project and run the following command:
npm install jsonwebtoken bcryptjs
This will install the jsonwebtoken
and bcryptjs
packages, which we’ll be using for generating and validating JWTs and hashing user passwords, respectively.
Once the packages are installed, we can set up our login form. In your React project, create a new file called “LoginForm.js” and add the following code:
import React, { useState } from 'react';
const LoginForm = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
// TODO: Implement login logic
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;
This is a simple login form with fields for the user’s email and password. The form has a submit button that will trigger the handleSubmit
function when clicked. Currently, the handleSubmit
function is a placeholder for the login logic that we’ll implement later.
Now that we have our login form set up, we can move on to implementing authentication without a backend.
Implementing Login Authentication:
One way to implement authentication without a backend is to use JSON Web Tokens (JWTs). JWTs are a type of token that can be used to authenticate users. They consist of a header, a payload, and a signature, and are usually encoded as a JSON object and signed with a secret key.
To use JWTs for authentication, we first need to generate a new JWT when the user logs in successfully. To do this, we’ll need to hash the user’s password and compare it to a stored hash. If the passwords match, we can generate a new JWT and store it in local storage or a cookie.
Here’s an example of how we might generate a JWT upon successful login:
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
const handleSubmit = async (event) => {
event.preventDefault();
try {
// Hash the user's password and compare it to the stored hash
const isMatch = await bcrypt.compare(password, storedHash);
if (!isMatch) {
// Password is incorrect, throw an error
throw new Error('Incorrect password');
}
// Password is correct, generate a new JWT
const token = jwt.sign({ email }, secretKey, { expiresIn: '24h' });
// Store the JWT in local storage
localStorage.setItem('token', token);
} catch (error) {
console.error(error);
}
};
In this example, we’re using the bcryptjs library to compare the user’s password to the stored hash. If the passwords match, we use the JSON web token library to generate a new JWT with the user’s email as the payload. We’re also setting an expiration time of 24 hours for the JWT. Finally, we store the JWT in local storage so that it can be used for future authentication.
Now that we have a way to generate and store JWTs, let’s move on to validating the user’s credentials.
Validating the User’s Credentials:
In order to validate the user’s credentials, we’ll need to hash their password and compare it to a stored hash. To do this, we’ll need to set up a database or file system to store the user’s hashed password.
One way to store the user’s hashed password is to use a serverless function, such as an AWS Lambda function or a Google Cloud Function. These functions can be triggered by HTTP requests and can be used to store and retrieve data from a database or file system.
Here’s an example of how we might set up a serverless function to store the user’s hashed password:
import bcrypt from 'bcryptjs';
exports.handler = async (event) => {
try {
// Get the user's email and password from the event data
const { email, password } = event.data;
// Hash the user's password
const hashedPassword = await bcrypt.hash(password, 10);
// Store the user's email and hashed password in a database or file system
await storeUser({ email, hashedPassword });
return { statusCode: 200 };
} catch (error) {
console.error(error);
return { statusCode: 500 };
}
};
This function takes an event object as an argument and extracts the user’s email and password from the event data. It then hashes the user’s password and stores the email and hashed password in a database or file system.
To validate the user’s credentials, we can send an HTTP request to this serverless function with the user’s email and password. If the function returns a status code of 200, it means the user’s credentials are valid. If the function returns a status code of 500, it means there was an error storing the user’s hashed password.
With the user’s hashed password stored in a database or file system, we can now validate the user’s credentials by comparing the user’s hashed password to the stored hash. Let’s see how this might look in our login form:
import axios from 'axios';
const handleSubmit = async (event) => {
event.preventDefault();
try {
// Send a request to the serverless function with the user's email and password
const response = await axios.post('/store-password', { email, password });
if (response.status !== 200) {
// There was an error storing the user's hashed password, throw an error
throw new Error('Error storing password');
}
// The user's credentials are valid, generate a new JWT
const token = jwt.sign({ email }, secretKey, { expiresIn: '1h' });
// Store the JWT in local storage
localStorage.setItem('token', token);
} catch (error) {
console.error(error);
}
};
In this example, we’re using the Axios library to send an HTTP request to the serverless function with the user’s email and password. If the function returns a status code of 200, it means the user’s credentials are valid and we can proceed to generate a new JWT. If the function returns a different status code, it means there was an error storing the user’s hashed password and we should throw an error.
Now that we have a way to validate the user’s credentials, we’ll now try to protect some routes & pages of the website only for the logged-in users.
Protecting Routes and Pages:
Now that we have a way to generate and validate JWTs, we can use them to protect routes and pages from unauthorized access. To do this, we’ll need to use a route guard or a higher-order component (HOC) that checks the validity of the JWT on page load.
Here’s an example of how we might use a route guard to protect a route in React Router:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import jwt from 'jsonwebtoken';
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
const token = localStorage.getItem('token');
if (!token) {
// No JWT found, redirect to login page
return <Redirect to="/login" />;
}
try {
// Verify the JWT
jwt.verify(token, secretKey);
// JWT is valid, render the protected component
return <Component {...props} />;
} catch (error) {
// JWT is invalid, redirect to login page
return <Redirect to="/login" />;
}
}}
/>
);
export default PrivateRoute;
In this example, we’re using the react-router-dom library to define a private route that requires authentication. The private route checks for the presence of a JWT in local storage. If no JWT is found, the user is redirected to the login page. If a JWT is found, it is verified using the secret key. If the JWT is valid, the protected component is rendered. If the JWT is invalid, the user is redirected to the login page.
We can use this private route to protect any routes that require authentication:
import PrivateRoute from './PrivateRoute';
<PrivateRoute path="/protected" component={ProtectedComponent} />
Alternatively, we can use a higher-order component (HOC) to check the validity of the JWT on page load. A HOC is a function that takes a component as an argument and returns a new component with additional functionality.
Here’s an example of how we might use a HOC to check the validity of the JWT on page load:
import React from 'react';
import { Redirect } from 'react-router-dom';
import jwt from 'jsonwebtoken';
const withAuth = (WrappedComponent) => {
return class extends React.Component {
render() {
const token = localStorage.getItem('token');
if (!token) {
// No JWT found, redirect to login page
return <Redirect to="/login" />;
}
try {
// Verify the JWT
jwt.verify(token, secretKey);
// JWT is valid, render the wrapped component
return <WrappedComponent {...this.props} />;
} catch (error) {
// JWT is invalid, redirect to login page
return <Redirect to="/login" />;
}
}
};
};
export default withAuth;
In this example, we’re using the withAuth HOC to wrap a protected component and check the validity of the JWT on page load. If no JSON Web token is found, the user is redirected to the login page. If a JWT is found, it is verified using the secret key. If the JWT is valid, the wrapped component is rendered. If the JSON Web Token is invalid, the user is redirected to the login page.
To use this HOC, we can simply wrap our protected component with the withAuth function:
import withAuth from './withAuth';
const ProtectedComponent = () => {
// Protected component code goes here
};
export default withAuth(ProtectedComponent);
So, that’s how we can implement login authentication without a backend. So now that we’ve done that thing, let’s see some FAQs.
FAQs:
What are JSON Web Tokens (JWTs)?
JSON Web Tokens (JWTs) are a type of token that can be used to authenticate users. They consist of a header, a payload, and a signature, and are usually encoded as a JSON object and signed with a secret key. JWTs can be used to securely transmit information between parties and are commonly used for authentication purposes in web development.
How do I store JWTs in local storage or a cookie?
To store a JWT in local storage, you can use the “localStorage.setItem” function:
localStorage.setItem('token', token);
To store a JWT in a cookie, you can use the “setCookie” function from a library like js-cookie:import Cookies from 'js-cookie';
Cookies.set('token', token);How do I encrypt JWTs with a secret key?
To encrypt a JWT with a secret key, you can use the “sign” function from the jsonwebtoken library:
import jwt from 'jsonwebtoken';
In this example, we’re using the “sign” function to generate a new JWT with the user’s email as the payload and a secret key as the signing key. The JWT will also have an expiration time of 24 hours.
const token = jwt.sign({ email }, secretKey, { expiresIn: '24h' });How do I hash user passwords?
To hash user passwords, you can use the “hash” function from a library like bcryptjs:
import bcrypt from 'bcryptjs';
const hashedPassword = await bcrypt.hash(password, 10);
In this example, we’re using the “hash” function to generate a new hashed password from the user’s plaintext password. The “10” parameter indicates the number of rounds of hashing to use, which determines the level of security and computational cost. A higher number of rounds will result in a more secure hash, but will also take longer to compute.
It’s important to store the hashed password in a secure manner, such as in a database or file system that is protected by a serverless function or backend server. This will prevent the plaintext password from being compromised if the database or file system is accessed by an unauthorized user.How do I verify a JWT?
To verify a JWT, you can use the “verify” function from the jsonwebtoken library:
import jwt from 'jsonwebtoken';
try {
jwt.verify(token, secretKey);
} catch (error) {
console.error(error);
}
In this example, we’re using the “verify” function to check the validity of a JWT with a secret key. If the JWT is valid, the function will return the decoded payload. If the JWT is invalid or has expired, the function will throw an error. You can use this function to ensure that
Conclusion:
In this tutorial, we learned how to implement login authentication without a backend server. We used the React framework for the front end and JSON Web Tokens (JWTs) for storing and validating user credentials. We set up a login form, hashed and stored user passwords, generated JWTs, and protected routes and pages from unauthorized access.
There are pros and cons to this approach. One advantage is that it allows you to implement authentication without the need for a backend server, which can be helpful if you’re working on a small or low-budget project. However, it also means that you’ll need to handle all the authentication logic on the front end, which can be complex and error-prone.
If you’re working on a larger or more complex project, you may want to consider using an external authentication service, such as Auth0 or Okta or building your own backend server to handle authentication. These options provide more security and scalability, but may also require more time and resources to set up.