---
title: "TOTP & HOTP — Crypto for Laravel | Roundly"
description: "RFC 6238 TOTP and RFC 4226 HOTP with a timing-flat drift window, plus the otpauth:// URI authenticator apps enrol from."
url: https://roundly-consulting.com/open-source/docs/crypto-for-laravel/otp
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Crypto for Laravel](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)

# TOTP & HOTP

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:

```php
use RoundlyConsulting\Crypto\Facades\Crypto;

$secret = Crypto::random()->secret(32);                                   // enrol
$uri    = Crypto::provisioningUri($secret, 'alice@example.com', '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:

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

$secret = Secret::base32(32);                     // enrol
$uri = ProvisioningUri::totp($secret, 'alice@example.com', '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.

## Secret requirements

A secret must decode — as strict, canonical base32, where a bad character throws InvalidEncodingException — to at least 10 bytes (80 bits, Hotp::MIN\_SECRET\_BYTES) that are not all zero. Otherwise Hotp::at(), and so every Totp method that computes a code (at(), codeAt(), and verify() once the code is well-formed), throws InvalidOtpParameterException: emptySecret() — usually a missing config value — shortSecret() or zeroSecret(). HMAC zero-pads a short key, so an empty or all-zero key gives codes anyone can compute. Enrol new users with Secret::base32(), which generates 160 bits (32 characters) by default; the 80-bit floor keeps the 16-character secrets authenticator apps have issued for years verifying.

## 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:

```php
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

| Method | Returns | Notes |
| --- | --- | --- |
| `at($secret, $timestep)` | string | The code at an explicit timestep index. |
| `codeAt($secret, $timestamp = null)` | string | The code at a unix timestamp (default now). |
| `timestepAt($timestamp)` | int | The timestep index for a timestamp. |
| `verify($secret, $code, $window = 1, $timestamp = null)` | int\|false | The 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:

```php
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:  'alice@example.com',
    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
```

ProvisioningUri::totp() validates the secret too, so an app is never handed one Totp cannot decode: a non-base32 secret throws InvalidEncodingException, an empty one InvalidOtpParameterException::emptySecret(). The secret is written uppercase and unpadded, whatever case or padding it arrived in. The 80-bit floor is not applied here — enrol with Secret::base32() and it is never in question.

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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)
