jiripudil / otp
Library that generates and verifies one-time passwords.
Fund package maintenance!
jiripudil
Installs: 9 713
Dependents: 1
Suggesters: 0
Security: 0
Stars: 21
Watchers: 3
Forks: 3
Open Issues: 0
Requires
- php: ^8.1
- paragonie/constant_time_encoding: ^2.0
Requires (Dev)
- mockery/mockery: ^1.5
- nette/tester: ^2.4
- phpstan/phpstan: ^1.6
README
OTP is a library that generates and verifies One-Time Passwords conforming to the HOTP (RFC 4226) and TOTP (RFC 6238) algorithms. These one-time passwords are commonly used as a second factor during user authentication. In short, your application and the user's OTP application are both able to generate a number based on a shared secret. Whenever you need to authenticate the user, ask them to enter the code generated by their application and verify it with this library.
Installation and requirements
$ composer require jiripudil/otp
OTP requires PHP >= 8.1.
Usage
The entrypoint of this library is the JiriPudil\OTP\OTP
class:
$otp = new JiriPudil\OTP\OTP('My Application', $otpType);
It expects you to provide:
- The
$issuer
name. This is often used by the end-user applications to distinguish OTPs for various services, and should thus clearly identify your application. - An
$otp
type. This library provides implementations for both HOTP and TOTP. See below for details. - The hashing
$algorithm
. This is optional and defaults to SHA-1. ⚠ Please be aware that some end-user implementations such as Google Authenticator only support this algorithm.
Time-based OTP
The JiriPudil\OTP\TimeBasedOtp
class implements the TOTP according to RFC 6238 and it is perhaps the most commonly used type of OTPs. It generates a code in a fixed-time interval that defaults to 30 seconds. ⚠ While the interval can be changed, again, some end-user implementations do not support different intervals than the default.
Using time-based OTPs requires the clocks on your server and in the user's application to be in sync. To compensate for the possible differences between the times, you can provide an optional $tolerance
which determines how many time periods before and after the current time should be considered valid. It defaults to 1, which means that the code for the previous and the next period will pass the verification.
$otpType = new JiriPudil\OTP\TimeBasedOTP();
HMAC-based OTP
The JiriPudil\OTP\HmacBasedOTP
class implements the HOTP according to RFC 4226. Instead of time, this type of OTP relies on a counter that is kept both by the user's application and by the server. That's why the class requires you to provide an implementation of a JiriPudil\OTP\HmacBasedOTP\CounterRepository
which retrieves and updates the counter for a given $account
. Your implementation should operate upon some persistent storage such as a relational database.
The user's counter is incremented every time they request a new code, while the server's counter is only incremented after a successful verification attempt. To account for a possible desynchronization of the counter value, you can configure a $lookAhead
parameter which tells this library to check several subsequent counter values. This parameter defaults to 3.
$otpType = new JiriPudil\OTP\HmacBasedOTP($myCounterRepository);
Setting up 2FA
First of all, you need to generate a random secret. The OTP
class provides a method for that which makes sure that the secret is long enough for the configured hashing algorithm:
$secret = $otp->generateSecret();
The secret must be unique to the user, and should therefore be stored somewhere with other user data – preferably encrypted because it is a very sensitive value:
$myUserRepository->encryptAndSaveOtpSecret($myUser, $secret);
A second value that is tied to the user is the account name: this should be a value that uniquely identifies the user in your application, e.g. their email address. These two pieces of information are exposed to this OTP library in the form of JiriPudil\OTP\Account\AccountDescriptor
. There is a handy simple implementation in JiriPudil\OTP\Account\SimpleAccountDescriptor
which should be sufficient for most cases:
$account = new SimpleAccountDescriptor($myUser->getEmailAddress(), $myUser->getOtpSecret());
You can then call getProvisioningUri
for this $account
to retrieve a URI that can be used to set up the end-user OTP application. You can optionally specify the number of digits the generated code should have – this can be a value between 6 and 8 inclusive, and it defaults to 6 which is the most commonly used value:
$uri = $otp->getProvisioningUri($account, digits: 6);
This URI is usually displayed in the form of a QR code that the user application can scan. Alternatively, you can directly display the Base32-encoded secret for the user to type or copy into their application:
$encodedSecret = $account->getSecret()->asBase32();
Verification
Once the user has set up the account in their OTP application, you can ask them to enter the code and easily verify that the setup is correct:
if ($otp->verify($account, $enteredCode, expectedDigits: 6)) { // successfully verified } else { // incorrect code }
At this point, you should generate and display a set of recovery codes in case the user loses access to their OTP application. After that, you can consider the user's OTP setup in your application finished, and you can start requiring the code as a second factor during their authentication.
Client usage
This package can also be used as an OTP client:
$code = $otp->generate($account, digits: 6);