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

U2Flib Server Laravel Package

yubico/u2flib-server

Server-side PHP library for U2F registration and authentication. It lets applications issue challenges, register compatible hardware security keys, and verify signed responses, handling protocol parsing and validation. U2F is legacy; use WebAuthn for new projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: yubico/u2flib-server is archived and marked OBSOLETE, and it implements U2F — a protocol that browsers no longer expose. Chrome and Firefox removed the u2f-api.js JS API; current keys are FIDO2/WebAuthn. There are zero dependents. So "day-to-day leverage" here means exactly one thing: maintaining an existing U2F flow on a legacy app, or building a server-side verifier behind a client you control. For new auth work, stop and use laragear/webauthn or web-auth/webauthn-lib instead — old U2F keys still register fine as WebAuthn fido-u2f attestation credentials.

If you're proceeding anyway:

composer require yubico/u2flib-server
php -m | grep -i gmp   # optional; enables faster bignum math

Sanity-check it loads on your PHP version — the library predates PHP 8 and can emit deprecation notices (dynamic properties) on 8.1+.

Where to look first: vendor/yubico/u2flib-server/src/u2flib_server/U2F.php and Error.php. The whole library is a few hundred lines and two classes. Read U2F.php end to end — it has four methods you'll actually call:

  • getRegisterData($registrations) → [$registerRequest, $signRequests]
  • doRegister($registerRequest, $clientResponse) → Registration
  • getAuthenticateData($registrations) → sign request(s)
  • doAuthenticate($requests, $registrations, $clientResponse) → updated Registration

First use case — bind it and register a key. There is no service provider, so wire it yourself:

// app/Providers/AppServiceProvider.php
$this->app->singleton(\u2flib_server\U2F::class, fn () => new \u2flib_server\U2F(
    config('u2f.app_id')   // e.g. https://app.example.com — origin only, no path
));

// config/u2f.php
return ['app_id' => env('U2F_APP_ID', config('app.url'))];

Migration — store the registration verbatim:

Schema::create('u2f_registrations', function (Blueprint $t) {
    $t->id();
    $t->foreignId('user_id')->constrained()->cascadeOnDelete();
    $t->string('key_handle', 255)->unique();
    $t->text('public_key');
    $t->text('certificate');
    $t->unsignedBigInteger('counter')->default(0);
    $t->string('name')->nullable();
    $t->timestamps();
});

Controller:

public function begin(Request $request, U2F $u2f)
{
    [$registerRequest, $signRequests] = $u2f->getRegisterData(
        $request->user()->u2fRegistrations()->get()->map->toRegistration()->all()
    );

    Cache::put("u2f:reg:{$request->user()->id}", $registerRequest, now()->addMinutes(5));

    return response()->json([
        'registerRequests' => [$registerRequest],
        'signRequests'     => $signRequests,
    ]);
}

public function finish(Request $request, U2F $u2f)
{
    $request->validate(['response' => ['required', 'array']]);
    $regRequest = Cache::pull("u2f:reg:{$request->user()->id}");
    abort_unless($regRequest, 419, 'Challenge expired');

    $reg = $u2f->doRegister($regRequest, (object) $request->input('response'));

    $request->user()->u2fRegistrations()->create([
        'key_handle'  => $reg->keyHandle,
        'public_key'  => $reg->publicKey,
        'certificate' => $reg->certificate,
        'counter'     => $reg->counter,
        'name'        => $request->input('name'),
    ]);

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

Implementation Patterns

Keep the challenge in the cache, not the DB. There's no server-side nonce store in the library — getRegisterData/getAuthenticateData generate a random challenge and you must hand the same object back to doRegister/doAuthenticate. Session or Cache::put(..., 5 minutes) both work; key it per-user and per-purpose (u2f:reg:{id} vs u2f:auth:{id}) so two open tabs don't clobber each other.

Round-trip registrations as objects, not arrays. The library reads $registration->keyHandle, ->publicKey, ->counter, etc. A model accessor that returns the decoded struct:

public function toRegistration(): \u2flib_server\Registration
{
    return (object) [
        'keyHandle'   => $this->key_handle,
        'publicKey'   => $this->public_key,
        'certificate' => $this->certificate,
        'counter'     => (int) $this->counter,
    ];
}

Never json_decode($json, true) into arrays for this — doAuthenticate will blow up.

Authentication flow mirrors registration, plus a counter write-back:

$requests = $u2f->getAuthenticateData($user->u2fRegistrations->get()->map->toRegistration()->all());
Cache::put("u2f:auth:{$user->id}", $requests, now()->addMinutes(5));
// client posts { signRequests, signatures }
$updated = $u2f->doAuthenticate(
    Cache::pull("u2f:auth:{$user->id}"),
    $user->u2fRegistrations->get()->map->toRegistration()->all(),
    (object) $request->input('response')
);
$user->u2fRegistrations()->where('key_handle', $updated->keyHandle)
     ->update(['counter' => $updated->counter]);

Gate it as a second factor, not a login. The natural Laravel pattern is a EnsureU2fVerified middleware plus a password-confirmation step (password.confirm) before /u2f/register/*, so a stolen session can't enrol an attacker's key.

CSRF. The client posts JSON via fetch/axios; either send X-CSRF-TOKEN (axios does this from the meta tag automatically) or add u2f/* to VerifyCsrfToken::$except. Prefer the header — you want the CSRF check.

Testing. Don't try to forge client responses. Bind a fake in the container:

$this->mock(\u2flib_server\U2F::class, function ($m) {
    $m->shouldReceive('getRegisterData')->andReturn([['challenge' => 'x'], []]);
    $m->shouldReceive('doRegister')->andReturn((object) [
        'keyHandle' => 'kh', 'publicKey' => 'pk', 'certificate' => 'c', 'counter' => 0,
    ]);
});

Then assert the row lands in u2f_registrations and the challenge was pulled from cache. Test the counter-regression path separately by feeding doAuthenticate a lower counter through a stub.

Behind a load balancer / proxy: set AppID from a config value, never from $request->getSchemeAndHttpHost(). Host-header spoofing would let an attacker mint a challenge for their own origin. Also make sure TrustProxies and URL::forceScheme('https') are configured — U2F only works over HTTPS (localhost excepted).

Gotchas and Tips

  • The AppID must match the browser origin exactly. Scheme, host, port. https://example.com ≠ https://example.com/ ≠ https://www.example.com. A mismatch shows up as a signature/BAD_REQUEST error on the client with no useful server-side message. Put it in .env per environment and assert it on boot.
  • Error handling is by exception, not return value. u2flib_server\Error carries getCode() with U2F_* constants. Catch it and translate to human text before it hits your 500 handler — otherwise users see an ugly stack trace for "you tapped the wrong key":
    catch (\u2flib_server\Error $e) {
        throw ValidationException::withMessages(['u2f' => 'Key rejected. Try again.']);
    }
    
  • Counter regression is a feature, not a bug. doAuthenticate throws when the new counter isn't greater than the stored one (clone detection). Some cheap tokens always report 0, which the library tolerates, but if you restore a DB backup you'll lock users out. Provide an admin "reset counter" path.
  • Challenges expire. If the user fumbles the touch prompt, doRegister gets a stale request. Always Cache::pull (delete-on-read) and return a 419 so the JS can restart the ceremony cleanly.
  • No ext-gmp = slow pure-PHP bignum. Fine for a login endpoint, painful if you script a bulk verify. The second constructor arg (new U2F($appId, $useGmp = true)) is the toggle.
  • json_encode of a Registration includes certificate, which can be a binary blob. Store base64 or raw and decode on read — don't let a text column choke on invalid UTF-8.
  • The client side is the real blocker. You need a copy of u2f-api.js (Google's, unmaintained) and browsers that still expose window.u2f. Current Chrome and Firefox do not. Test this before you write any server code, or you'll ship a login flow nobody can complete.
  • No extension points. No events, no config, no plugin seam. If you need to adapt it (logging, policy on allowed key handles, WebAuthn compatibility), fork it into app/Support/U2f — it's ~300 lines and you'll have more control than patching via composer.
  • Migration plan up front. Because the client API is gone, plan for users to re-enrol via WebAuthn using their existing hardware keys; keep u2f_registrations readable so you can offer "your YubiKey still works" messaging rather than a hard cutover.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route