chillerlan/php-authenticator
PHP 8.4+ library to generate and validate HOTP (RFC 4226) and TOTP (RFC 6238) one-time passwords—Google Authenticator compatible. Includes Steam Guard server time sync (cURL) and constant-time encoding/hex helpers (Sodium or fallback).
Installation:
composer require chillerlan/php-authenticator
Ensure your composer.json specifies PHP 8.4+ and includes ext-curl and ext-sodium (or paragonie/constant_time_encoding as fallback).
First Use Case (TOTP Setup):
use chillerlan\Authenticator\Authenticator;
$auth = new Authenticator();
$secret = $auth->createSecret(); // Generate a 20-byte secret (default)
$user->otp_secret = $secret; // Store in DB
$qrUri = $auth->getUri('User Account', 'YourAppName'); // Generate QR URI
First Use Case (Verification):
$userSecret = $user->otp_secret;
$auth->setSecret($userSecret);
$otp = '123456'; // From user input
if ($auth->verify($otp)) {
// Authenticated
}
Authenticator class: Core functionality for secret generation, code verification, and URI creation.AuthenticatorOptions: Configure TOTP/HOTP settings (digits, period, algorithm, etc.).AuthenticatorInterface: Constants for modes (TOTP, HOTP) and algorithms (SHA1, SHA256, SHA512).// In a registration/controller method
$auth = new Authenticator([
'digits' => 6,
'period' => 30,
'algorithm' => AuthenticatorInterface::ALGO_SHA256,
]);
// Generate and display secret (e.g., QR code + manual entry backup)
$secret = $auth->createSecret();
$user->otp_secret = $secret;
$user->save();
// Return QR URI for scanning
return response()->json(['qr_uri' => $auth->getUri('User Email', 'YourApp')]);
// In a login controller
$auth = new Authenticator();
$auth->setSecret($user->otp_secret);
$auth->setOptions(new AuthenticatorOptions(['adjacent' => 1])); // Allow 1 adjacent code
$otp = request()->input('otp');
if ($auth->verify($otp)) {
// Proceed to session/auth
} else {
return back()->withErrors(['otp' => 'Invalid code']);
}
// Setup (e.g., during password reset)
$auth = new Authenticator(['mode' => AuthenticatorInterface::HOTP]);
$secret = $auth->createSecret();
$user->otp_secret = $secret;
$user->otp_counter = 0; // Initialize counter
$user->save();
// Verification (e.g., during login)
$auth->setSecret($user->otp_secret);
$auth->setOptions(['mode' => AuthenticatorInterface::HOTP]);
$otp = request()->input('otp');
$storedCounter = $user->otp_counter;
if ($auth->verify($otp, $storedCounter)) {
$user->otp_counter++; // Increment counter
$user->save();
}
// Rotate secret periodically (e.g., every 90 days)
$auth = new Authenticator();
$newSecret = $auth->createSecret();
$user->otp_secret = $newSecret;
$user->save();
// Notify user via email with new QR URI
$newUri = $auth->getUri($user->email, 'YourApp');
Mail::to($user->email)->send(new OTPUpdated($newUri));
Service Provider Binding:
// In AppServiceProvider
$this->app->singleton(Authenticator::class, function () {
return new Authenticator(config('auth.otp'));
});
Configure in config/auth.php:
'otp' => [
'digits' => env('OTP_DIGITS', 6),
'period' => env('OTP_PERIOD', 30),
'algorithm' => \chillerlan\Authenticator\AuthenticatorInterface::ALGO_SHA256,
'mode' => \chillerlan\Authenticator\AuthenticatorInterface::TOTP,
],
Middleware for OTP Protection:
// app/Http/Middleware/VerifyOTP.php
public function handle(Request $request, Closure $next) {
if ($request->user()->requires_otp && !$request->user()->otp_verified) {
$auth = app(Authenticator::class);
$auth->setSecret($request->user()->otp_secret);
if (!$auth->verify($request->input('otp'))) {
return redirect()->route('otp.verify');
}
}
return $next($request);
}
Artisan Command for Secret Management:
// app/Console/Commands/GenerateOTPSecret.php
public function handle() {
$auth = new Authenticator();
$secret = $auth->createSecret();
$this->info("New OTP Secret: " . $secret);
$this->info("QR URI: " . $auth->getUri('Admin Account', 'YourApp'));
}
Unit Test for Verification:
public function test_otp_verification() {
$auth = new Authenticator();
$secret = $auth->createSecret();
$auth->setSecret($secret);
// Mock current time to test specific code
$this->mockTime(1234567890);
$expectedCode = $auth->code(); // Get expected OTP
$this->assertTrue($auth->verify($expectedCode));
}
Feature Test for QR URI:
public function test_qr_uri_generation() {
$auth = new Authenticator();
$secret = $auth->createSecret();
$uri = $auth->getUri('Test User', 'TestApp');
$this->assertStringStartsWith('otpauth://totp/', $uri);
$this->assertStringContainsString('secret=' . $secret, $uri);
}
Time Synchronization Issues:
useLocalTime: false and ensure NTP synchronization:
$auth = new Authenticator(['useLocalTime' => false]);
forceTimeRefresh:
$auth = new Authenticator([
'mode' => AuthenticatorInterface::STEAM_GUARD,
'forceTimeRefresh' => true,
]);
Secret Storage:
encrypt():
$user->otp_secret = encrypt($secret);
Adjacent Codes:
adjacent > 1) can weaken security.adjacent: 1 and document the risk:
$auth->setOptions(['adjacent' => 1]);
HOTP Counter Management:
DB::transaction(function () use ($user, $auth, $otp) {
if ($auth->verify($otp, $user->otp_counter)) {
$user->otp_counter++;
$user->save();
}
});
Algorithm Compatibility:
SHA512 or custom periods.SHA1/SHA256 and standard periods (30s):
$auth = new Authenticator([
'algorithm' => AuthenticatorInterface::ALGO_SHA256,
'period' => 30,
]);
URI Generation Quirks:
issuer or digits in URIs.$auth = new Authenticator(['omitUriSettings
How can I help you explore Laravel packages today?