NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Secrets are the base32 text an authenticator app displays. Enrol a user with a fresh secret and a provisioning URI (usually rendered as a QR code), then verify their codes:

use RoundlyConsulting\Crypto\Facades\Crypto;

$secret = Crypto::random()->secret(32);                                   // enrol
$uri    = Crypto::provisioningUri($secret, '[email protected]', 'Acme Inc');

$code        = Crypto::totp()->codeAt($secret);           // SHA1, 6 digits, 30s (default profile)
$matchedStep = Crypto::totp()->verify($secret, $userCode); // int|false, timing-flat, drift window

Crypto::totp(digits: 8)->verify($secret, $userCode);       // a custom profile
Crypto::hotp()->at($secret, $counter);                     // RFC 4226, counter-based

Or call the classes the facade fronts directly:

use RoundlyConsulting\Crypto\Otp\Totp;
use RoundlyConsulting\Crypto\Otp\ProvisioningUri;
use RoundlyConsulting\Crypto\Random\Secret;

$secret = Secret::base32(32);                     // enrol
$uri = ProvisioningUri::totp($secret, '[email protected]', 'Acme Inc');

$totp = new Totp;                                 // SHA1, 6 digits, 30s (default profile)
$code = $totp->codeAt($secret);
$matchedStep = $totp->verify($secret, $userCode); // int|false, timing-flat, drift window

if ($matchedStep === false) {
    // reject
}

verify() returns the matched timestep index or false. A match at timestep 0 returns the int 0, so always compare with === false.

Profiles and the drift window

The default profile — SHA-1, 6 digits, 30 seconds — matches every mainstream authenticator app. Custom profiles use named arguments; digits must be 6–10 and the period at least 1, otherwise InvalidOtpParameterException is thrown:

use RoundlyConsulting\Crypto\Otp\OtpAlgorithm;
use RoundlyConsulting\Crypto\Otp\Totp;

$totp8 = new Totp(OtpAlgorithm::Sha256, digits: 8, period: 60);

$totp->verify($secret, $userCode, window: 2);                // ±2 steps (max Totp::MAX_WINDOW = 10)
$totp->verify($secret, $userCode, timestamp: $submittedAt);  // evaluate at a specific instant

verify() checks every step in the ± window without an early return, so a late match costs the same as an early one, and every comparison is constant-time. Malformed codes are rejected before any HMAC work. The window must be 0–10 (Totp::MAX_WINDOW); anything else throws. The current time is read via CarbonImmutable::now(), so tests can pin it.

Totp methods

MethodReturnsNotes
at($secret, $timestep)stringThe code at an explicit timestep index.
codeAt($secret, $timestamp = null)stringThe code at a unix timestamp (default now).
timestepAt($timestamp)intThe timestep index for a timestamp.
verify($secret, $code, $window = 1, $timestamp = null)int|falseThe matched timestep, or false.

HOTP and provisioning URIs

Hotp computes the counter-based RFC 4226 value; ProvisioningUri builds the otpauth:// link an authenticator app imports. The issuer is always an explicit argument:

use RoundlyConsulting\Crypto\Otp\Hotp;
use RoundlyConsulting\Crypto\Otp\ProvisioningUri;

$hotp = new Hotp;                       // SHA-1, 6 digits
$code = $hotp->at($secret, $counter);   // RFC 4226 value for a base32 secret + counter

$uri = ProvisioningUri::totp(
    secret: $secret,
    label:  '[email protected]',
    issuer: 'Acme Inc',
    // algorithm: OtpAlgorithm::Sha1, digits: 6, period: 30   (defaults)
);
// otpauth://totp/Acme%20Inc:alice%40example.com?secret=…&issuer=Acme%20Inc&algorithm=SHA1&digits=6&period=30

OTP algorithms come from Otp\OtpAlgorithm (Sha1, Sha256, Sha512). HOTP truncation is computed without relying on 64-bit integers, so codes are correct on 32-bit PHP too.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.