HOW TO Build a Secure User Authentication System in PHP

In today’s digital landscape, securing user data is paramount. Implementing a robust user authentication system ensures that only authorized individuals can access specific parts of your application. This guide will walk you through creating an advanced user authentication system in PHP using the Slim Framework, Eloquent ORM, and JWT (JSON Web Tokens) for secure authentication. We’ll cover user registration, login, token generation, and protecting routes to ensure only authenticated users can access certain endpoints. Whether you’re building a simple website or a complex web application, mastering authentication is essential for safeguarding your users and data.

About the Author

[SRIJAN ACHARYA] is a seasoned web developer with extensive experience in PHP and modern web technologies. Passionate about creating secure and efficient applications, [Your Name] enjoys sharing knowledge through comprehensive tutorials and guides to help others excel in web development.

Prerequisites

Before diving into the code, ensure you have the following set up:

Familiarity with JWT concepts is beneficial but not mandator

PHP 7.4 or higher installed on your system.

Composer for managing dependencies.

MySQL or any other relational database.

Setting Up the Project

1. Initialize the Project Directory

Start by creating a new directory for your project and navigating into it:

Basic knowledge of PHP, SQL, and RESTful principles.

mkdir php-authentication
cd php-authentication
Bash

2. Initialize Composer

Initialize Composer to manage your project dependencies:

composer init
Bash

Follow the prompts to set up your composer.json file.

3. Install Necessary Packages

We’ll use Slim Framework for routing, Eloquent ORM for database interactions, and Firebase PHP-JWT for handling JWT tokens:

composer require slim/slim "^4.0"
composer require illuminate/database "^8.0"
composer require slim/psr7
composer require firebase/php-jwt
Bash

Creating the Database

For this authentication system, we’ll create a simple users table.

Create a Database

CREATE DATABASE user_auth;
Bash

Create a users Table

USE user_auth;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Bash

Building the Authentication System

1. Setting Up the Entry Point

Create an index.php file at the root of your project:

<?php
require 'vendor/autoload.php';

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Illuminate\Database\Capsule\Manager as Capsule;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Initialize Slim App
$app = AppFactory::create();

// Set up Eloquent ORM
$capsule = new Capsule;
$capsule->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'user_auth',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
]);

$capsule->setAsGlobal();
$capsule->bootEloquent();

// Secret key for JWT
$secretKey = "your-secret-key";

// Middleware to handle JSON parsing
$app->addBodyParsingMiddleware();

// Define Routes here

$app->run();
Bash

Explanation:

Line 34: Runs the Slim application to handle incoming requests.

Line 1: Starts the PHP script.

Line 2: Includes Composer’s autoload to manage dependencies.

Lines 4-8: Import necessary classes from Slim, Eloquent, and JWT libraries.

Line 11: Initializes the Slim application.

Lines 14-23: Configures Eloquent ORM with database credentials and boots it.

Line 26: Defines a secret key used for signing JWT tokens. Ensure you use a strong, unique key in production.

Line 29: Adds middleware to parse JSON bodies in incoming requests.

Line 32: Placeholder for defining API routes.

2. Defining the User Model

Create a User.php file inside a models directory:

<?php
namespace Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model {
    protected $table = 'users';
    protected $fillable = ['name', 'email', 'password'];
    protected $hidden = ['password'];
}
Bash

Explanation:

Line 9: Hides the password attribute when the model is converted to arrays or JSON.

Line 1: Starts the PHP script.

Line 2: Defines the namespace for the model.

Line 4: Imports the Eloquent Model class.

Line 6: Defines the User class extending Eloquent’s Model.

Line 7: Specifies the table associated with the model.

Line 8: Defines which attributes are mass assignable.

3. Creating API Routes

Back in index.php, define the API routes for user registration, login, and protected access:

use Models\User;

// User Registration
$app->post('/register', function (Request $request, Response $response, $args) use ($secretKey) {
    $data = $request->getParsedBody();

    // Validate input
    if (!isset($data['name']) || !isset($data['email']) || !isset($data['password'])) {
        $response->getBody()->write(json_encode(['message' => 'Invalid input']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
    }

    // Check if user already exists
    $user = User::where('email', $data['email'])->first();
    if ($user) {
        $response->getBody()->write(json_encode(['message' => 'User already exists']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(409);
    }

    // Hash password
    $hashedPassword = password_hash($data['password'], PASSWORD_DEFAULT);

    // Create new user
    $newUser = User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => $hashedPassword
    ]);

    // Generate JWT Token
    $token = JWT::encode(['id' => $newUser->id, 'email' => $newUser->email], $secretKey, 'HS256');

    // Respond with token
    $response->getBody()->write(json_encode(['token' => $token]));
    return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
});

// User Login
$app->post('/login', function (Request $request, Response $response, $args) use ($secretKey) {
    $data = $request->getParsedBody();

    // Validate input
    if (!isset($data['email']) || !isset($data['password'])) {
        $response->getBody()->write(json_encode(['message' => 'Invalid input']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
    }

    // Find user by email
    $user = User::where('email', $data['email'])->first();
    if (!$user) {
        $response->getBody()->write(json_encode(['message' => 'User not found']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
    }

    // Verify password
    if (!password_verify($data['password'], $user->password)) {
        $response->getBody()->write(json_encode(['message' => 'Invalid credentials']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
    }

    // Generate JWT Token
    $token = JWT::encode(['id' => $user->id, 'email' => $user->email], $secretKey, 'HS256');

    // Respond with token
    $response->getBody()->write(json_encode(['token' => $token]));
    return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
});

// Protected Route Example
$app->get('/profile', function (Request $request, Response $response, $args) {
    $user = $request->getAttribute('user');
    $response->getBody()->write(json_encode(['user' => $user]));
    return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
})->add(function (Request $request, Response $response, $next) use ($secretKey) {
    $authHeader = $request->getHeader('Authorization');
    if (!$authHeader) {
        $response->getBody()->write(json_encode(['message' => 'Authorization header missing']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
    }

    $token = explode(" ", $authHeader[0])[1] ?? '';
    if (!$token) {
        $response->getBody()->write(json_encode(['message' => 'Token not provided']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
    }

    try {
        $decoded = JWT::decode($token, new Key($secretKey, 'HS256'));
        $user = User::find($decoded->id);
        if (!$user) {
            $response->getBody()->write(json_encode(['message' => 'User not found']));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
        }
        // Add user to request attributes
        $request = $request->withAttribute('user', $user);
        return $next($request, $response);
    } catch (Exception $e) {
        $response->getBody()->write(json_encode(['message' => 'Invalid token']));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
    }
});
Bash

Explanation:

  • Line 36: Imports the User model.
Route 1: User Registration
  • Line 39: Defines a POST route at /register.
  • Line 40: Retrieves the parsed body of the request.
  • Lines 43-46: Validates the input to ensure name, email, and password are provided.
  • Lines 49-53: Checks if a user with the provided email already exists. If so, returns a 409 Conflict status.
  • Line 56: Hashes the user’s password using PHP’s password_hash function for security.
  • Lines 59-63: Creates a new user record in the database with the provided and hashed data.
  • Line 66: Generates a JWT token containing the user’s id and email, signed with the secret key.
  • Lines 69-70: Responds with the generated token and a 201 Created status.
Route 2: User Login
  • Line 73: Defines a POST route at /login.
  • Line 74: Retrieves the parsed body of the request.
  • Lines 77-80: Validates the input to ensure email and password are provided.
  • Lines 83-86: Searches for the user by email. If not found, returns a 404 Not Found status.
  • Lines 89-92: Verifies the provided password against the hashed password in the database. If invalid, returns a 401 Unauthorized status.
  • Line 95: Generates a JWT token for the authenticated user.
  • Lines 98-99: Responds with the generated token and a 200 OK status.
Route 3: Protected Route Example
  • Line 102: Defines a GET route at /profile.
  • Line 103: Retrieves the authenticated user from the request attributes.
  • Line 104: Responds with the user’s data.
  • Line 105: Sets the Content-Type header and returns the response with a 200 OK status.
Middleware: Token Validation

Lines 119-128: Attempts to decode the JWT token using the secret key. If successful, retrieves the user from the database and adds the user to the request attributes. If decoding fails, responds with a 401 Unauthorized status.

Line 106: Adds middleware to the /profile route to handle JWT validation.

Line 107: Retrieves the Authorization header from the incoming request.

Lines 108-111: If the Authorization header is missing, responds with a 401 Unauthorized status.

Line 113: Extracts the token from the Authorization header. The expected format is Bearer <token>.

Lines 114-117: If the token is not provided, responds with a 401 Unauthorized status.

4. Testing the Authentication System

You can use tools like Postman or cURL to test the API endpoints.

Example: User Registration

Request:

POST /register HTTP/1.1
Host: localhost:8000
Content-Type: application/json

{
    "name": "John Doe",
    "email": "john.doe@example.com",
    "password": "SecurePassword123"
}
Bash

Response:

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
Bash

Example: User Login

Request:

POST /login HTTP/1.1
Host: localhost:8000
Content-Type: application/json

{
    "email": "john.doe@example.com",
    "password": "SecurePassword123"
}
Bash

Response:

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
Bash

Example: Accessing a Protected Route

Request:

GET /profile HTTP/1.1
Host: localhost:8000
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
Bash

Response:

{
    "user": {
        "id": 1,
        "name": "John Doe",
        "email": "john.doe@example.com",
        "created_at": "2024-04-27T12:34:56"
    }
}
Bash

Running the Application

Start the PHP built-in server to run your application:

php -S localhost:8000
Bash
Your authentication system is now accessible at http://localhost:8000. You can use Postman, cURL, or any other API testing tool to interact with the endpoints.

Conclusion

Creating a secure user authentication system is a foundational aspect of modern web development. By leveraging the Slim Framework for routing, Eloquent ORM for database interactions, and JWT for token-based authentication, you can build a scalable and secure authentication mechanism in PHP. This guide provided a comprehensive walkthrough, from setting up the project to implementing registration, login, and protected routes. With this foundation, you can further enhance your system by adding features like password reset, email verification, role-based access control, and more to meet the evolving needs of your application.


References


Additional Resources


Leave a Reply

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

Resize text
Scroll to Top