Installation:
composer require spomky-labs/otphp
Add to composer.json if using Laravel's autoloader:
"autoload": {
"psr-4": {
"App\\": "app/",
"OTPHP\\": "vendor/spomky-labs/otphp/src/"
}
}
First Use Case: Generate a TOTP (Time-based OTP) for Google Authenticator:
use OTPHP\TOTP;
use OTPHP\InternalClock;
$clock = new InternalClock();
$totp = TOTP::generate($clock);
$secret = $totp->getSecret();
$provisioningUri = $totp->getProvisioningUri();
Output the provisioningUri (e.g., otpauth://totp/...) to scan in Google Authenticator.
vendor/spomky-labs/otphp/doc/ (e.g., Factory.md, Customize.md).OTPHP\TOTP (Time-based OTP).OTPHP\HOTP (Counter-based OTP).OTPHP\Factory (for provisioning URI parsing).tests/ for real-world usage patterns.// Generate
$totp = TOTP::generate(new InternalClock())
->withLabel('user@example.com')
->withIssuer('MyApp');
// Verify user input (e.g., from a form)
$userInput = request('otp_code');
$isValid = $totp->verify($userInput); // Uses default window (1)
$hotp = HOTP::createFromSecret('SECRET_BASE32')
->withCounter(0); // Start counter at 0
// Verify with a window (e.g., for resync)
$isValid = $hotp->verify('123456', 999, 5); // Checks counters 999-1003
// Generate URI for QR code
$uri = $totp->getProvisioningUri();
// OR parse existing URI
$parsedOtp = Factory::loadFromProvisioningUri($uri, $clock);
$customTotp = TOTP::createFromSecret('SECRET')
->withPeriod(15) // 15-second window
->withDigits(8) // 8-digit codes
->withDigest('sha512') // Stronger hash
->withParameter('image', 'https://app.com/logo.png');
// Migration
Schema::create('otp_secrets', function (Blueprint $table) {
$table->id();
$table->string('user_id');
$table->string('secret'); // Base32-encoded
$table->string('label')->nullable();
$table->string('issuer')->nullable();
$table->timestamps();
});
// Model
class OtpSecret extends Model {
public function getOtpObject() {
return TOTP::createFromSecret($this->secret)
->withLabel($this->label)
->withIssuer($this->issuer);
}
}
public function handle($request, Closure $next) {
$user = Auth::user();
$otpSecret = $user->otpSecret;
$otp = $otpSecret->getOtpObject();
if (!$otp->verify($request->otp_code)) {
return redirect()->back()->withErrors(['otp' => 'Invalid code.']);
}
return $next($request);
}
use SimpleSoftwareIO\QrCode\Facades\QrCode;
$uri = $totp->getProvisioningUri();
$qrCode = QrCode::size(200)->generate($uri);
return response($qrCode)->header('Content-Type', 'image/png');
// Cache the OTP object for 1 hour
$otp = Cache::remember("otp_{$user->id}", 3600, function () use ($user) {
return $user->otpSecret->getOtpObject();
});
Clock Dependency:
Clock interface (PSR-20). Use InternalClock for testing, but replace with a custom clock (e.g., Symfony\Clock) in production for time adjustments.Factory::loadFromProvisioningUri() or TOTP::generate().
$clock = new Symfony\Clock\MockClock(); // For testing
$totp = TOTP::generate($clock);
Window Size:
1 (strict verification). For user-friendly apps, increase to 3–5 for TOTP.counter ± 5).±10 seconds with a 30-second period).
$totp->verify('123456', null, 3); // Checks ±3 periods
Secret Handling:
= or padding issues.OTPHP\Utils\Base32.
use OTPHP\Utils\Base32;
$secret = Base32::encodeUpper(random_bytes(20)); // 20 bytes = 32 chars
Epoch Misuse:
epoch (TOTP-only) shifts the time window. Use sparingly—prefer default (0).Label/Issuer Conflicts:
MyApp:user@example.com).withIssuerIncludedAsParameter(false) to avoid duplication in URIs.Verify Secrets:
$secret = 'JBSWY3DPEHPK3PXP';
$totp = TOTP::createFromSecret($secret);
echo $totp->now(); // Current OTP code
Time Drift:
verify() fails, check the clock skew:
$now = $clock->getTimestamp();
$period = $totp->getPeriod();
$expectedRange = [$now - $period, $now + $period];
echo "Current time: $now | Expected range: " . implode('–', $expectedRange);
Provisioning URI Issues:
try {
$otp = Factory::loadFromProvisioningUri($uri, $clock);
} catch (\OTPHP\Exception\InvalidArgumentException $e) {
// Handle malformed URI
}
Custom Clock for Testing:
class FixedClock implements ClockInterface {
public function getTimestamp(): int {
return 1625097600; // Fixed Unix timestamp
}
}
Override Defaults:
TOTP or HOTP to enforce app-specific rules:
class AppTOTP extends TOTP {
public function verify(string $code, ?int $timestamp = null, ?int $window = null): bool {
return parent::verify($code, $timestamp, $window ?? 3); // Force window=3
}
}
Add Custom Parameters:
OTPInterface to support app-specific metadata:How can I help you explore Laravel packages today?