Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

U2F Php Server Laravel Package

samyoul/u2f-php-server

A PHP server-side library for adding U2F (Universal 2nd Factor) registration and authentication to your application, with helpers for handling FIDO U2F requests and responses.

Deep Wiki
Context7

Getting Started

This is a low-level server-side U2F library, not a Laravel package. There is no service provider, facade, config file, migration, or UI. You will wire all of that yourself. Treat it as a small protocol helper behind your own service class.

Install it and confirm the namespace:

composer require samyoul/u2f-php-server

The public API lives in u2flib_server\U2F:

  • getRegisterData(array $registrations = [])
  • doRegister($request, $response)
  • getAuthenticateData(array $registrations)
  • doAuthenticate($requests, array $registrations, $response)
  • Errors are thrown as u2flib_server\Error

Start by creating a config value and a singleton wrapper. The app ID must be the exact HTTPS origin your frontend uses:

// config/u2f.php
return [
    'app_id' => env('U2F_APP_ID', config('app.url')),
];
// app/Providers/AppServiceProvider.php
use u2flib_server\U2F;

$this->app->singleton(U2F::class, function () {
    return new U2F(config('u2f.app_id'));
});

Your first use case should be registering a security key as a second factor. A minimal registration endpoint looks like this:

use u2flib_server\U2F;

public function challenge(Request $request, U2F $u2f)
{
    $data = $u2f->getRegisterData(
        $request->user()->u2fKeys
            ->map(fn ($key) => $key->registrationObject())
            ->all()
    );

    $request->session()->put('u2f.register', $data);

    return response()->json([
        'request'    => [$data->request],
        'signatures' => $data->signatures,
    ]);
}

public function store(Request $request, U2F $u2f)
{
    $data = $request->session()->pull('u2f.register');
    abort_unless($data, 419);

    try {
        $registration = $u2f->doRegister(
            $data->request,
            (object) $request->input('response')
        );
    } catch (\u2flib_server\Error $e) {
        return response()->json(['error' => 'Registration failed'], 422);
    }

    $request->user()->u2fKeys()->create([
        'label'        => $request->input('label', 'Security key'),
        'key_handle'   => $registration->keyHandle,
        'registration' => json_encode($registration),
    ]);

    return response()->json(['ok' => true]);
}

On the client you still need a U2F JavaScript implementation such as u2f-api or Google’s u2f-api.js. The library only handles the server half.

Implementation Patterns

Store the full registration object, not just the key handle

doRegister() returns an object containing the credential material needed later: keyHandle, publicKey, certificate, and counter. Persist the entire object as JSON, plus a separate indexed key_handle column for lookup.

Schema::create('u2f_keys', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('label')->nullable();
    $table->string('key_handle')->unique();
    $table->json('registration');
    $table->timestamp('last_used_at')->nullable();
    $table->timestamps();
});
class U2fKey extends Model
{
    protected $casts = [
        'registration' => 'array',
    ];

    public function registrationObject(): object
    {
        // Important: the library expects an object, not an array
        return json_decode($this->getRawOriginal('registration'));
    }
}

Registration flow

  1. getRegisterData() creates a challenge and a set of signatures.
  2. Store the whole returned object in the session or cache; the request and signatures must both be available when the response comes back.
  3. Send request and signatures to the browser.
  4. Call the U2F JS API:
u2f.register(
  request.appId,
  [request],
  signatures,
  (response) => {
    if (response.errorCode) return handleError(response);
    axios.post('/u2f/register', { response });
  }
);
  1. On POST, call doRegister($storedRequest, $response).
  2. Persist the returned credential and invalidate the session challenge.

Authentication flow

Authentication is a challenge-response with a stored credential list. Pass all of the user’s stored registrations to getAuthenticateData() so the browser can pick any registered key:

public function challenge(Request $request, U2F $u2f)
{
    $keys = $request->user()->u2fKeys;
    abort_if($keys->isEmpty(), 400);

    $data = $u2f->getAuthenticateData(
        $keys->map(fn ($key) => $key->registrationObject())->all()
    );

    $request->session()->put('u2f.auth', $data);

    return response()->json([
        'requests'   => $data->requests,
        'signatures' => $data->signatures,
    ]);
}

Client:

u2f.sign(
  appId,
  data.requests,
  data.signatures,
  (response) => {
    if (response.errorCode) return handleError(response);
    axios.post('/u2f/verify', { response });
  }
);

Server verification and counter persistence:

public function verify(Request $request, U2F $u2f)
{
    $data = $request->session()->pull('u2f.auth');
    abort_unless($data, 419);

    $keys = $request->user()->u2fKeys;

    try {
        $updated = $u2f->doAuthenticate(
            $data->requests,
            $keys->map(fn ($key) => $key->registrationObject())->all(),
            (object) $request->input('response')
        );
    } catch (\u2flib_server\Error $e) {
        return response()->json(['error' => 'Authentication failed'], 422);
    }

    $key = $keys->firstWhere('key_handle', $updated->keyHandle);
    $key->update([
        'registration'  => json_encode($updated),
        'last_used_at'  => now(),
    ]);

    $request->session()->put('u2f.passed', true);

    return response()->json(['ok' => true]);
}

The returned registration object contains an updated signature counter. You must persist it. If you don’t, cloned-key detection is weakened and strict authenticators may fail future checks.

Step-up middleware

U2F is usually a step-up check, not a primary login method. After normal password authentication, require a U2F verification for sensitive routes:

class RequireU2f
{
    public function handle($request, Closure $next)
    {
        abort_unless($request->session()->get('u2f.passed'), 423, 'U2F required');
        return $next($request);
    }
}

Clear u2f.passed on logout and on privilege changes.

Wrap it behind an interface

Because this package is old and U2F is legacy, put it behind your own contract. That makes it much easier to swap in WebAuthn later:

interface SecondFactorProvider
{
    public function registrationChallenge(array $credentials): object;
    public function register(object $challenge, object $response): object;
    public function authenticationChallenge(array $credentials): object;
    public function authenticate(array $challenges, array $credentials, object $response): object;
}

Bind u2flib_server\U2F inside a U2fSecondFactorProvider, not directly in controllers.

Testing

Do not exercise real U2F crypto in feature tests. Mock your SecondFactorProvider and assert:

  • challenges are stored in the session,
  • challenges are pulled and cannot be reused,
  • failed u2flib_server\Error exceptions return a generic error,
  • the counter is updated,
  • the middleware blocks unverified sessions.

For the wrapper itself, keep one small integration test around the library API so deprecations or PHP-version incompatibilities are caught immediately.

Gotchas and Tips

U2F is deprecated; plan the exit

This is the biggest issue. The package’s last release was 2018, and modern browsers have removed or are removing the legacy u2f JavaScript API. If you are starting fresh, use WebAuthn instead (for Laravel, look at laragear/webauthn). Use samyoul/u2f-php-server only to maintain existing U2F credentials, and design a migration path where users re-register as WebAuthn passkeys.

PHP and framework compatibility

The library predates modern PHP versions. Before committing, run it on your exact PHP version in CI. Watch for deprecations, strict typing issues, and old autoload assumptions. There is no active Laravel integration, so you own all compatibility risks.

The app ID must match exactly

The app ID passed to new U2F(...) must exactly match the origin used by the browser. A trailing slash, mismatched scheme, different subdomain, or proxy-generated URL will cause registration and authentication failures. Set U2F_APP_ID explicitly rather than deriving it from config('app.url') in production, and configure TrustProxies if you are behind a load balancer.

Store the challenge before the browser call

doRegister() needs the exact request object issued by getRegisterData(). You cannot regenerate it at verification time. Store both request and signatures in the session or a short-lived cache. Pull and delete them on POST so a challenge cannot be replayed.

Arrays versus objects is a real failure mode

Eloquent JSON casts return arrays. The library expects stdClass objects. Passing an array where an object is expected often produces confusing errors deep in the library. Always pass json_decode($raw) or cast to (object) at the boundary. Keep property names exactly as returned: keyHandle, publicKey, certificate, counter.

Persist the counter

doAuthenticate() returns an updated credential. If you discard the return value, the counter never advances. Persist the updated registration object after every successful authentication. Also expect counter resets to 0 on some authenticators and handle u2flib_server\Error accordingly.

Catch and sanitize library errors

The library throws u2flib_server\Error for protocol failures. Do not leak the message to the browser; return a generic “authentication failed” response and log the detail. Distinguish between user error (wrong key, cancelled prompt) and server error (bad stored credential, counter regression).

Prevent duplicate key registration

Check key_handle uniqueness before saving. Without a unique constraint, the same physical key can be enrolled repeatedly or attached to multiple accounts. Add a unique index and catch the database exception.

Handle browser error codes before posting

The U2F JavaScript API returns errorCode and errorMessage. Short-circuit in JavaScript before making a request to your server. Common cases are user cancellation and timeout; these should not produce server-side error logs.

CSRF, sessions, and AJAX

Your register and verify endpoints are normal Laravel POST routes, so include the CSRF token in Axios/fetch requests. If you use fetch without the Laravel bootstrap, either send X-CSRF-TOKEN or exclude only those endpoints from CSRF protection—not the whole auth area.

Extension points

Good extension points are your own SecondFactorProvider, a HasU2fKeys trait on the User model, a RequireU2f middleware, and Laravel auth events for audit logging. Avoid subclassing or patching the library itself; it is unmaintained and you will want to remove it cleanly when you migrate to WebAuthn.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
cgv/localized-resource