centamiv/advanced-fingerprint

Advanced Server-Side and Client-Side Fingerprinting for Laravel

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/centamiv/advanced-fingerprint

dev-master 2025-12-30 15:49 UTC

This package is auto-updated.

Last update: 2025-12-30 15:51:33 UTC


README

Advanced Server-Side and Client-Side Fingerprinting for Laravel Applications.

Introduction

centamiv/advanced-fingerprint is a powerful Laravel package designed to identify and track devices interacting with your application. Unlike simple IP-based tracking, this library combines server-side analysis (HTTP headers, Client Hints) with optional client-side techniques (Canvas Fingerprinting) to create a robust "Device Fingerprint".

This fingerprint allows you to:

  • Detect account sharing.
  • Prevent session hijacking.
  • Implement more granular rate limiting.
  • Track unique devices for analytics.
  • Block bots that rotate IPs but keep the same browser configuration.

Why "Advanced"?

Standard fingerprinting usually relies on just the User-Agent and IP address. This is easily spoofed and often inaccurate (e.g., all users behind a corporate proxy might share an IP).

This package goes further by:

  1. Utilizing Client Hints: Leveraging modern Sec-CH-UA headers that replace the User-Agent string.
  2. Hybrid Approach: Merging server-side data with a client-side calculated hash (Canvas + Environment) for maximum entropy.
  3. Anomaly Detection: Automatically detecting if a logged-in user's device signature changes mid-session (a sign of token theft).
  4. Developer Friendly: Providing simple helpers, facades, and events to integrate deeply into your business logic.

Key Features

  • Zero-Config Start: Works out of the box with sensible defaults.
  • Configurable Entropy: Choose which signals (IP, UA, Accept-Language, Platform) to include.
  • Dual-Layer Identification: Server Hash (fast, immediate) + Client Hash (accurate, delayed).
  • Session Protection: Built-in event firing when a signature mismatch occurs.
  • Rate Limit Helper: Limit actions per device, not just per IP.
  • Blade Directive: One-line integration for client-side fingerprinting script.

Requirements

  • PHP: 8.1 or higher
  • Laravel: 10.0 or higher

Installation

Install the package via Composer:

composer require centamiv/advanced-fingerprint

The package will automatically register its service provider.

Next, you should publish the configuration file. This is crucial for customizing the fingerprinting logic:

php artisan vendor:publish --tag=fingerprint-config

This will create a config/fingerprint.php file in your application.

Configuration

The configuration file is the heart of the library. It determines how "sensitive" or "unique" your fingerprints are.

Open config/fingerprint.php:

return [
    /*
    |--------------------------------------------------------------------------
    | Fingerprint Algorithm
    |--------------------------------------------------------------------------
    | The hashing algorithm to use for generating the signature.
    | 'sha256' is recommended for a balance of speed and collision resistance.
    */
    'algo' => 'sha256',

    /*
    |--------------------------------------------------------------------------
    | Components
    |--------------------------------------------------------------------------
    | List of components to consider for fingerprint calculation.
    | The more components you add, the more unique (but volatile) the fingerprint will be.
    */
    'components' => [
        // The IP Address. High entropy, but changes on mobile networks.
        // Set to false if you want to track devices across different networks.
        'ip' => true,

        // The User-Agent string. Standard, but legacy.
        'user_agent' => true,

        // The Accept-Language header. Helps distinguish users in the same region.
        'accept_language' => true,

        // Modern Client Hints (Sec-CH-UA). Highly recommended for Chrome/Edge.
        'sec_ch_ua' => true,
        'sec_ch_ua_platform' => true,
        'sec_ch_ua_mobile' => true,
    ],

    /*
    |--------------------------------------------------------------------------
    | Ignore Routes
    |--------------------------------------------------------------------------
    | Routes to exclude from fingerprint injection (optional).
    */
    'ignore_routes' => [
        'api/health-check',
    ],

    /*
    |--------------------------------------------------------------------------
    | Cache & Rate Limiting
    |--------------------------------------------------------------------------
    */
    'cache' => [
        'prefix' => 'device_sig',
        'ttl' => 60, // Cache duration in minutes
    ],

    /*
    |--------------------------------------------------------------------------
    | Anomaly Detection
    |--------------------------------------------------------------------------
    | Features to detect session hijacking or spoofing.
    */
    'anomaly_detection' => [
        'enabled' => true,
        // Fires Centamiv\AdvancedFingerprint\Events\DeviceSignatureChanged
        'fire_event' => true,
    ],
];

Note on configuration

If you set 'ip' => false, the fingerprint will be identical for the same browser on any network. This is useful for "Remember Device" features but bad for rate limiting (collisions). Choose wisely based on your use case.

Core Concepts

Understanding the architecture helps you use the library effectively.

Server-Side Fingerprint

This is generated immediately on every request. It is calculated from HTTP headers and IP. Pros: Available instantly, cannot be blocked by ad-blockers. Cons: Lower entropy (uniqueness).

Client-Side Fingerprint

This is generated by JavaScript relative to the browser (Canvas, Screen properties, Timezone). It is automatically sent asynchronously to the library. Pros: Very high unique entropy (can distinguish identical laptops). Cons: Requires JS, slightly delayed (exists in session after 2nd request).

Basic Usage

Accessing the Fingerprint

The easiest way is to access the fingerprint via the deviceSignature() method in the Request object:

public function index(Request $request)
{
    $signature = $request->deviceSignature();

    // The server-side fingerprint (always available)
    echo $signature->serverHash;

    // The client-side fingerprint (available after JS sync)
    if ($signature->clientHash) {
        echo $signature->clientHash;
    }
}

Using the Facade

use Centamiv\AdvancedFingerprint\Facades\Fingerprint;

// Get the current fingerprint object
$signature = Fingerprint::current();

Checks

You can check if a fingerprint matches the current request:

if ($request->deviceSignature()->matches('some-stored-hash')) {
    // It's the same device
}

Frontend Integration

To enable the powerful client-side fingerprinting, you must include the script in your layout file.

The Blade Directive

Add the @deviceFingerprintScript directive to your main layout, preferably directly before the closing </body> tag.

<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
</head>
<body>
    @yield('content')

    <!-- Injects the fingerprinting script -->
    @deviceFingerprintScript
</body>
</html>

It automatically generates the client-side fingerprint and sends it to the server via a POST request automatically handled by the library.

Customizing the JS

The default client JS logic is at resources/views/vendor/advanced-fingerprint/script.blade.php. If you want to modyfy the client fingerprint logic itself you need to publish the views:

php artisan vendor:publish --tag=advanced-fingerprint-views

Middleware Usage

The package registers a middleware alias: device.signature.

You can apply it to specific routes to enforce device signature checks:

Route::middleware(['auth', 'device.signature'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

Or add it globally to your web middleware group in app/Http/Kernel.php to run on every request:

protected $middlewareGroups = [
    'web' => [
        // ...
        \Centamiv\AdvancedFingerprint\Middleware\CheckDeviceSignature::class,
    ],
];

Events & Listeners

The library fires Centamiv\AdvancedFingerprint\Events\DeviceSignatureChanged whenever a fingerprint mismatch is detected.

Payload:

  • $user (Authenticatable): The logged-in user.
  • $oldSignature (string): The signature stored in cache/session.
  • $newSignature (string): The signature detected in the current request.

This is your hook for "Security Alerts".

Cookbook: Real-World Use Cases

Here are extensive examples of how to use this library in production.

1. Intelligent Rate Limiting (Beyond IP)

Standard Laravel throttling limits by IP or User ID. A smart attacker can rotate IPs. With this library, you can limit by Device Fingerprint. Even if they switch IPs (VPN/Proxy), if the browser signals remain the same, they stay blocked.

Implementation:

Create a custom middleware or modify App\Http\Kernel.php.

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Centamiv\AdvancedFingerprint\DeviceSignatureService;
use Symfony\Component\HttpFoundation\Response;

class RateLimitByDevice
{
    protected $service;

    public function __construct(DeviceSignatureService $service)
    {
        $this->service = $service;
    }

    public function handle(Request $request, Closure $next, int $maxAttempts = 5)
    {
        // Generate the fingerprint
        $fingerprint = $request->deviceSignature();
        
        // Use the service helper to check limits
        // This checks cache key: device_sig:{hash}
        if ($this->service->isRateLimited($fingerprint->serverHash, $maxAttempts)) {
            return response()->json([
                'message' => 'Too many requests from this device.',
                'device_id' => $fingerprint->serverHash
            ], 429);
        }

        return $next($request);
    }
}

Now apply this middleware to your sensitive routes (e.g., Login, Registration, Content Scraping targets).

2. Session Hijacking Protection

If a user logs in on their laptop, and suddenly a request comes with the same Session Cookie but from a different Browser or OS, it's highly likely their session was stolen (XSS or simplified Cookie theft).

We can listen for the DeviceSignatureChanged event to invalidate the session immediately.

Step 1: Create the Listener

php artisan make:listener RevokeSessionOnHijack --event=\Centamiv\AdvancedFingerprint\Events\DeviceSignatureChanged

Step 2: Implement the logic

// app/Listeners/RevokeSessionOnHijack.php

namespace App\Listeners;

use Centamiv\AdvancedFingerprint\Events\DeviceSignatureChanged;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;

class RevokeSessionOnHijack
{
    public function handle(DeviceSignatureChanged $event): void
    {
        // Log the security incident
        Log::warning('Session Hijacking Detected', [
            'user_id' => $event->user->id,
            'old_sig' => $event->oldSignature,
            'new_sig' => $event->newSignature,
            'ip' => request()->ip(),
        ]);

        // Logout the user to force re-authentication
        Auth::guard('web')->logout();

        // Invalidate the session
        Session::invalidate();
        Session::regenerateToken(); // Protection against CSRF

        // (Optional) Notify the user via Email
        // Mail::to($event->user)->send(new SecurityAlertMail(...));
        
        abort(403, 'Security violation: Device fingerprint mismatch. Please login again.');
    }
}

Step 3: Register in EventServiceProvider

In app/Providers/EventServiceProvider.php:

protected $listen = [
    \Centamiv\AdvancedFingerprint\Events\DeviceSignatureChanged::class => [
        \App\Listeners\RevokeSessionOnHijack::class,
    ],
];

Ensure anomaly_detection.enabled is true in config/fingerprint.php.

3. Device Content Locking (Paywalls)

Imagine a news site or streaming service that allows "3 free articles per device per month". Using IP is bad (shared Wi-Fi or NAT networking blocks users because they share the same IP). Cookies are cleared by users. Device fingerprinting is persistent.

// app/Http/Controllers/ArticleController.php

public function show($id)
{
    $request = request();
    $fingerprint = $request->deviceSignature()->serverHash;
    
    // Check database for usage
    $usage = \DB::table('device_usage')
                ->where('device_hash', $fingerprint)
                ->where('month', now()->format('Y-m'))
                ->first();

    if ($usage && $usage->view_count >= 3) {
        // If not subscribed, block
        if (! $request->user()?->isSubscribed()) {
            return view('paywall.limit_reached');
        }
    }

    // Increment usage
    \DB::table('device_usage')->updateOrInsert(
        ['device_hash' => $fingerprint, 'month' => now()->format('Y-m')],
        ['view_count' => \DB::raw('view_count + 1'), 'updated_at' => now()]
    );

    return view('articles.show', ['article' => Article::find($id)]);
}

4. Tracking User Devices History

Show users a list of "Devices where you are logged in", similar to Google or Facebook security dashboards.

Step 1: Create a table

php artisan make:migration create_user_devices_table
Schema::create('user_devices', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->string('device_fingerprint'); // The hash
    $table->string('ip_address');
    $table->string('user_agent');
    $table->timestamp('last_active_at');
    $table->timestamps();
});

Step 2: Update the table on login/activity

You can use a middleware for this, or hook into the Login event.

// app/Listeners/LogDeviceOnLogin.php

public function handle(Login $event)
{
    $fingerprint = request()->deviceSignature()->serverHash;
    
    $device = \App\Models\UserDevice::updateOrCreate(
        [
            'user_id' => $event->user->id,
            'device_fingerprint' => $fingerprint
        ],
        [
            'ip_address' => request()->ip(),
            'user_agent' => request()->header('User-Agent'),
            'last_active_at' => now(),
        ]
    );
}

Step 3: Show to User

In your User Profile controller:

public function devices()
{
    $devices = Auth::user()->devices()->orderByDesc('last_active_at')->get();
    
    return view('profile.devices', compact('devices'));
}

Deep Dive: Inner Workings

This section details how the fingerprint generation actually works, which is useful for security auditing.

Entropy Sources

Entropy is the measure of randomness or uniqueness. A higher entropy means a lower chance of collision (two different devices having the same fingerprint).

1. Server-Side Entropy (Low - Medium Reliability)

  • IP Address: Very specific, but changes with networks (Wi-Fi vs 4G).
  • User-Agent: Describes the browser version and OS. Chrome freezes this string to improve privacy, making it less useful.
  • Accept-Language: Can reveal the user's preferred language and region (e.g., en-US,en;q=0.9,it;q=0.8).
  • Sec-CH-UA-*: Modern replacements for User-Agent. They provide the platform (Windows, Android), architecture, and mobile status.

2. Client-Side Entropy (High Reliability)

  • Canvas: The script draws a hidden text with specific emoji/styles on a HTML5 Canvas. Different Graphics Cards (GPU) and Drivers render fonts and anti-aliasing slightly differently. By exporting the image data (base64) and hashing it, we get a signature unique to the hardware stack.
  • Hardware Concurrency: Number of logical processors.
  • Color Depth: e.g., 24-bit.
  • Timezone: The user's local timezone.

All these values are hashed using SHA-256 by default.

Canvas Fingerprinting Explained

Canvas fingerprinting is one of the most reliable entropy sources because it exploits the hardware-level differences in how graphics are rendered.

  1. Instruction: The library instructs the browser to draw a hidden 2D image containing specific text, emojis, and gradients using the HTML5 <canvas> API.
  2. Rendering: The browser uses the operating system's font-rendering engine and the local GPU's anti-aliasing algorithms to draw the image.
  3. Variation: Subtle differences in graphics drivers, hardware architecture, and OS-level sub-pixel rendering mean that the exact pixel data of the resulting image is unique to that specific hardware/software stack.
  4. Hashing: This raw pixel data is converted into a hash. Unlike cookies, this identifier is persistent and remains the same even if the user clears their browsing data or uses "Incognito" mode, as it is tied to the machine's physical capabilities.

Testing Your Application

When writing Feature tests for your Laravel application, you might encounter issues if your tests expect real fingerprinting data (especially headers that aren't present in testing environments).

Mocking the Signature

You should mock the DeviceSignatureService in your tests to return a predictable hash.

namespace Tests\Feature;

use Tests\TestCase;
use Centamiv\AdvancedFingerprint\DeviceSignatureService;

class LoginTest extends TestCase
{
    public function test_user_login_with_fingerprint()
    {
        // Mock the service
        $this->mock(DeviceSignatureService::class, function ($mock) {
            $mock->shouldReceive('generate')
                 ->andReturn('dummy-test-hash-123');
                 
             // If you use checkAnomaly, mock that too
             $mock->shouldReceive('checkAnomaly')->andReturn(null);
        });

        $response = $this->post('/login', [
            'email' => 'test@example.com',
            'password' => 'password',
        ]);

        $response->assertRedirect('/home');
    }
}

Testing Middleware

If you use the device.signature middleware, you can disable it in tests if it gets in the way:

$this->withoutMiddleware([\Centamiv\AdvancedFingerprint\Middleware\CheckDeviceSignature::class]);

However, it is better to ensure your request headers in tests mimic a real browser to verify the integration properly.

API Reference

DeviceSignatureService

The core singleton class responsible for logic.

generate(Request $request): string Generates the server-side SHA-256 hash based on the current configuration.

checkAnomaly(Authenticatable $user, string $currentSignature): void Compares the current signature with the one cached for the user. Fires DeviceSignatureChanged if different.

isRateLimited(string $signature, int $maxAttempts = 60): bool Atomic rate limiter using Cache. Returns true if limit exceeded.

DeviceSignature Class

A simple value object holding the state.

public string $serverHash The hash calculated from HTTP headers.

public ?string $clientHash The hash calculated from JS (Canvas). Null if not yet synced.

matches(string $hash): bool Returns true if $serverHash equals the provided string.

Troubleshooting

"The Client Hash is always null"

  • Ensure you included @deviceFingerprintScript in your layout.
  • The client hash is sent via AJAX after the page loads. It will not be available in the initial GET request of the page load. It will be available in subsequent requests (AJAX or navigation).
  • Check your browser console for "Fingerprint Sync Failed" errors.
  • Ensure your route('fingerprint.sync') is accessible and not blocked by other middleware. Only the web middleware group is applied by default.

"My Rate Limiting is blocking everyone on the same Wi-Fi"

  • You have 'ip' => true in your config.
  • If you want to distinguish devices behind a NAT (e.g., Corporate Office), set 'ip' => false and rely on Header/Canvas entropy.
  • Warning: Removing IP reduces security against attackers who can simulate headers. It's a trade-off.

Privacy & GDPR

  • Device Fingerprinting can be considered "Tracking" under GDPR/ePrivacy.
  • You must disclose this in your Privacy Policy.
  • If used strictly for "Security" (Fraud detection, Account protection), it is generally a "Legitimate Interest".
  • If used for "Analytics" or "Marketing", you likely need Consent (Cookie Banner).

Extending the Library

Custom Generators

Currently, the generator logic is inside DeviceSignatureService. If you need to add custom logic (e.g., checking specific custom headers passed by your mobile app), you can extend the service and re-bind it.

// App\Services\MyCustomSignatureService.php
namespace App\Services;

use Centamiv\AdvancedFingerprint\DeviceSignatureService;
use Illuminate\Http\Request;

class MyCustomSignatureService extends DeviceSignatureService
{
    public function generate(Request $request): string
    {
        // Call parent or write complete custom logic
        $hash = parent::generate($request);
        
        // Add a custom header to the mix
        if ($request->hasHeader('X-My-App-ID')) {
            $hash = hash('sha256', $hash . $request->header('X-My-App-ID'));
        }
        
        return $hash;
    }
}

In your AppServiceProvider:

$this->app->singleton(\Centamiv\AdvancedFingerprint\DeviceSignatureService::class, \App\Services\MyCustomSignatureService::class);

License

The MIT License (MIT). Please see License File for more information.

Built with ❤️ by Ivan Centamori