Skip to content

Lab 10: Login & Authentication (Part 2)

In this lab, you will implement authentication and authorization for your e-commerce application. Building on Part 1 (User Registration), you will create login functionality, manage user sessions, protect routes with middleware, and implement role-based access control.

Your e-commerce site has two protected areas:

  1. Admin Panel: only administrators can access the admin panel.
  2. Checkout Process: only logged-in customers can checkout items from their cart.

  • Implement password verification using password_verify().
  • Create a login form and authentication logic.
  • Manage user sessions using SessionManager.
  • Build middleware to protect routes (AuthMiddleware, AdminAuthMiddleware).
  • Implement role-based access control (admin vs. customer).
  • Create a protected dashboard view.

  • Part 1 completed: user registration system with UserModel.
  • Session Middleware lab completed (SessionManager and SessionMiddleware).
  • Flash Messages lab completed.
  • SessionMiddleware registered globally in the application.
  • UserModel and AuthController classes implemented.

Step 1: Add verifyCredentials() Method to UserModel

Section titled “Step 1: Add verifyCredentials() Method to UserModel”

This method accepts an identifier (email or username) and a plain-text password. It looks up the user by email first, then by username. If a user is found, it verifies the password against the stored hash. It returns the user data on success or null on failure. Always return null for invalid credentials to avoid revealing whether an email or username exists in the system.

Open app/Domain/Models/UserModel.php and add:

/**
* Verify user credentials by email/username and password.
*
* @param string $identifier Email or username
* @param string $password Plain-text password to verify
* @return array|null User data if credentials are valid, null otherwise
*/
public function verifyCredentials(string $identifier, string $password): ?array
{
// TODO: Look up the user by their identifier (try email first, then username).
// If found, verify the password against the stored hash.
// Return the user data on success or null on failure.
}

The login form uses a single “identifier” field that accepts either an email address or a username, giving users flexibility to log in with whichever they prefer.

Create app/Views/auth/login.php:

app/Views/auth/login.php
<?php
use App\Helpers\ViewHelper;
ViewHelper::loadHeader($title);
?>
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card">
<div class="card-header">
<h3 class="text-center">Login</h3>
</div>
<div class="card-body">
<form method="POST" action="<?= APP_BASE_URL ?>/login">
<div class="mb-3">
<label for="identifier" class="form-label">Email or Username</label>
<input
type="text"
class="form-control"
id="identifier"
name="identifier"
placeholder="Enter your email or username"
required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
type="password"
class="form-control"
id="password"
name="password"
placeholder="Enter your password"
required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
<div class="mt-3 text-center">
<p>Don't have an account? <a href="<?= APP_BASE_URL ?>/register">Register here</a></p>
</div>
</div>
</div>
</div>
</div>
<?php ViewHelper::loadFooter(); ?>

The AuthController handles user authentication by processing login requests, verifying credentials against the database, managing user sessions, and redirecting users based on their roles (admin vs. customer).

Open app/Controllers/AuthController.php and add:

/**
* Display the login form (GET request).
*/
public function login(Request $request, Response $response, array $args): Response
{
// TODO: Create a $data array with 'title' => 'Login'
// TODO: Render 'auth/login.php' view and pass $data
}
/**
* Process login form submission (POST request).
*/
public function authenticate(Request $request, Response $response, array $args): Response
{
// TODO: Extract the identifier and password from the submitted form
// TODO: Validate that both fields are present
// TODO: Attempt to verify the credentials using the UserModel
// TODO: If authentication fails, display an error and redirect to the login page
// TODO: Store the authenticated user's data in the session
// (ID, email, full name, role, and a flag indicating they are authenticated)
// TODO: Display a welcome message and redirect based on role:
// admins go to the admin dashboard, customers go to the user dashboard
}
/**
* Logout the current user (GET request).
*/
public function logout(Request $request, Response $response, array $args): Response
{
// TODO: Destroy the session, display a success message, and redirect to the login page
}

AuthMiddleware acts as a gatekeeper for protected routes. It intercepts requests, checks if a user is authenticated (logged in), and either allows the request to proceed or redirects to the login page.

Create app/Middleware/AuthMiddleware.php:

app/Middleware/AuthMiddleware.php
<?php
namespace App\Middleware;
use App\Helpers\FlashMessage;
use App\Helpers\SessionManager;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Routing\RouteContext;
class AuthMiddleware implements MiddlewareInterface
{
public function __construct(private ResponseFactoryInterface $responseFactory)
{
}
/**
* Process the request - check if user is authenticated.
*/
public function process(Request $request, RequestHandler $handler): Response
{
// TODO: Retrieve the user's authentication status from the session.
// If not authenticated, display an error flash message and redirect to the login page.
// If authenticated, allow the request to proceed.
//
// Use the following to generate the login URL and create a redirect response:
// $routeParser = RouteContext::fromRequest($request)->getRouteParser();
// $loginUrl = $routeParser->urlFor('auth.login');
// $response = $this->responseFactory->createResponse(302);
// return $response->withHeader('Location', $loginUrl);
}
}

AdminAuthMiddleware provides enhanced protection for admin-only routes. It performs two checks: verifying the user is logged in AND confirming they have the admin role. Regular customers are redirected to their dashboard even if they are logged in.

Create app/Middleware/AdminAuthMiddleware.php:

app/Middleware/AdminAuthMiddleware.php
<?php
namespace App\Middleware;
use App\Helpers\FlashMessage;
use App\Helpers\SessionManager;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Routing\RouteContext;
class AdminAuthMiddleware implements MiddlewareInterface
{
public function __construct(private ResponseFactoryInterface $responseFactory)
{
}
/**
* Process the request - check if user is authenticated AND is an admin.
*/
public function process(Request $request, RequestHandler $handler): Response
{
// TODO: Retrieve the user's authentication status and role from the session.
// If not authenticated, redirect to the login page.
// If authenticated but not an admin, redirect to the user dashboard
// with an access denied message.
// If both checks pass, allow the request to proceed.
//
// Use the same redirect pattern as AuthMiddleware (RouteParser + responseFactory).
}
}

This is a simple customer dashboard that displays user information from the session. The header layout is loaded via ViewHelper, so flash messages are already rendered.

Create app/Views/user/dashboard.php:

app/Views/user/dashboard.php
<?php
use App\Helpers\SessionManager;
use App\Helpers\ViewHelper;
ViewHelper::loadHeader($title);
?>
<h1>User Dashboard</h1>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">My Profile</h5>
<p class="card-text">
<strong>Name:</strong> <?= hs(SessionManager::get('user_name', 'N/A')) ?><br>
<strong>Email:</strong> <?= hs(SessionManager::get('user_email', 'N/A')) ?><br>
<strong>Role:</strong> <?= hs(SessionManager::get('user_role', 'N/A')) ?>
</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-body">
<h5 class="card-title">Quick Actions</h5>
<div class="d-grid gap-2">
<a href="<?= APP_BASE_URL ?>/" class="btn btn-primary">Browse Products</a>
<a href="<?= APP_BASE_URL ?>/logout" class="btn btn-outline-secondary">Logout</a>
</div>
</div>
</div>
</div>
</div>
<?php ViewHelper::loadFooter(); ?>

Add to AuthController.php:

/**
* Display user dashboard (protected route).
*/
public function dashboard(Request $request, Response $response, array $args): Response
{
// TODO: Create a $data array with 'title' => 'Dashboard'
// TODO: Render 'user/dashboard.php' view and pass $data
}

Some routes require authentication while others do not. Open app/Routes/web-routes.php and follow the steps below.


Add these imports at the top of your routes file:

use App\Controllers\AuthController;
use App\Middleware\AuthMiddleware;
use App\Middleware\AdminAuthMiddleware;

The following routes do not require authentication. Users must be able to access the login and logout pages without being logged in.

// Public routes (no authentication required)
// TODO: Create a GET route for '/login' that maps to AuthController::class 'login' method
// Set the route name to 'auth.login'
// TODO: Create a POST route for '/login' that maps to AuthController::class 'authenticate' method
// TODO: Create a GET route for '/logout' that maps to AuthController::class 'logout' method
// Set the route name to 'auth.logout'

Step 7.3: Register Protected User Dashboard Route

Section titled “Step 7.3: Register Protected User Dashboard Route”

This route requires authentication but is available to all logged-in users (both admin and customer).

// TODO: Create a GET route for '/dashboard' that maps to AuthController::class 'dashboard' method
// Set the route name to 'user.dashboard'
// Apply AuthMiddleware using ->add(AuthMiddleware::class)

For example:

$app->get('/dashboard', [AuthController::class, 'dashboard'])
->setName('user.dashboard')
->add(AuthMiddleware::class);

The middleware checks the session before the route handler runs. If the user is not logged in, they are redirected to the login page. If they are logged in, the request proceeds to the dashboard.


Step 7.4: Protect Checkout Routes (Optional)

Section titled “Step 7.4: Protect Checkout Routes (Optional)”

If you have implemented a shopping cart, protect checkout routes with AuthMiddleware so that only logged-in users can place orders. Otherwise, skip this step.

// TODO: Create a route group for '/checkout' and apply AuthMiddleware
// $app->group('/checkout', function ($group) {
// $group->get('', [CartController::class, 'checkout']);
// $group->post('', [CartController::class, 'processCheckout']);
// })->add(AuthMiddleware::class);

Using a route group applies the middleware to all routes in the group at once, so you don’t need to add it to each route individually.


Apply AdminAuthMiddleware to your existing admin route group. This middleware checks both authentication AND the admin role, so regular customers are denied access even if they are logged in.

// TODO: Apply AdminAuthMiddleware to your existing admin route group
// Find your '/admin' route group and add ->add(AdminAuthMiddleware::class) at the end
//
// $app->group('/admin', function ($group) {
// $group->get('/dashboard', [AdminController::class, 'dashboard'])->setName('admin.dashboard');
// // ... your other admin routes
// })->add(AdminAuthMiddleware::class);

Make sure your admin dashboard route has the name 'admin.dashboard' so the login redirect works correctly for admin users.


  1. Visit http://localhost/[your-app]/login
  2. Enter valid credentials from Part 1
  3. Click “Login”
  4. You should see a success message and be redirected to the dashboard

Test with a wrong password, a non-existent email, and empty fields. In all cases, the error message should say “Invalid credentials” without revealing whether the email or username exists.

Try logging in with your username instead of email. It should work the same way.

  1. Without logging in, visit http://localhost/[your-app]/dashboard. You should be redirected to the login page with an error message.
  2. Log in, then visit /dashboard again. The dashboard should display your user information.
  1. Register a new user through the registration form, then open phpMyAdmin and change the role column to 'admin' for that user.
  2. Log in as a regular customer and try to visit /admin/dashboard. You should see “Access denied” and be redirected to the user dashboard.
  3. Log in as the admin user and visit /admin/dashboard. The admin dashboard should display successfully.
  1. While logged in, visit /logout
  2. You should see a success message and be redirected to the login page
  3. Try to access /dashboard again. You should be redirected to login since the session was destroyed.

After login, customers should be redirected to /dashboard and admins to /admin/dashboard.