TOTP Guide#
RFC context#
TOTP is defined in RFC6238. Conceptually, it is HOTP over a moving time-step instead of a manually tracked counter.
The usual flow is:
client and server share a secret
both derive the current time-step from the current Unix timestamp
both generate the OTP from that time-step
the server verifies the submitted OTP inside an allowed drift window
This package exposes both the simple boolean verification flow and a richer result-based flow for drift and replay handling.
Generating a secret#
You can generate a new Base32 secret directly from the TOTP class:
<?php
use Infocyph\OTP\TOTP;
$secret = TOTP::generateSecret();
If you want to control the random byte length:
<?php
$secret = TOTP::generateSecret(64);
Creating a TOTP instance#
<?php
use Infocyph\OTP\TOTP;
$totp = new TOTP($secret, digitCount: 6, period: 30);
Key parameters:
secretis a Base32 secretdigitCountis typically6or8periodis the time-step in seconds, commonly30
Generating an OTP#
<?php
$otp = $totp->getOTP();
Generate for a specific timestamp:
<?php
$otp = $totp->getOTP(1716532624);
Basic verification#
Use verify() when a boolean is enough:
<?php
$totp->verify($otp);
Or for a specific timestamp:
<?php
$totp->verify($otp, timestamp: 1716532624);
Windowed verification#
Real-world authenticators can drift slightly. RFC6238 deployments commonly allow a small validation window around the current time-step.
This library supports:
previous windows only
future windows only
symmetric windows
Example with both past and future drift:
<?php
use Infocyph\OTP\ValueObjects\VerificationWindow;
$result = $totp->verifyWithWindow(
$otp,
timestamp: time(),
window: new VerificationWindow(past: 1, future: 0),
);
$result->matched;
$result->matchedTimestep;
$result->driftOffset;
$result->isExact();
$result->isDrifted();
Another example using the lightweight bool API:
<?php
$isValid = $totp->verify(
$otp,
timestamp: time(),
pastWindows: 1,
futureWindows: 1,
);
Time helpers#
<?php
$totp->getCurrentTimeStep();
$totp->getRemainingSeconds();
$totp->getTimeStepFromTimestamp(1716532624);
These helpers are useful when you want to:
display countdown UI
log or inspect drift
persist the matched time-step as replay state
Replay protection#
RFC6238 itself defines the OTP derivation algorithm, but replay prevention is an application responsibility.
Typical server-side policy:
accept a code once for a given user and time-step
reject reuse of that accepted time-step
Example with replay-aware verification:
<?php
use Infocyph\OTP\Stores\InMemoryReplayStore;
use Infocyph\OTP\ValueObjects\VerificationWindow;
$store = new InMemoryReplayStore();
$result = $totp->verifyWithWindow(
$otp,
window: new VerificationWindow(past: 1, future: 1),
replayStore: $store,
binding: 'user-42',
);
$result->matched;
$result->replayDetected;
Rotation helper#
When rotating a secret, you may want a controlled overlap period:
<?php
$rotation = $totp->rotateSecret($newSecret, gracePeriodInSeconds: 3600);
$rotation['current'];
$rotation['next'];
$rotation['overlapUntil'];
Provisioning#
TOTP is commonly provisioned into authenticator apps using otpauth:// URIs.
<?php
$uri = $totp->getProvisioningUri('[email protected]', 'Example App');
$svg = $totp->getProvisioningUriQR('[email protected]', 'Example App');
Detailed QR setup example:
<?php
$payload = $totp->getEnrollmentPayload(
'[email protected]',
'Example App',
withQrSvg: true,
);
$secret = $payload->secret;
$uri = $payload->uri;
$svg = $payload->qrSvg;
// store $secret securely and render $svg during enrollment
See Provisioning Guide for the full provisioning flow.