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.
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)u2flib_server\ErrorStart 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.
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'));
}
}
getRegisterData() creates a challenge and a set of signatures.request and signatures must both be available when the response comes back.request and signatures to the browser.u2f.register(
request.appId,
[request],
signatures,
(response) => {
if (response.errorCode) return handleError(response);
axios.post('/u2f/register', { response });
}
);
doRegister($storedRequest, $response).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.
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.
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.
Do not exercise real U2F crypto in feature tests. Mock your SecondFactorProvider and assert:
u2flib_server\Error exceptions return a generic error,For the wrapper itself, keep one small integration test around the library API so deprecations or PHP-version incompatibilities are caught immediately.
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.
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 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.
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.
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.
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.
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).
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.
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.
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.
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.
How can I help you explore Laravel packages today?