Skip to content

Authentication

Precious Lyson edited this page Nov 13, 2026 · 2 revisions

Features

  • Multiple Authentication Guards: Session, Token, JWT, and OAuth
  • Multiple User Providers: Database, LDAP, OAuth
  • Third-Party OAuth Support: Google, GitHub, Facebook, Twitter, Yahoo, LinkedIn, Microsoft, Instagram, GitLab, Bitbucket, Amazon, Slack
  • Remember Me Functionality: Secure token-based persistent login
  • Session Security: Fingerprinting, timeout, regeneration
  • LDAP Integration: Enterprise directory authentication
  • Event System: Hooks for login, logout, and failure events
  • Caching: Built-in cache support for user lookups
  • Middleware: Authentication and authorization middleware
  • Rate Limiting: Throttle middleware for brute-force protection
  • Extensible: Easy to add custom guards and providers

Quick Start

1. Basic Setup

use Mlangeni\Machinjiri\Core\Authentication\Auth;

// Check if user is logged in
if (Auth::check()) {
    $user = Auth::user();
    echo "Welcome, " . $user->name;
}

// Attempt login
if (Auth::attempt(['email' => 'user@example.com', 'password' => 'secret'])) {
    // Login successful
    redirect('/dashboard');
}

// Logout
Auth::logout();

2. Protect Routes with Middleware

use Mlangeni\Machinjiri\Core\Authentication\Middleware\Authenticate;

// In your route definition
$router->get('/dashboard', [DashboardController::class, 'index'])
    ->middleware(Authenticate::class);

3. Using Multiple Guards

// Use the API token guard
if (Auth::guard('api')->check()) {
    $user = Auth::guard('api')->user();
}

// Use the JWT guard
if (Auth::guard('jwt')->check()) {
    $user = Auth::guard('jwt')->user();
}

Configuration

Configuration File Structure

Create a configuration array for the auth system:

// config/auth.php
return [
    'defaults' => [
        'guard' => 'session',
        'provider' => 'users',
    ],

    'guards' => [
        'session' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],
        'jwt' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
        'ldap' => [
            'driver' => 'session',
            'provider' => 'ldap',
        ],
        'oauth' => [
            'driver' => 'session',
            'provider' => 'oauth',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'database',
            'model' => App\Models\User::class,
            'table' => 'users',
        ],
        'ldap' => [
            'driver' => 'ldap',
            'model' => Mlangeni\Machinjiri\Core\Authentication\Models\LdapModel::class,
            'username_attribute' => 'uid',
            'search_fields' => ['uid', 'mail'],
            'sync_attributes' => [
                'username' => 'uid',
                'email' => 'mail',
                'name' => 'cn',
            ],
        ],
        'oauth' => [
            'driver' => 'oauth',
            'model' => App\Models\User::class,
        ],
    ],

    'session' => [
        'lifetime' => 120, // minutes
        'cookie' => 'machinjiri_session',
        'path' => '/',
        'domain' => '',
        'secure' => false,
        'http_only' => true,
        'same_site' => 'lax',
    ],

    'remember' => [
        'expiration' => 2592000, // 30 days in seconds
    ],
];

Environment Variables for OAuth

# Google
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

# GitHub
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

# Facebook
FACEBOOK_CLIENT_ID=your-facebook-client-id
FACEBOOK_CLIENT_SECRET=your-facebook-client-secret

# Twitter
TWITTER_CLIENT_ID=your-twitter-client-id
TWITTER_CLIENT_SECRET=your-twitter-client-secret

# LinkedIn
LINKEDIN_CLIENT_ID=your-linkedin-client-id
LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret

# Microsoft
MICROSOFT_CLIENT_ID=your-microsoft-client-id
MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret

# GitLab
GITLAB_CLIENT_ID=your-gitlab-client-id
GITLAB_CLIENT_SECRET=your-gitlab-client-secret

# Application URL (for OAuth callbacks)
APP_URL=https://your-app.com

Core Concepts

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                         Application                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐  │
│  │    Auth     │───▶│ AuthManager │───▶│       Guards        │  │
│  │  (Facade)   │    │             │    │                     │  │
│  └─────────────┘    └─────────────┘    │  ┌───────────────┐  │  │
│                            │           │  │  SessionGuard │  │  │
│                            │           │  ├───────────────┤  │  │
│                            │           │  │  TokenGuard   │  │  │
│                            │           │  ├───────────────┤  │  │
│                            │           │  │   JwtGuard    │  │  │
│                            │           │  └───────────────┘  │  │
│                            │           └─────────────────────┘  │
│                            │                     │               │
│                            ▼                     ▼               │
│                   ┌─────────────────┐   ┌─────────────────────┐ │
│                   │ UserProviders   │   │   Authenticatable   │ │
│                   │                 │   │      (User)         │ │
│                   │ ┌─────────────┐ │   └─────────────────────┘ │
│                   │ │  Database   │ │                            │
│                   │ ├─────────────┤ │                            │
│                   │ │    LDAP     │ │                            │
│                   │ ├─────────────┤ │                            │
│                   │ │   OAuth     │ │                            │
│                   │ └─────────────┘ │                            │
│                   └─────────────────┘                            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Key Components

Component Description
Auth Static facade for easy access
AuthManager Central manager that coordinates guards
Guard Interface for authentication strategies
UserProvider Interface for retrieving users
Authenticatable Interface for user models

Authentication Guards

Session Guard

The default guard for web applications. Stores authentication state in the session.

// Login with session
Auth::guard('session')->attempt($credentials, $remember = false);

// Check authentication
Auth::guard('session')->check();

Features:

  • Session-based state persistence
  • "Remember me" cookie support
  • Session fingerprinting for security
  • Automatic session regeneration

Token Guard

For API authentication using static tokens.

// Token is passed via Authorization header or query parameter
// Authorization: Bearer <token>
// or ?api_token=<token>

Auth::guard('api')->check();

Features:

  • Stateless authentication
  • Token stored in api_token column
  • Supports Bearer tokens

JWT Guard

For JSON Web Token authentication.

// Pass JWT via Authorization header
// Authorization: Bearer <jwt>

Auth::guard('jwt')->check();

// Generate a token
$token = Auth::guard('jwt')->generateToken($user, ['role' => 'admin']);

Features:

  • Stateless authentication
  • Custom claims support
  • Token expiration handling

User Providers

Database User Provider

Retrieves users from a database table.

use Mlangeni\Machinjiri\Core\Authentication\UserProviders\DatabaseUserProvider;

$provider = new DatabaseUserProvider(
    $queryBuilder,
    User::class,
    $events,
    $logger,
    $cache,
    $cacheTtl = 300
);

LDAP User Provider

Authenticates against an LDAP directory.

use Mlangeni\Machinjiri\Core\Authentication\UserProviders\LdapUserProvider;

$provider = new LdapUserProvider(
    $ldapManager,
    LdapModel::class,
    $syncAttributes,
    $config
);

OAuth User Provider

Handles OAuth-authenticated users.

use Mlangeni\Machinjiri\Core\Authentication\UserProviders\OAuthUserProvider;

$provider = new OAuthUserProvider(
    $thirdPartyAuth,
    User::class,
    $logger,
    $events
);

Session Authentication

Basic Usage

use Mlangeni\Machinjiri\Core\Authentication\Auth;

// Attempt authentication
if (Auth::attempt(['email' => $email, 'password' => $password])) {
    // Success - user is now logged in
    return redirect('/dashboard');
}

// With "remember me"
if (Auth::attempt($credentials, $remember = true)) {
    // User will stay logged in for 30 days
}

// Check authentication status
if (Auth::check()) {
    $user = Auth::user();
    $id = Auth::id();
}

// Guest check
if (Auth::guest()) {
    // User is not logged in
}

// Logout
Auth::logout();

Manual Login

$user = User::find(1);

// Login without credentials
Auth::login($user);

// Login with "remember me"
Auth::login($user, $remember = true);

Validation Only

// Check credentials without logging in
if (Auth::validate(['email' => $email, 'password' => $password])) {
    // Credentials are valid, but user is not logged in
}

Custom User Class

Your User model must implement the Authenticatable interface:

<?php

namespace App\Models;

use Mlangeni\Machinjiri\Core\Authentication\Authenticatable;
use Mlangeni\Machinjiri\Core\Artisans\Base\AbstractModel;

class User extends AbstractModel implements Authenticatable
{
    protected string $table = 'users';
    protected string $primaryKey = 'id';
    protected array $fillable = ['name', 'email', 'password', 'remember_token'];
    protected array $hidden = ['password', 'remember_token'];

    public function getAuthIdentifier(): mixed
    {
        return $this->getAttribute($this->primaryKey);
    }

    public function getAuthPassword(): string
    {
        return $this->getAttribute('password') ?? '';
    }

    public function getRememberToken(): string
    {
        return $this->getAttribute('remember_token') ?? '';
    }

    public function setRememberToken(string $token): void
    {
        $this->setAttribute('remember_token', $token);
    }

    public function getRememberTokenName(): string
    {
        return 'remember_token';
    }

    public function getAuthIdentifierName(): string
    {
        return $this->primaryKey;
    }

    public function hasRole(string $role): bool
    {
        return $this->getAttribute('role') === $role;
    }

    public function hasPermission(string $permission): bool
    {
        $permissions = $this->getAttribute('permissions') ?? [];
        return in_array($permission, $permissions);
    }
}

Remember Me Implementation

The "remember me" feature uses a secure token stored in both the database and a cookie:

  1. On Login with Remember:

    • Generate a random 32-byte token
    • Hash the token and store in database
    • Set cookie with userId|plainToken
  2. On Subsequent Request:

    • Parse cookie to extract user ID and plain token
    • Retrieve user from database
    • Verify plain token against hashed token
    • If valid, regenerate session and log in
  3. Security Features:

    • Tokens are hashed before storage
    • Constant-time comparison
    • Token rotation on login

Token Authentication

Setup

Add an api_token column to your users table:

ALTER TABLE users ADD COLUMN api_token VARCHAR(255) UNIQUE NULL;

Usage

// Client sends token in header
// Authorization: Bearer your-api-token-here

// Or as query parameter
// GET /api/resource?api_token=your-api-token-here

use Mlangeni\Machinjiri\Core\Authentication\Auth;

if (Auth::guard('api')->check()) {
    $user = Auth::guard('api')->user();
    // Process API request
}

Generating Tokens

$user = User::find(1);
$token = bin2hex(random_bytes(32));
$user->api_token = hash('sha256', $token);
$user->save();

// Return the plain token to the client
return json_encode(['token' => $token]);

Protecting API Routes

$router->group(['middleware' => [Authenticate::class . ':api']], function ($router) {
    $router->get('/api/user', [UserController::class, 'show']);
    $router->get('/api/posts', [PostController::class, 'index']);
});

JWT Authentication

Setup

JWT authentication requires the Bangwe encryption class from the core package.

Generating Tokens

use Mlangeni\Machinjiri\Core\Authentication\Auth;

$user = User::find(1);

// Basic token
$token = Auth::guard('jwt')->generateToken($user);

// Token with custom claims
$token = Auth::guard('jwt')->generateToken($user, [
    'role' => 'admin',
    'permissions' => ['read', 'write'],
]);

Using Tokens

// Client sends JWT in header
// Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...

// Or as query parameter
// GET /api/resource?token=eyJ0eXAiOiJKV1QiLCJhbGc...

if (Auth::guard('jwt')->check()) {
    $user = Auth::guard('jwt')->user();
}

// Validate token without authentication
if (Auth::guard('jwt')->validate(['token' => $token])) {
    // Token is valid
}

Token Payload

The JWT contains:

{
    "sub": 1,
    "iat": 1640000000,
    "exp": 1640003600,
    "role": "admin"
}

OAuth / Third-Party Authentication

Supported Providers

Provider Authorization Endpoint User Info Endpoint
Google accounts.google.com googleapis.com/oauth2/v3/userinfo
GitHub github.com/login/oauth api.github.com/user
Facebook facebook.com/dialog/oauth graph.facebook.com/me
Twitter twitter.com/i/oauth2 api.twitter.com/2/users/me
LinkedIn linkedin.com/oauth/v2 api.linkedin.com/v2/me
Microsoft login.microsoftonline.com graph.microsoft.com/v1.0/me
GitLab gitlab.com/oauth gitlab.com/api/v4/user
Bitbucket bitbucket.org/site/oauth2 api.bitbucket.org/2.0/user
Amazon amazon.com/ap/oa api.amazon.com/user/profile
Slack slack.com/oauth/v2 slack.com/api/users.profile.get
Yahoo api.login.yahoo.com api.login.yahoo.com/openid/v1/userinfo
Instagram api.instagram.com/oauth graph.instagram.com/me

Setup

  1. Register your application with each provider
  2. Add credentials to .env
  3. Configure the ThirdPartyAuth service

Basic Flow

use Mlangeni\Machinjiri\Core\Authentication\ThirdParty\ThirdPartyAuth;
use Mlangeni\Machinjiri\Core\Authentication\Session;
use Mlangeni\Machinjiri\Core\Authentication\Cookie;

// Initialize
$thirdPartyAuth = new ThirdPartyAuth([
    'redirect_uri' => 'https://your-app.com/auth/callback',
    'auto_create_users' => true,
    'default_role' => 'user',
]);

// Step 1: Redirect to provider
$router->get('/auth/{provider}', function ($request, $response, $provider) use ($thirdPartyAuth) {
    $thirdPartyAuth->redirectToProvider($provider, $response);
});

// Step 2: Handle callback
$router->get('/auth/callback', function ($request, $response) use ($thirdPartyAuth) {
    try {
        $result = $thirdPartyAuth->handleCallback($request);
        
        // Log the user in
        Auth::login(new User($result['user']));
        
        return $response->redirect('/dashboard');
    } catch (Exception $e) {
        return $response->redirect('/login?error=' . urlencode($e->getMessage()));
    }
});

Advanced OAuth Usage

Custom User Mapping

$thirdPartyAuth->setUserMapping([
    'id' => 'provider_user_id',
    'name' => 'display_name',
    'email' => 'email_address',
    'avatar' => 'profile_image',
]);

Manual Token Authentication

// Authenticate with an access token directly
$user = $thirdPartyAuth->authenticateWithToken('google', $accessToken);

if ($user) {
    Auth::login(new User($user));
}

Getting User Info

// Get user info without creating/updating user
$userInfo = $thirdPartyAuth->getUserInfo('github', $accessToken);

// Returns:
// [
//     'provider' => 'github',
//     'provider_id' => '12345',
//     'name' => 'John Doe',
//     'email' => 'john@example.com',
//     'avatar_url' => 'https://avatars.githubusercontent.com/u/12345',
// ]

Managing Provider Connections

// Get all connected providers for a user
$providers = $thirdPartyAuth->getUserProviders($userId);

// Disconnect a provider
$thirdPartyAuth->disconnectProvider('github', $userId);

// Refresh an access token
$newToken = $thirdPartyAuth->refreshToken('google', $userId);

Generating Login Buttons

// Single button
echo $thirdPartyAuth->getProviderButton('google', ['class' => 'btn btn-google']);

// All available buttons
echo $thirdPartyAuth->getLoginButtons();

// Specific buttons
echo $thirdPartyAuth->getLoginButtons(['google', 'github'], ['class' => 'btn btn-social']);

Database Schema for OAuth

-- Users table (if not already exists)
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255),
    email VARCHAR(255) UNIQUE,
    password VARCHAR(255) NULL,
    avatar_url VARCHAR(500) NULL,
    email_verified_at TIMESTAMP NULL,
    role VARCHAR(50) DEFAULT 'user',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Provider connections
CREATE TABLE user_providers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    provider VARCHAR(50) NOT NULL,
    provider_id VARCHAR(255) NOT NULL,
    access_token TEXT,
    refresh_token TEXT,
    token_expires TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY unique_provider (provider, provider_id)
);

-- Token storage
CREATE TABLE user_tokens (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    provider VARCHAR(50) NOT NULL,
    access_token TEXT,
    refresh_token TEXT,
    token_type VARCHAR(50) DEFAULT 'bearer',
    expires_in INT,
    scope TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

Complete OAuth Example

<?php

namespace App\Controllers;

use Mlangeni\Machinjiri\Core\Authentication\Auth;
use Mlangeni\Machinjiri\Core\Authentication\ThirdParty\ThirdPartyAuth;
use App\Models\User;

class AuthController
{
    private ThirdPartyAuth $oauth;

    public function __construct()
    {
        $this->oauth = new ThirdPartyAuth([
            'redirect_uri' => env('APP_URL') . '/auth/callback',
            'auto_create_users' => true,
            'auto_sync_profile' => true,
            'default_role' => 'user',
        ]);
    }

    /**
     * Show login page with OAuth buttons
     */
    public function showLogin($request, $response)
    {
        $buttons = $this->oauth->getLoginButtons(['google', 'github'], [
            'class' => 'btn btn-outline-dark w-100 mb-2'
        ]);

        return $response->render('auth/login', ['buttons' => $buttons]);
    }

    /**
     * Redirect to OAuth provider
     */
    public function redirectToProvider($request, $response, string $provider)
    {
        try {
            $this->oauth->redirectToProvider($provider, $response);
        } catch (Exception $e) {
            return $response->redirect('/login?error=' . urlencode($e->getMessage()));
        }
    }

    /**
     * Handle OAuth callback
     */
    public function handleCallback($request, $response)
    {
        try {
            $result = $this->oauth->handleCallback($request);
            
            // Find or create local user
            $userData = $result['user'];
            $user = User::find($userData['id']);
            
            if (!$user) {
                $user = new User($userData);
            }

            // Log the user in
            Auth::login($user, $remember = true);

            // Redirect to intended page or dashboard
            $intended = session('url.intended', '/dashboard');
            return $response->redirect($intended);
        } catch (Exception $e) {
            logger()->error('OAuth callback failed', ['error' => $e->getMessage()]);
            return $response->redirect('/login?error=authentication_failed');
        }
    }

    /**
     * Logout
     */
    public function logout($request, $response)
    {
        Auth::logout();
        $this->oauth->logout();
        
        return $response->redirect('/login');
    }

    /**
     * Link additional provider to existing account
     */
    public function linkProvider($request, $response, string $provider)
    {
        if (!Auth::check()) {
            return $response->redirect('/login');
        }

        // Store current user ID in session
        session(['link_user_id' => Auth::id()]);
        
        $this->oauth->redirectToProvider($provider, $response);
    }

    /**
     * Handle provider linking callback
     */
    public function handleLinkCallback($request, $response)
    {
        $userId = session('link_user_id');
        if (!$userId) {
            return $response->redirect('/settings');
        }

        try {
            $result = $this->oauth->handleCallback($request);
            
            // Link provider to existing user
            // The processUser method will find the existing user by email
            // and link the provider
            
            session()->remove('link_user_id');
            return $response->redirect('/settings?linked=1');
        } catch (Exception $e) {
            return $response->redirect('/settings?error=link_failed');
        }
    }
}

LDAP Authentication

Setup

Configure LDAP connection in your application:

// config/ldap.php
return [
    'host' => env('LDAP_HOST', 'ldap.example.com'),
    'port' => env('LDAP_PORT', 389),
    'base_dn' => env('LDAP_BASE_DN', 'dc=example,dc=com'),
    'username' => env('LDAP_USERNAME'),
    'password' => env('LDAP_PASSWORD'),
    'use_ssl' => env('LDAP_SSL', false),
    'use_tls' => env('LDAP_TLS', false),
];

Auth Configuration

'guards' => [
    'ldap' => [
        'driver' => 'session',
        'provider' => 'ldap',
    ],
],

'providers' => [
    'ldap' => [
        'driver' => 'ldap',
        'model' => LdapModel::class,
        'username_attribute' => 'uid',
        'search_fields' => ['uid', 'mail', 'sAMAccountName'],
        'sync_attributes' => [
            'username' => 'uid',
            'email' => 'mail',
            'name' => 'cn',
            'department' => 'department',
        ],
        'cache_ttl' => 300,
    ],
],

Usage

use Mlangeni\Machinjiri\Core\Authentication\Auth;

// Attempt LDAP login
if (Auth::guard('ldap')->attempt([
    'username' => 'jdoe',
    'password' => 'secret',
])) {
    // User authenticated against LDAP
    $user = Auth::guard('ldap')->user();
}

// With email instead of username
if (Auth::guard('ldap')->attempt([
    'email' => 'jdoe@example.com',
    'password' => 'secret',
])) {
    // ...
}

LdapModel

The LdapModel wraps LDAP entries:

use Mlangeni\Machinjiri\Core\Authentication\Models\LdapModel;

class LdapModel extends AbstractModel implements Authenticatable
{
    protected array $attributes = ['mail', 'password', 'uid'];
    protected array $fillable = ['remember_token'];

    public function getAuthIdentifier(): mixed
    {
        return $this->ldapEntry->getAttribute($this->identifier);
    }

    public function getAuthPassword(): string
    {
        return ''; // LDAP users don't have local passwords
    }

    // ... other Authenticatable methods
}

Custom LDAP Configuration

use Mlangeni\Machinjiri\Core\Authentication\UserProviders\LdapUserProvider;
use Mlangeni\Machinjiri\Core\Components\LDAP\Manager as LdapManager;

$ldapManager = new LdapManager([
    'host' => 'ldap.company.com',
    'port' => 636,
    'use_ssl' => true,
    'base_dn' => 'ou=users,dc=company,dc=com',
]);

$provider = new LdapUserProvider(
    $ldapManager,
    LdapModel::class,
    [
        'username' => 'uid',
        'email' => 'mail',
        'name' => 'cn',
    ],
    [
        'username_attribute' => 'uid',
        'search_fields' => ['uid', 'mail'],
        'cache_ttl' => 600,
    ]
);

Middleware

Authenticate Middleware

Protects routes requiring authentication.

use Mlangeni\Machinjiri\Core\Authentication\Middleware\Authenticate;

// Single guard
$router->get('/dashboard', [DashboardController::class, 'index'])
    ->middleware(Authenticate::class);

// Specific guard
$router->get('/api/user', [UserController::class, 'show'])
    ->middleware(Authenticate::class . ':api');

// Multiple guards (any can authenticate)
$router->get('/admin', [AdminController::class, 'index'])
    ->middleware(Authenticate::class . ':session,jwt');

// Custom redirect
$router->get('/account', [AccountController::class, 'index'])
    ->middleware((new Authenticate())->redirectTo('/signin'));

Behavior:

  • JSON requests receive 401 Unauthorized response
  • Web requests are redirected to /login (or custom path)

Authorize Middleware

Checks permissions after authentication.

use Mlangeni\Machinjiri\Core\Authentication\Middleware\Authorize;

$router->get('/admin/users', [AdminController::class, 'users'])
    ->middleware(Authorize::class . ':manage-users');

$router->delete('/posts/{id}', [PostController::class, 'destroy'])
    ->middleware(Authorize::class . ':delete-posts');

Throttle Middleware

Limits authentication attempts to prevent brute-force attacks.

use Mlangeni\Machinjiri\Core\Authentication\Middleware\Throttle;

$router->post('/login', [AuthController::class, 'login'])
    ->middleware(new Throttle($cache, $maxAttempts = 5, $decaySeconds = 60));

Features:

  • Configurable max attempts
  • Configurable decay period
  • Identifies by IP + username
  • Returns 429 Too Many Requests when exceeded

Combined Middleware Example

// Login route with rate limiting
$router->post('/login', [AuthController::class, 'login'])
    ->middleware(new Throttle($cache, 5, 300)); // 5 attempts per 5 minutes

// Protected routes
$router->group(['middleware' => Authenticate::class], function ($router) {
    
    $router->get('/dashboard', [DashboardController::class, 'index']);
    $router->get('/profile', [ProfileController::class, 'show']);
    
    // Admin-only routes
    $router->group(['middleware' => Authorize::class . ':admin'], function ($router) {
        $router->get('/admin/users', [AdminController::class, 'users']);
        $router->get('/admin/settings', [AdminController::class, 'settings']);
    });
});

// API routes with token auth
$router->group(['middleware' => Authenticate::class . ':api'], function ($router) {
    $router->get('/api/user', [Api\UserController::class, 'show']);
    $router->get('/api/posts', [Api\PostController::class, 'index']);
});

Events

The authentication system triggers events at key points:

Available Events

Event Triggered When Payload
auth.login User successfully logs in $user
auth.logout User logs out $user
auth.failed Authentication attempt fails $credentials
auth.oauth.retrieved OAuth user retrieved ['user' => $user, 'provider' => $provider]
auth.oauth.failed OAuth retrieval failed ['provider' => $provider]
ldap.user.retrieving Before LDAP user lookup ['username' => $username]
ldap.user.retrieved LDAP user found ['user' => $user]
ldap.user.notfound LDAP user not found ['username' => $username]

Listening to Events

use Mlangeni\Machinjiri\Core\Artisans\Events\EventListener;

$events = resolve(EventListener::class);

// Log successful logins
$events->listen('auth.login', function ($user) {
    logger()->info('User logged in', [
        'user_id' => $user->getAuthIdentifier(),
        'ip' => $_SERVER['REMOTE_ADDR'],
        'time' => date('Y-m-d H:i:s'),
    ]);
});

// Track failed attempts
$events->listen('auth.failed', function ($credentials) {
    logger()->warning('Failed login attempt', [
        'email' => $credentials['email'] ?? 'unknown',
        'ip' => $_SERVER['REMOTE_ADDR'],
    ]);
    
    // Could trigger additional security measures
    // e.g., increment failed attempt counter, send alert
});

// Handle logout
$events->listen('auth.logout', function ($user) {
    logger()->info('User logged out', [
        'user_id' => $user ? $user->getAuthIdentifier() : null,
    ]);
});

Custom Event Handlers

// app/Listeners/AuthenticationListener.php
namespace App\Listeners;

class AuthenticationListener
{
    public function onLogin($user): void
    {
        // Update last login timestamp
        $user->last_login_at = date('Y-m-d H:i:s');
        $user->last_login_ip = $_SERVER['REMOTE_ADDR'];
        $user->save();

        // Clear failed attempts
        cache()->delete('failed_login:' . $user->getAuthIdentifier());
    }

    public function onFailed($credentials): void
    {
        $email = $credentials['email'] ?? 'unknown';
        $key = 'failed_login:' . md5($email);
        
        $attempts = (int) cache()->get($key, 0);
        cache()->set($key, $attempts + 1, 3600);

        // Lock account after 10 failed attempts
        if ($attempts >= 10) {
            cache()->set('account_locked:' . md5($email), true, 3600);
        }
    }

    public function onLogout($user): void
    {
        // Cleanup session data, etc.
    }
}

// Register listeners
$events->listen('auth.login', [AuthenticationListener::class, 'onLogin']);
$events->listen('auth.failed', [AuthenticationListener::class, 'onFailed']);
$events->listen('auth.logout', [AuthenticationListener::class, 'onLogout']);

Advanced Usage

Custom Guard Implementation

<?php

namespace App\Authentication\Guards;

use Mlangeni\Machinjiri\Core\Authentication\Guard;
use Mlangeni\Machinjiri\Core\Authentication\Authenticatable;
use Mlangeni\Machinjiri\Core\Authentication\UserProviders\UserProvider;

class ApiKeyGuard implements Guard
{
    protected ?Authenticatable $user = null;
    protected UserProvider $provider;

    public function __construct(UserProvider $provider)
    {
        $this->provider = $provider;
    }

    public function check(): bool
    {
        return !is_null($this->user());
    }

    public function guest(): bool
    {
        return !$this->check();
    }

    public function user(): ?Authenticatable
    {
        if ($this->user !== null) {
            return $this->user;
        }

        $apiKey = $this->getApiKeyFromRequest();
        if (!$apiKey) {
            return null;
        }

        $this->user = $this->provider->retrieveByCredentials(['api_key' => $apiKey]);
        return $this->user;
    }

    public function id(): mixed
    {
        $user = $this->user();
        return $user ? $user->getAuthIdentifier() : null;
    }

    public function validate(array $credentials): bool
    {
        $apiKey = $credentials['api_key'] ?? $this->getApiKeyFromRequest();
        if (!$apiKey) {
            return false;
        }
        return !is_null($this->provider->retrieveByCredentials(['api_key' => $apiKey]));
    }

    public function setUser(Authenticatable $user): void
    {
        $this->user = $user;
    }

    public function attempt(array $credentials, bool $remember = false): bool
    {
        return $this->validate($credentials);
    }

    public function logout(): void
    {
        $this->user = null;
    }

    protected function getApiKeyFromRequest(): ?string
    {
        return $_SERVER['HTTP_X_API_KEY'] ?? null;
    }
}

Registering Custom Guard

// In your service provider or bootstrap
use Mlangeni\Machinjiri\Core\Authentication\AuthManager;

$authManager = resolve(AuthManager::class);
$authManager->extend('apikey', function ($container, $config) {
    $provider = $container->resolve(UserProvider::class);
    return new ApiKeyGuard($provider);
});

Multi-Tenant Authentication

// Custom provider that filters by tenant
class TenantUserProvider extends DatabaseUserProvider
{
    protected int $tenantId;

    public function __construct(
        QueryBuilder $query,
        string $model,
        EventListener $events,
        Logger $logger,
        CacheManager $cache,
        int $tenantId
    ) {
        parent::__construct($query, $model, $events, $logger, $cache);
        $this->tenantId = $tenantId;
    }

    public function retrieveByCredentials(array $credentials): ?Authenticatable
    {
        $query = clone $this->query;
        $query->where('tenant_id', '=', $this->tenantId);

        foreach ($credentials as $key => $value) {
            if ($key !== 'password' && $key !== 'tenant_id') {
                $query->where($key, '=', $value);
            }
        }

        $result = $query->first();
        return $result ? $this->hydrateModel($result) : null;
    }
}

Two-Factor Authentication

class TwoFactorGuard implements Guard
{
    protected Guard $baseGuard;
    protected Session $session;

    public function __construct(Guard $baseGuard, Session $session)
    {
        $this->baseGuard = $baseGuard;
        $this->session = $session;
    }

    public function check(): bool
    {
        if (!$this->baseGuard->check()) {
            return false;
        }

        $user = $this->baseGuard->user();
        
        // If 2FA is not enabled for this user, allow access
        if (!$user->two_factor_enabled) {
            return true;
        }

        // Check if 2FA was completed this session
        return $this->session->get('2fa_verified') === true;
    }

    public function user(): ?Authenticatable
    {
        if (!$this->check()) {
            return null;
        }
        return $this->baseGuard->user();
    }

    // ... delegate other methods to baseGuard
}

Account Lockout

class LoginController
{
    public function login($request, $response)
    {
        $email = $request->input('email');
        
        // Check if account is locked
        if (cache()->get('account_locked:' . md5($email))) {
            return $response->status(423)->json([
                'error' => 'Account temporarily locked. Try again later.'
            ]);
        }

        if (Auth::attempt($request->only(['email', 'password']))) {
            // Clear failed attempts on successful login
            cache()->delete('failed_login:' . md5($email));
            return $response->redirect('/dashboard');
        }

        // Track failed attempt
        $key = 'failed_login:' . md5($email);
        $attempts = (int) cache()->get($key, 0) + 1;
        cache()->set($key, $attempts, 3600);

        // Lock after 5 failed attempts
        if ($attempts >= 5) {
            cache()->set('account_locked:' . md5($email), true, 3600);
            return $response->status(423)->json([
                'error' => 'Too many failed attempts. Account locked for 1 hour.'
            ]);
        }

        return $response->status(401)->json([
            'error' => 'Invalid credentials',
            'attempts_remaining' => 5 - $attempts
        ]);
    }
}

Session Fingerprinting

The Session class includes built-in fingerprinting:

// Fingerprint is generated from User-Agent + IP
// Stored in session and validated on each request

// If fingerprint doesn't match:
// - Session is destroyed
// - Exception is thrown
// - User must log in again

To customize fingerprinting:

class CustomSession extends Session
{
    protected function generateFingerprint(): string
    {
        $ip = $this->request->getServerParam()['REMOTE_ADDR'] ?? '127.0.0.1';
        $agent = $this->request->getServerParam()['HTTP_USER_AGENT'] ?? '';
        
        // Add additional factors
        $acceptLanguage = $this->request->getServerParam()['HTTP_ACCEPT_LANGUAGE'] ?? '';
        $acceptEncoding = $this->request->getServerParam()['HTTP_ACCEPT_ENCODING'] ?? '';
        
        return hash('sha256', $agent . $ip . $acceptLanguage . $acceptEncoding);
    }
}

Security Best Practices

1. Password Hashing

Always use the built-in Hasher class:

use Mlangeni\Machinjiri\Core\Security\Hashing\Hasher;

$hasher = new Hasher();

// Hash a password
$hashed = $hasher->make('plain-password');

// Verify a password
if ($hasher->verify('plain-password', $hashed)) {
    // Password is correct
}

// Check if rehash is needed (e.g., after changing cost factor)
if ($hasher->needsRehash($hashed)) {
    $user->password = $hasher->make('plain-password');
    $user->save();
}

2. Session Security

// In your session configuration
'session' => [
    'lifetime' => 120,           // 2 hours
    'expire_on_close' => false,
    'encrypt' => true,            // Encrypt session data
    'secure' => true,             // HTTPS only
    'http_only' => true,          // Not accessible via JavaScript
    'same_site' => 'lax',         // CSRF protection
],

// Regenerate session ID on login
Auth::login($user);
// SessionGuard automatically regenerates the session ID

// Destroy session on logout
Auth::logout();
// SessionGuard automatically destroys the session

3. Remember Token Security

// Remember tokens are:
// 1. Generated with cryptographically secure random_bytes()
// 2. Hashed with bcrypt before storage
// 3. Verified with constant-time comparison
// 4. Rotated on each login

// Token format: userId|plainToken
// Only the plainToken is hashed in the database
// The plainToken is sent to the client in an HttpOnly cookie

4. CSRF Protection

// State parameter for OAuth prevents CSRF
// Session-based CSRF token for forms

// In your forms:
<form method="POST">
    <input type="hidden" name="_token" value="<?= csrf_token() ?>">
    <!-- form fields -->
</form>

// Middleware to verify CSRF
class VerifyCsrfToken
{
    public function handle($request, $response, $next)
    {
        if ($request->method() === 'POST') {
            $token = $request->input('_token');
            if (!hash_equals(session('_token'), $token)) {
                return $response->status(419)->json(['error' => 'CSRF token mismatch']);
            }
        }
        return $next($request, $response);
    }
}

5. Rate Limiting

// Apply Throttle middleware to login routes
$router->post('/login', [AuthController::class, 'login'])
    ->middleware(new Throttle($cache, 5, 300)); // 5 attempts per 5 minutes

6. HTTPS Only

// Force HTTPS in production
if (env('APP_ENV') === 'production' && empty($_SERVER['HTTPS'])) {
    header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
    exit;
}

// Set secure cookies
'secure' => env('APP_ENV') === 'production',

7. Password Reset Security

class PasswordResetController
{
    public function request($request, $response)
    {
        $email = $request->input('email');
        
        // Rate limit reset requests
        $key = 'password_reset:' . md5($email);
        if (cache()->get($key . ':count', 0) >= 3) {
            return $response->status(429)->json(['error' => 'Too many reset requests']);
        }
        cache()->set($key . ':count', cache()->get($key . ':count', 0) + 1, 3600);

        // Generate secure token
        $token = bin2hex(random_bytes(32));
        $hashedToken = hash('sha256', $token);

        // Store hashed token with expiration
        cache()->set($key . ':token', $hashedToken, 3600); // 1 hour

        // Send email with plain token
        // The URL should be: /password/reset?token=<plain>&email=<email>
        
        // Always return success to prevent email enumeration
        return $response->json(['message' => 'If the email exists, a reset link has been sent.']);
    }

    public function reset($request, $response)
    {
        $token = $request->input('token');
        $email = $request->input('email');
        $password = $request->input('password');

        $key = 'password_reset:' . md5($email);
        $storedHash = cache()->get($key . ':token');

        if (!$storedHash || !hash_equals($storedHash, hash('sha256', $token))) {
            return $response->status(400)->json(['error' => 'Invalid or expired token']);
        }

        // Update password
        $user = User::where('email', $email)->first();
        if ($user) {
            $user->password = (new Hasher())->make($password);
            $user->save();
        }

        // Invalidate token
        cache()->delete($key . ':token');

        return $response->json(['message' => 'Password reset successfully']);
    }
}

API Reference

Auth Facade

Method Description
Auth::check(): bool Check if user is authenticated
Auth::guest(): bool Check if user is a guest
Auth::user(): ?Authenticatable Get authenticated user
Auth::id(): mixed Get authenticated user ID
Auth::attempt(array $credentials, bool $remember = false): bool Attempt login
Auth::login(Authenticatable $user, bool $remember = false): void Login a user
Auth::logout(): void Logout current user
Auth::validate(array $credentials): bool Validate credentials
Auth::guard(?string $name = null): Guard Get a guard instance

AuthManager

Method Description
guard(?string $name = null): Guard Get guard by name
check(): bool Check authentication (default guard)
guest(): bool Check guest status (default guard)
user(): ?Authenticatable Get user (default guard)
id(): mixed Get user ID (default guard)
attempt(array $credentials, bool $remember = false): bool Attempt login
login(Authenticatable $user, bool $remember = false): void Login user
logout(): void Logout user
validate(array $credentials): bool Validate credentials
setDefaultGuard(string $name): void Set default guard
getGuards(): array Get all guard names

Guard Interface

Method Description
check(): bool Check if authenticated
guest(): bool Check if guest
user(): ?Authenticatable Get authenticated user
id(): mixed Get user ID
validate(array $credentials): bool Validate credentials
setUser(Authenticatable $user): void Set current user
attempt(array $credentials, bool $remember = false): bool Attempt login
logout(): void Logout user

SessionGuard

Method Description
check(): bool Check authentication
guest(): bool Check guest
user(): ?Authenticatable Get user
id(): mixed Get user ID
validate(array $credentials): bool Validate credentials
setUser(Authenticatable $user): void Set current user
attempt(array $credentials, bool $remember = false): bool Attempt login
login(Authenticatable $user, bool $remember = false): void Login user
logout(): void Logout user

TokenGuard

Method Description
check(): bool Check authentication
user(): ?Authenticatable Get user via token
id(): mixed Get user ID
validate(array $credentials): bool Validate token
setUser(Authenticatable $user): void Set current user
attempt(array $credentials, bool $remember = false): bool Validate token
logout(): void Clear user

JwtGuard

Method Description
check(): bool Check authentication
user(): ?Authenticatable Get user via JWT
id(): mixed Get user ID
validate(array $credentials): bool Validate JWT
setUser(Authenticatable $user): void Set current user
attempt(array $credentials, bool $remember = false): bool Validate JWT
logout(): void Clear user
generateToken(Authenticatable $user, array $claims = []): string Generate JWT

UserProvider Interface

Method Description
retrieveById($id): ?Authenticatable Find user by ID
retrieveByCredentials(array $credentials): ?Authenticatable Find user by credentials
retrieveByRememberToken(string $token): ?Authenticatable Find user by remember token
updateRememberToken(Authenticatable $user, string $token): void Update remember token

ThirdPartyAuth

Method Description
getProvider(string $provider): ?OAuth Get OAuth provider instance
getAvailableProviders(): array Get available provider names
isProviderAvailable(string $provider): bool Check if provider is configured
redirectToProvider(string $provider, HttpResponse $response): void Redirect to OAuth provider
handleCallback(HttpRequest $request, ?string $provider = null): array Handle OAuth callback
getUserInfo(string $provider, string $accessToken): array Get user info from provider
authenticateWithToken(string $provider, string $accessToken): ?array Authenticate with token
disconnectProvider(string $provider, ?int $userId = null): bool Disconnect provider
getUserProviders(int $userId): array Get user's connected providers
refreshToken(string $provider, ?int $userId = null): ?array Refresh OAuth token
logout(?string $provider = null): void Logout from provider(s)
getLoginUrls(): array Get authorization URLs for all providers
getProviderButton(string $provider, array $attributes = []): string Generate login button HTML
getLoginButtons(?array $providers = null, array $attributes = []): string Generate all login buttons

Session

Method Description
set(string $key, mixed $value): void Set session value
get(string $key, mixed $default = null): mixed Get session value
has(string $key): bool Check if session key exists
remove(string $key): void Remove session key
destroy(bool $restart = false): void Destroy session
regenerateId(): void Regenerate session ID

Cookie

Method Description
set(string $name, mixed $value, int $expire = 0, ...): bool Set cookie
get(string $name, mixed $default = null): mixed Get cookie
delete(string $name, string $path = '/', string $domain = ''): void Delete cookie
has(string $name): bool Check if cookie exists

Troubleshooting

Common Issues

"Authentication Guard [name] not configured"

Cause: The guard name doesn't exist in your auth configuration.

Solution: Check your config/auth.php:

'guards' => [
    'session' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    // Add missing guard here
],

"User does not exist" exception

Cause: retrieveByCredentials throws when user is not found.

Solution: This is expected behavior. Catch the exception:

try {
    if (Auth::attempt($credentials)) {
        // Success
    }
} catch (AuthenticationException $e) {
    // Handle failed login
}

Session not persisting

Cause: Session configuration issues.

Solution: Check:

  1. session.save_path is writable
  2. Cookie domain matches your application domain
  3. secure is false for HTTP (development)
  4. Session lifetime is sufficient

OAuth callback fails

Cause: Various OAuth issues.

Solution: Check:

  1. Redirect URI matches exactly (including trailing slashes)
  2. Client ID and secret are correct
  3. Provider is configured in .env
  4. State parameter matches session
  5. Check logs for detailed error

LDAP authentication fails

Cause: LDAP connection or credential issues.

Solution: Check:

  1. LDAP host and port are correct
  2. Base DN is correct
  3. Username attribute matches your LDAP schema
  4. User exists in LDAP directory
  5. Password is correct
  6. Check LDAP logs

Clone this wiki locally