Let's now see how we can implement this logic with a real example.
Let's start by the controller, where we will modify the login method to store the user's ID in the session. This will allow us to authenticate the user on subsequent requests.
// app/app/Http/Controllers/UserController.php
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Todo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
class UserController extends Controller
{
protected $sessionFilePath = 'app/session_data.json';
public function login(Request $request)
{
$request->validate([
'username' => 'required',
'password' => 'required',
]);
$user = User::where('username', $request->username)->first();
if ($user && Hash::check($request->password, $user->password)) {
// Store user ID in session data
$sessionData = $this->getSessionData();
$newSessionData = [
'userId' => $user->id,
'token' => Str::uuid(),
];
$sessionData[$user->id] = $newSessionData;
$this->saveSessionData($sessionData);
return response()->json(['message' => 'Login successful', 'token' => $newSessionData['token'], 'user' => $user->id], 200);
}
return response()->json(['message' => 'Invalid credentials'], 401);
}
protected function getSessionData()
{
if (file_exists($this->sessionFilePath)) {
return json_decode(file_get_contents($this->sessionFilePath), true);
}
return [];
}
protected function saveSessionData($data)
{
file_put_contents(storage_path($this->sessionFilePath), json_encode($data));
}
}
Let's examine the code above thoroughly:
- We have added a new method
getSessionData that reads the session data from a file at storage/app/session_data.json. This file will store the session data for all users. Each entry is a key-value pair where the key is the user ID and the value is the session data for that user (which includes the user ID and the token).
- We have added a new method
saveSessionData that writes the session data to the file at storage/app/session_data.json.
- In the
login method, we have added the logic to store the user ID in the session data. We generate a new token using Str::uuid() and store the user ID and the token in the session data. We then save the session data to the file and return the token to the user.
Let's now examine how the client-side code will look like for login:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<script>
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({ username, password })
});
console.log(response);
if (!response.ok) {
throw new Error('Invalid credentials');
}
const data = await response.json();
// Assuming the token is included in the response data
const token = data.token;
const user = data.user;
if (token) {
// Store the token (e.g., in the local storage or a cookie)
localStorage.setItem('auth_token', token);
localStorage.setItem('user', user);
alert('Login successful. You can go to /todos now!');
// Optionally redirect user to another page or perform another action
}
} catch (error) {
console.error('Error during login:', error);
alert(error.message);
}
}
</script>
</head>
<body class="bg-gray-100">
<div class="container mx-auto mt-10">
<div class="max-w-md mx-auto bg-white p-5 rounded shadow">
<h2 class="text-2xl mb-4">Login</h2>
<form onsubmit="handleLogin(event)">
@csrf
<div class="mb-4">
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
<input type="text" name="username" id="username" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" required>
</div>
<div class="mb-4">
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input type="password" name="password" id="password" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" required>
</div>
<div class="mb-4">
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Login</button>
</div>
</form>
</div>
</div>
</body>
<meta name="csrf-token" content="{{ csrf_token() }}">
</html>
Notice that when user hits the Login button, the handleLogin function is called. This function sends a POST request to the /login endpoint with the username and password. If the login is successful, the token is stored in the local storage and an alert is shown to the user.
Let's now see how we secure the todos route using middleware. First let's note, that the controller does nothing for securing the todos route, it simply returns all the todos as before:
public function todos(Request $request)
{
$defaultTodos = [
['title' => 'Welcome Todo 1', 'description' => 'This is your first todo!'],
['title' => 'Welcome Todo 2', 'description' => 'Explore and add more todos!'],
];
foreach ($defaultTodos as $todo) {
Todo::create($todo);
}
$todos = Todo::all();
return response()->json(['todos' => $todos], 200);
}
All the security logic will be implemented in the middleware. Let's create a new middleware called CheckToken:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class CheckToken
{
public function handle(Request $request, Closure $next)
{
$token = $request->input('token');
$user = $request->input('user');
if (!$user || !$token) {
return response()->json(['error' => 'Missing token or user information'], 400);
}
$sessionData = $this->getSessionData();
if (isset($sessionData[$user]) && $sessionData[$user]['token'] === $token) {
return $next($request);
}
Log::warning('Unauthorized access attempt', ['user' => $user, 'token' => $token]);
return response()->json(['error' => 'Unauthorized'], 401);
}
protected function getSessionData()
{
if (file_exists(storage_path('app/session_data.json'))) {
return json_decode(file_get_contents(storage_path('app/session_data.json')), true);
}
return [];
}
}
Let's examine the code above step by step:
- We have created a new middleware called
CheckToken that checks if the user is authenticated based on the token and user ID passed in the request.
- In the
handle method, we first check if the token and user ID are present in the request. If they are not present, we return a 400 error.
- We then read the session data from the file and check if the user ID and token match the session data. If they match, we allow the request to proceed. If they do not match, we log a warning and return a
401 error.
- We have added a new method
getSessionData that reads the session data from the file at storage/app/session_data.json.
Remember, the middleware is registered in the app/Http/Kernel.php file in the $routeMiddleware array:
protected $routeMiddleware = [
...
'check.token' => \App\Http\Middleware\CheckToken::class,
];
Now, let's apply the middleware to the todos route in the routes/web.php file:
Route::get('/todos', 'App\Http\Controllers\UserController@todos')->middleware('check.token');
With this, when users try to access the /todos route, the CheckToken middleware will be executed first. If the user is not authenticated, the middleware will return a 401 error. If the user is authenticated, the middleware will allow the request to proceed, and the todos will be returned as before.
Let's also understand how the token and user ID are passed in the request.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todos</title>
<script>
async function fetchTodos() {
try {
const token = localStorage.getItem('auth_token');
const user = localStorage.getItem('user');
if (!token || !user) {
alert('Not authenticated. Please log in.');
}
console.log(token)
const response = await fetch('/get-todos?token=' + token + '&user=' + user);
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const todosList = document.getElementById('todos-list');
todosList.innerHTML = ''; // Clear previous todos
if (data.todos && data.todos.length) {
data.todos.forEach(todo => {
const listItem = document.createElement('li');
listItem.textContent = `${todo.title}: ${todo.description}`;
todosList.appendChild(listItem);
});
} else {
todosList.innerHTML = '<li>No todos available.</li>';
}
} catch (error) {
console.error('Error fetching todos:', error);
document.getElementById('todos-list').innerHTML = `<li>Error loading todos.</li>`;
}
}
window.onload = fetchTodos;
</script>
</head>
<body class="bg-gray-100">
<div class="container mx-auto mt-10">
<div class="max-w-md mx-auto bg-white p-5 rounded shadow">
<h2 class="text-2xl mb-4">Todos</h2>
<ul id="todos-list" class="list-disc pl-5">
<!-- Todos will be populated here -->
</ul>
</div>
</div>
</body>
</html>
Let's examine the code above:
- We have added a new function
fetchTodos that fetches the todos from the /get-todos endpoint. The token and user ID are passed as query parameters in the request.
- We have added a new
window.onload event listener that calls the fetchTodos function when the page loads.
- When the
fetchTodos function is called, it first checks if the token and user ID are present in the local storage. If they are not present, it shows an alert to the user.
- It then sends a
GET request to the /get-todos endpoint with the token and user ID as query parameters. If the request is successful, it populates the todos in the todos-list element. If the request fails, it shows an error message.
- The todos are displayed as a list of items with the title and description of each todo.
With this setup, only authenticated users can access the /todos route. If a user is not authenticated, they will be redirected to the login page. This adds an extra layer of security to the application and ensures that sensitive data is protected.
Finally, let's see how we can implement a logout functionality. When a user logs out
class UserController extends Controller
{
// ...
public function logout(Request $request)
{
$user = $request->input('user');
$sessionData = $this->getSessionData();
if (isset($sessionData[$user])) {
unset($sessionData[$user]);
$this->saveSessionData($sessionData);
}
return response()->json(['message' => 'Logged out successfully'], 200);
}
}
Notice, that in this method we simply remove the user from the session data. This effectively logs the user out. The client-side code for logging out will look like this:
<script>
...
async function logout() {
localStorage.removeItem('auth_token');
alert('Logged out successfully');
fetch('/logout&user=' + localStorage.getItem('user'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
});
}
</script>
...
<button onclick="logout()">Logout</button>
When the user clicks the Logout button, the logout function is called. This function removes the token from the local storage and sends a POST request to the /logout endpoint with the user ID. The user is then logged out and the session data is updated.
With this setup, you have implemented a complete authentication system with login, logout, and session management in your Laravel application. This adds an extra layer of security to your application and ensures that only authenticated users can access sensitive data.