Skip to content

Exceptions

Every exception the library throws implements OTPHP\Exception\OTPExceptionInterface, and every one of them also extends a standard PHP exception. So you can catch the library as a whole, catch one precise failure, or keep the catch (InvalidArgumentException) you already had — all three work.

use OTPHP\Exception\OTPExceptionInterface;

try {
    $otp = Factory::loadFromProvisioningUri($uri, $clock);
} catch (OTPExceptionInterface $e) {
    // anything this library refuses
}
Exception Extends Thrown when
InvalidParameterException InvalidArgumentException A parameter has an invalid value
InvalidLabelException InvalidArgumentException A label or issuer breaks the key URI grammar
InvalidProvisioningUriException InvalidArgumentException An otpauth:// URI cannot be read
ParameterNotFoundException InvalidArgumentException A parameter that was never set is read
SecretDecodingException RuntimeException A secret is not valid Base32

A wrong code is not an exception. verify() returns false.

InvalidParameterException

The one you will meet most. It carries the name and the value that were refused, as public readonly properties:

use OTPHP\Exception\InvalidParameterException;

try {
    $totp = $totp->withDigits($request->get('digits'));
} catch (InvalidParameterException $e) {
    $e->getMessage();      // "Digits must be between 1 and 10."
    $e->parameterName;     // "digits"
    $e->parameterValue;    // 0
}

It covers secret, algorithm, digits, period, epoch, counter, secretSize, timestamp, leeway and the internal timecode. The rules each of them enforces are on the pages of The parameters.

InvalidLabelException

Same shape, with labelName and labelValue:

use OTPHP\Exception\InvalidLabelException;

try {
    $totp = $totp->withLabel('user:invalid:format');
} catch (InvalidLabelException $e) {
    $e->getMessage();   // "Neither issuer nor account name in label may contain a colon."
    $e->labelName;      // "label"
    $e->labelValue;     // "user:invalid:format"
}

It is also what getProvisioningUri() throws when no label and no issuer are set, and when the label contains a colon. See The label and the issuer.

InvalidProvisioningUriException

From the factory: a scheme that is not otpauth, a missing host, path or query, a type other than totp or hotp, or no secret.

use OTPHP\Exception\InvalidProvisioningUriException;

try {
    $otp = Factory::loadFromProvisioningUri('invalid-uri', $clock);
} catch (InvalidProvisioningUriException $e) {
    $e->getMessage();   // "Not a valid OTP provisioning URI"
}

A URI whose shape is right but whose values are not throws the exception of the offending parameter instead — algorithm=md5 throws InvalidParameterException. Catch OTPExceptionInterface if you only want to know that the URI was unusable.

ParameterNotFoundException

getParameter() throws rather than returning null, so a typo cannot pass for an absent value:

use OTPHP\Exception\ParameterNotFoundException;

try {
    $totp->getParameter('nonexistent');
} catch (ParameterNotFoundException $e) {
    $e->parameterName;  // "nonexistent"
}

hasParameter() asks the same question without the exception.

SecretDecodingException

A RuntimeException, not an InvalidArgumentException, because of when it happens: the secret is decoded the first time a code is computed, not when it is set. An invalid secret is accepted by createFromSecret() and fails at now(), at() or verify().

use OTPHP\Exception\SecretDecodingException;

$totp = TOTP::createFromSecret('INVALID!@#', $clock);   // no exception here

try {
    $totp->now();                                       // here
} catch (SecretDecodingException $e) {
    // "Unable to decode the secret. Is it correctly base32 encoded?"
}

To validate a secret a user typed, generate a code from it. Constructing the object proves nothing.

Catching precisely

use OTPHP\Exception\InvalidParameterException;
use OTPHP\Exception\InvalidProvisioningUriException;

try {
    $otp = Factory::loadFromProvisioningUri($uri, $clock);
    $valid = $otp->verify($code);
} catch (InvalidProvisioningUriException $e) {
    $logger->error('Stored provisioning URI is unusable.', ['user' => $user->getId()]);
} catch (InvalidParameterException $e) {
    $logger->error('Refused parameter.', ['name' => $e->parameterName]);
}

Two habits worth keeping. Do not put the message in an HTTP response: these messages exist to be read by you, and $e->parameterValue may hold the secret. Do not log the URI or the secret — see Security recommendations.

Nothing to migrate

These classes arrived without breaking anything. Each extends the standard exception that was thrown before, the messages are unchanged, and existing catch (InvalidArgumentException) and catch (RuntimeException) blocks keep working. Move to the specific types when it helps you, at your own pace.