Zend Framework

Zend Framework 3 (ZF3) Authentication & Authorization Tutorial – Step by Step Guide

Zend Framework 3 (ZF3) Authentication System – Part 1

Zend Framework 3 (ZF3) is a modern PHP MVC framework that allows developers to build scalable and modular web applications. When developing a real-world application, such as a blog, admin panel, or dashboard, having a proper authentication system is essential. Authentication ensures that certain pages are accessible only to registered and logged-in users, while public pages remain open to all visitors.

✅ What is Authentication?

Authentication is the process of verifying a user’s identity. The basic workflow includes:

  • User opens the login page.
  • User enters a username and password.
  • The system validates credentials against a database or authentication source.
  • If credentials match, a session is created, granting access to protected pages.
  • If credentials are invalid, an error message is displayed, and the user is redirected to the login page.

Without authentication, anyone could access sensitive areas of your application, which is a significant security risk. That’s why we create a dedicated Auth Module in ZF3.

✅ ZF3 Authentication Flow

In ZF3, authentication is modular and clean. A typical flow includes:

  • Create a login form (`login.phtml`) for user credentials.
  • Use a controller (`AuthController`) to validate credentials and start a session.
  • Manage sessions with Laminas\Session\Container.
  • Override `onDispatch()` in protected controllers to check if the user is logged in.
  • Implement logout functionality to destroy the session and redirect the user to the login page.

✅ Project Module Structure

Before implementing authentication, it’s essential to understand the project’s module structure. For example:

  • Application – public homepage and general pages
  • About – public module
  • Contact – public module
  • Blog – protected module (requires login)
  • Product – protected module (requires login)
  • Catalog – protected module (requires login)
  • Auth – new module for authentication (login/logout)

✅ Step 1: Create Users Table in Database

First, we need a database table to store login credentials. Example MySQL table:

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(100) NOT NULL,
  password VARCHAR(255) NOT NULL
);

Add a default admin user for testing:

INSERT INTO users (username, password)
VALUES ('admin', MD5('admin123'));

Note: For production, always use bcrypt or another secure hashing algorithm instead of MD5.

✅ Step 2: Create the Auth Module

Create a new folder Auth inside module/ and structure it like this:

  • module/Auth/config/module.config.php – module configuration and routes
  • module/Auth/src/Controller/AuthController.php – handles login/logout logic
  • module/Auth/src/Model/UserTable.php – optional model to interact with database
  • module/Auth/view/auth/login.phtml – login form template

✅ Step 3: Module Configuration

The module.config.php file defines routes, controllers, and view paths. Example:

<?php namespace Auth; use Laminas\Router\Http\Segment; return [ 'controllers' => [
        'factories' => [
            Controller\AuthController::class => function($container) {
                return new Controller\AuthController();
            },
        ],
    ],
    'router' => [
        'routes' => [
            'auth' => [
                'type'    => Segment::class,
                'options' => [
                    'route'    => '/auth[/:action]',
                    'defaults' => [
                        'controller' => Controller\AuthController::class,
                        'action'     => 'login',
                    ],
                ],
            ],
        ],
    ],
    'view_manager' => [
        'template_path_stack' => [ __DIR__ . '/../view', ],
    ],
];

✅ Step 4: Login Controller

The AuthController handles login and logout. Here’s an example:

<?php namespace Auth\Controller; use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; use Laminas\Session\Container; class AuthController extends AbstractActionController { public function loginAction() { $session = new Container('user'); $request = $this->getRequest();

        if ($session->offsetExists('username')) {
            return $this->redirect()->toUrl('/blog');
        }

        if ($request->isPost()) {
            $username = $request->getPost('username');
            $password = md5($request->getPost('password'));

            if ($username == 'admin' && $password == md5('admin123')) {
                $session->offsetSet('username', $username);
                return $this->redirect()->toUrl('/blog');
            }

            return new ViewModel(['error' => 'Invalid credentials']);
        }

        return new ViewModel();
    }

    public function logoutAction()
    {
        $session = new Container('user');
        $session->getManager()->destroy();
        return $this->redirect()->toUrl('/auth/login');
    }
}

This is the basic login/logout functionality. Next, we will create the login form template.

✅ Step 5: Login Form

The login.phtml file is the HTML form for login:

Login

If login fails, an error message appears:

{{error message here}}



✅ Step 6: Protecting Controllers

To protect modules like Blog, Product, Catalog, override onDispatch() in the controller to check the session:

use Laminas\Session\Container;
use Laminas\Mvc\MvcEvent;

public function onDispatch(MvcEvent $e)
{
    $session = new Container('user');

    if (!$session->offsetExists('username')) {
        return $this->redirect()->toUrl('/auth/login');
    }

    return parent::onDispatch($e);
}

This ensures that any user trying to access protected modules without logging in will be redirected to the login page.

🎯 Part 1 Summary

  • We covered the concept of authentication and why it is necessary in ZF3 projects.
  • We created a Users table in the database for storing credentials.
  • We set up an Auth Module with routes, controller, and views.
  • We created login/logout logic using Laminas session.
  • We learned how to protect modules by checking session in controllers.

In Part 2, we will cover database-driven authentication with a UserTable model, hashed passwords, adding logout links to the navbar, and implementing a proper admin dashboard layout.

Zend Framework 3 (ZF3) Authentication System – Part 2

✅ Step 7: Database-Driven Authentication

Instead of hardcoding credentials, we can use a UserTable model to validate login credentials from the database. This approach makes the authentication system scalable and secure.

UserTable Model Example


Clicking this link will destroy the session and redirect the user to the login page.

✅ Step 11: Protecting Multiple Modules

To secure multiple modules like Blog, Product, Catalog, we can create a reusable function or extend a base controller:

protected function checkAuth()
{
    $session = new \Laminas\Session\Container('user');
    if (!$session->offsetExists('username')) {
        return $this->redirect()->toUrl('/auth/login');
    }
}

Call $this->checkAuth() at the beginning of onDispatch() in each protected controller.

✅ Step 12: Admin Dashboard Layout

We can create a clean admin dashboard layout using your existing CSS classes:

Manage Blog Posts

Create, edit, or delete blog posts.

Manage Users

Add or remove users and assign roles.

View Analytics

Check page views and user activity.

This dashboard uses .course-box-container and .course-box-card CSS classes for a clean, responsive layout.

✅ Step 13: Error Handling and Security

  • Always escape output in views to prevent XSS attacks.
  • Use HTTPS in production for secure session management.
  • Limit login attempts to prevent brute-force attacks.
  • Consider adding CSRF tokens in login forms.
  • Use Laminas\Authentication adapters if you want LDAP or other authentication sources.

🎯 Part 2 Summary

  • We implemented database-driven authentication using a UserTable model.
  • We replaced MD5 with bcrypt hashing for secure passwords.
  • We updated the AuthController to verify credentials against the database.
  • We added a logout link in the main layout for easy user logout.
  • We showed how to protect multiple modules and create a reusable authentication check.
  • We designed a simple admin dashboard using your existing CSS classes.
  • We covered important security best practices.

In Part 3, we will cover advanced features: Role-Based Access Control (RBAC), user permissions, ACL, and integrating authentication with more complex modules.

 

Zend Framework 3 (ZF3) Authentication System – Part 3

✅ Step 14: Introduction to Authorization (RBAC)

Authorization is the process of determining what actions a logged-in user can perform within your application. While authentication verifies identity, authorization controls access to resources. In ZF3, we can implement Role-Based Access Control (RBAC) to assign roles to users (e.g., admin, editor, viewer) and define permissions.

✅ Step 15: Database Structure for Roles and Permissions

For RBAC, create additional tables in your database:

CREATE TABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    role_name VARCHAR(50) NOT NULL
);

CREATE TABLE permissions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    permission_name VARCHAR(50) NOT NULL
);

CREATE TABLE role_permissions (
    role_id INT NOT NULL,
    permission_id INT NOT NULL,
    PRIMARY KEY(role_id, permission_id)
);

CREATE TABLE user_roles (
    user_id INT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY(user_id, role_id)
);

This setup allows a user to have multiple roles, and roles to have multiple permissions. It’s scalable for complex applications.

✅ Step 16: User Role Table Model

We create a UserRoleTable model to fetch roles and permissions for a given user:


✅ Step 20: Combining Authentication and Authorization

With authentication and authorization combined:

  • Only logged-in users can access protected modules.
  • Users can perform actions based on their assigned roles.
  • Unauthorized access redirects users to a safe page.
  • The dashboard menu dynamically changes depending on user roles.

✅ Step 21: Advanced Security Best Practices

  • Use secure password hashing (bcrypt) and salting.
  • Enforce HTTPS for all protected routes.
  • Use Laminas\Authentication\Adapter\DbTable for database authentication.
  • Implement CSRF tokens in all forms.
  • Limit login attempts to prevent brute-force attacks.
  • Log all authentication attempts for audit purposes.
  • Use session timeouts for idle users.

🎯 Part 3 Summary

  • Introduced RBAC (Role-Based Access Control) for authorization in ZF3.
  • Created database tables for roles, permissions, and user-role mapping.
  • Implemented middleware/controller checks to restrict access based on roles.
  • Created an unauthorized page and dynamic menu rendering based on user roles.
  • Reviewed advanced security best practices for authentication and authorization.

In Part 4, we will complete the full system with final touches, session expiration, advanced dashboard widgets, logging, and production-ready deployment recommendations.

 

Zend Framework 3 (ZF3) Authentication System – Part 4

✅ Step 22: Session Expiration

For security, it’s important to expire sessions after a period of inactivity. Laminas\Session provides tools for this:

use Laminas\Session\Container;

$session = new Container('user');

// Set session expiration (e.g., 30 minutes)
$session->getManager()->rememberMe(1800); // 1800 seconds = 30 minutes

This ensures that if a user leaves the application idle, the session will automatically expire, and they will need to log in again.

✅ Step 23: Logging User Activity

Logging login attempts and user actions helps track security incidents and monitor usage. You can use Laminas\Log:

use Laminas\Log\Logger;
use Laminas\Log\Writer\Stream;

$logger = new Logger();
$writer = new Stream('./data/logs/auth.log');
$logger->addWriter($writer);

// Example: log login attempt
$logger->info('User admin logged in successfully');

You can log failed login attempts, logout actions, and restricted page access to enhance security auditing.

✅ Step 24: Advanced Dashboard Widgets

The admin dashboard can be enhanced with useful widgets. Using your existing CSS classes:

Recent Posts

Quick view of the last 5 blog posts.

Active Users

Number of currently logged-in users.

Pending Comments

Approve or reject user comments.

System Logs

Monitor login attempts and security events.

✅ Step 25: Protecting the Entire Admin Module

To secure the whole admin module:

namespace Admin\Controller;

use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\Session\Container;
use Laminas\Mvc\MvcEvent;

class AdminController extends AbstractActionController
{
    public function onDispatch(MvcEvent $e)
    {
        $session = new Container('user');
        if (!$session->offsetExists('username')) {
            return $this->redirect()->toUrl('/auth/login');
        }

        // Optional: check for admin role
        if (!in_array('admin', $session->roles)) {
            return $this->redirect()->toUrl('/auth/unauthorized');
        }

        return parent::onDispatch($e);
    }
}

✅ Step 26: CSRF Protection in Forms

For all forms (login, add/edit posts), enable CSRF protection using Laminas\Form\Element\Csrf:

use Laminas\Form\Element\Csrf;

$csrf = new Csrf('csrf');
$form->add($csrf);

This prevents cross-site request forgery attacks, ensuring only valid requests from your forms are processed.

✅ Step 27: Production Deployment Tips

  • Use HTTPS and SSL certificates for secure data transmission.
  • Set proper file permissions for data/logs and module/Auth/config.
  • Disable debug and display errors in production by setting display_errors=0 in php.ini.
  • Use OPcache to improve performance.
  • Monitor logs regularly and backup the database.
  • Regularly update Laminas components to patch security vulnerabilities.

🎯 Step 28: Full Authentication + Authorization Summary

  • Part 1: Authentication concept, login flow, module setup, basic login/logout, protecting controllers.
  • Part 2: Database-driven authentication, bcrypt hashed passwords, logout links, admin dashboard layout.
  • Part 3: Role-Based Access Control (RBAC), user permissions, middleware/controller authorization, dynamic menus, unauthorized page.
  • Part 4: Session expiration, logging user activity, advanced dashboard widgets, CSRF protection, production deployment tips.

✅ Final Notes

By implementing the steps in all 4 parts, you now have a **robust ZF3 authentication and authorization system**:

  • Secure login/logout with hashed passwords.
  • Session management with expiration.
  • Role-based access control for fine-grained authorization.
  • Protected modules and dynamic dashboard features.
  • Security best practices including CSRF, HTTPS, logging, and limited access.
  • Production-ready deployment tips for secure and efficient applications.

With this foundation, you can now expand your application with additional features such as user registration, password reset, multi-role support, and audit trails.

Complete ZF3 Authentication Module  Code

✅ Step 1: Module Structure

A complete Auth module in Zend Framework 3 typically looks like this:

module/Auth/
├── config/module.config.php
├── src/Controller/AuthController.php
├── src/Model/User.php
├── src/Model/UserTable.php
├── src/Model/UserRoleTable.php
├── view/auth/login.phtml
├── view/auth/dashboard.phtml
├── view/auth/unauthorized.phtml
└── Module.php

✅ Step 3: module.config.php

<?php
namespace Auth;

use Laminas\Router\Http\Segment;

return [
    'controllers' => [
        'factories' => [
            Controller\AuthController::class => function($container) {
                return new Controller\AuthController(
                    $container->get(Model\UserTable::class),
                    $container->get(Model\UserRoleTable::class)
                );
            },
        ],
    ],
    'router' => [
        'routes' => [
            'auth' => [
                'type' => Segment::class,
                'options' => [
                    'route' => '/auth[/:action]',
                    'defaults' => [
                        'controller' => Controller\AuthController::class,
                        'action' => 'login',
                    ],
                ],
            ],
        ],
    ],
    'view_manager' => [
        'template_path_stack' => [
            __DIR__ . '/../view',
        ],
    ],
];

✅ Step 4: AuthController.php

<?php
namespace Auth\Controller;

use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
use Laminas\Session\Container;
use Auth\Model\UserTable;
use Auth\Model\UserRoleTable;

class AuthController extends AbstractActionController
{
    private $userTable;
    private $userRoleTable;

    public function __construct(UserTable $userTable, UserRoleTable $userRoleTable)
    {
        $this->userTable = $userTable;
        $this->userRoleTable = $userRoleTable;
    }

    public function loginAction()
    {
        $session = new Container('user');
        $request = $this->getRequest();

        if ($session->offsetExists('username')) {
            return $this->redirect()->toUrl('/auth/dashboard');
        }

        if ($request->isPost()) {
            $username = $request->getPost('username');
            $password = $request->getPost('password');

            $user = $this->userTable->getUser($username);
            if ($user && password_verify($password, $user->password)) {
                $session->offsetSet('username', $username);
                $session->offsetSet('userId', $user->id);
                $roles = $this->userRoleTable->getUserRoles($user->id);
                $session->offsetSet('roles', $roles);
                return $this->redirect()->toUrl('/auth/dashboard');
            }

            return new ViewModel(['error' => 'Invalid credentials']);
        }

        return new ViewModel();
    }

    public function logoutAction()
    {
        $session = new Container('user');
        $session->getManager()->destroy();
        return $this->redirect()->toUrl('/auth/login');
    }

    public function dashboardAction()
    {
        $session = new Container('user');
        if (!$session->offsetExists('username')) {
            return $this->redirect()->toUrl('/auth/login');
        }

        return new ViewModel(['username' => $session->username]);
    }

    public function unauthorizedAction()
    {
        return new ViewModel();
    }
}

✅ Step 5: User.php

<?php
namespace Auth\Model;

class User
{
    public $id;
    public $username;
    public $password;

    public function exchangeArray(array $data)
    {
        $this->id = $data['id'] ?? null;
        $this->username = $data['username'] ?? null;
        $this->password = $data['password'] ?? null;
    }
}

✅ Step 6: UserTable.php

<?php
namespace Auth\Model;

use Laminas\Db\TableGateway\TableGatewayInterface;

class UserTable
{
    private $tableGateway;

    public function __construct(TableGatewayInterface $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function getUser($username)
    {
        $rowset = $this->tableGateway->select(['username' => $username]);
        return $rowset->current();
    }
}

✅ Step 7: UserRoleTable.php

<?php
namespace Auth\Model;

use Laminas\Db\TableGateway\TableGatewayInterface;

class UserRoleTable
{
    private $tableGateway;

    public function __construct(TableGatewayInterface $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function getUserRoles($userId)
    {
        $rowset = $this->tableGateway->select(['user_id' => $userId]);
        $roles = [];
        foreach ($rowset as $row) {
            $roles[] = $row->role_name;
        }
        return $roles;
    }
}

✅ Step 8: login.phtml

Login

(Shows error if login fails)



✅ Step 9: dashboard.phtml

Admin Dashboard

Welcome, Admin!

Manage Blog Posts

Edit, add, or delete blog posts.

Manage Users

Add or edit user accounts and roles.

View Analytics

Monitor site traffic and user activity.

Logout

✅ Step 10: unauthorized.phtml

Access Denied

You do not have permission to access this page.

Go to Login

✅ Step 11: Summary & Best Practices

  • Use bcrypt password hashing for security.
  • Store roles in session for RBAC control.
  • Protect admin pages using session & role checks.
  • Use CSRF tokens on all forms.
  • Expire sessions after inactivity for safety.
  • Log login attempts for audit purposes.
  • Deploy securely with HTTPS and proper file permissions.

Login Page

<?php if(isset($this->error)): ?>
<p style="color:red;"><?= $this->error ?></p>
<?php endif; ?>

<form method="post">
  <p><label>Username</label><br><input type="text" name="username" required></p>
  <p><label>Password</label><br><input type="password" name="password" required></p>
  <p><input type="submit" value="Login"></p>
</form>

After successful login, you are redirected to the Dashboard.

Admin Dashboard

<p>Welcome, <strong><?= $this->username ?? 'User' ?></strong>!</p>

<div class="course-box-container">
  <div class="course-box-card">
    <h3>Manage Blog Posts</h3>
    <p>Add, edit, or delete posts in your blog module.</p>
  </div>
  <div class="course-box-card">
    <h3>Manage Users</h3>
    <p>Create new users, assign roles, or edit permissions.</p>
  </div>
  <div class="course-box-card">
    <h3>View Analytics</h3>
    <p>Monitor site traffic, login activity, and user engagement.</p>
  </div>
</div>

<p><a href="/auth/logout">Logout</a></p>

Access Denied

<p>You do not have permission to access this page.</p>
<p>If you think this is an error, please contact the administrator.</p>
<p><a href="/auth/login">Go to Login Page</a></p>

Leave a Reply

Your email address will not be published. Required fields are marked *