/laravel-otp

A laravel package to protect your routes with one time passwords (otp)

Primary LanguagePHPMIT LicenseMIT

Laravel OTP

Latest Version on Packagist Software License Build Status StyleCI Quality Score Code Coverage Total Downloads

This package allows you to secure your resources with one time password access (otp).

Example Usage:

Route::get('secret', function (\Illuminate\Http\Request $request): string {
    $token = $request->otpToken();
    $messages[] = "The otp token {$token} has {$token->timeLeft()} out of {$token->expiryTime()} seconds.";

    $token->refresh();
    $messages[] = "The time left can be reset using the refresh method: {$token->timeLeft()}/{$token->expiryTime()}";

    $token->extend(30);
    $messages[] = "The expiry time can be increased using the extend method: {$token->timeLeft()}/{$token->expiryTime()}";

    $messages[] = "You can also invalidate the token immediately. Try refreshing the page ;)";
    $request->otpToken()->invalidate();

    return implode('<br>', $messages);
})->middleware('auth', 'otp');

Contents

Installation

1- Add the package to your dependencies.

$ composer require erdemkeren/laravel-otp;

2- Register the package in your config/app.php file:

only if you are using Laravel <=5.4 or your auto package discovery off.

Erdemkeren\Otp\OtpServiceProvider::class,

3- Publish the components:

Publishes a migration, two views and a configuration file.

$ php artisan vendor:publish

4- Apply the migrations:

Will create a table called otp_tokens to store generated token information.

$ php artisan migrate

5- Register the routes:

These routes are required if you are planning to use otp middleware.

In your RouteServiceProvider, append the following line inside the map method:

// App\RouteServiceProvider@map:
\Erdemkeren\Otp\OtpRoutes::register();

6- Register the route middleware:

Register the otp route middleware inside your App\Http\Kernel.

/**
 * The application's route middleware.
 *
 * These middleware may be assigned to groups or used individually.
 *
 * @var array
 */
protected $routeMiddleware = [
    // [...]
    'otp' => \Erdemkeren\Otp\Http\Middleware\Otp::class,
];

Configuration

This package comes with a set of handy configuration options:

password_generator: The password generator option allows you to decide which generator implementation to be used to generate new passwords.

Available built-in options: string, numeric and numeric-no-0. default: string

table: The name of the table to be used to store the otp tokens.

default: otp_tokens

expiry_time: The expiry time of the tokens in minutes.

default: 15

default_channels: The default notification channels of the token notification.

Usage

Basic Usage

After configuring your instance of the package, you can use the built-in otp middleware alias to secure your endpoints:

Route::get('secret', function (Request $request): string {
    $request->otpToken()->refresh();

    return 'The secret of immortality';
})->middleware('auth', 'otp');

This middleware will redirect any unauthenticated request to the otp/create endpoint which we have registered in the installation process:

  • A password will be generated using the configured password generator.
  • The authenticated user will be notified about the password via the configured notification channel.
  • The user will see a form to submit their password.
  • You can change the appearance of the view under your resources/views/otp directory, modifying create.blade.php file.
  • After a successful authentication; the user will be redirected back to the original route they requested at the first step.
  • The redirected request will also include the otpToken() instance being used by the user.

Advanced Usage

Adding the notification channel method:

If you are not using the mail channel, or your notification channel is expecting a method different than mail or sms, you can register your own method like:

// AppServiceProvider::register():
TokenNotification::macro('AcmeSms', function () {
    // $this is TokenNotification class.
    return $this->notification->code;
});

Don't forget to change your configuration file as well.

Using your own password generator:

To add your own password generator implemetation, you can call addPasswordGenerator method on Otp service like:

// AppServiceProvider::register():
app('otp')->addPasswordGenerator('acme', function (int $length): string {
    return 'your_implementation';
});

If you need more power, you can also create your own password generator class too:

<?php namespace App\Acme\PasswordGenerators;

use Erdemkeren\Otp\PasswordGeneratorInterface;

class AcmePasswordGenerator implements PasswordGeneratorInterface
{
    /**
     * Generate an acme password with the given length.
     *
     * @param  int    $length
     * @return string
     */
    public function generate(int $length): string
    {
        return 'your implementation';
    }
}

You can register you password generator like the previous one:

// AppServiceProvider::register():
Otp::addPasswordGenerator('acme', AcmePasswordGenerator::class);

Don't forget to change your configuration file as well.

Determining the otp channel per notifiable

The Notification class checks otpChannels existence inside the notifiable being notified. If so, this method is being called to determine which notification channel is going to be used to notify the notifiable.

Deeper Knowledge:

The public API consists of two main components: OtpService and the Token which generally is being returned by the service.

Otp Service:

If you are planning to create your own API or the basic functionality is not enough four you, you can use the Otp Service API:

Chencking the validity of a given token:
$isTokenValid = Otp::check($authenticableId, $token);
Setting the password generator:
Otp::setPasswordGenerator('string');
Creating a new token for a given user:
$token = Otp::create(auth()->user(), $length = 6);
// See what can be done with tokens below.
Retrieveing an existing token from the storage by the given plain password:
$token = Otp::retrieveByPlainText(auth()->id(), $otpPassword);
// See what can be done with tokens below.
Retrieveing an existing token from the storage by the given cipher text (token):
$token = Otp::retrieveByCipherText(auth()->id(), $otpPassword);
// See what can be done with tokens below.
Changing the behavior of the Service

The package comes with a ServiceProvider which registers the Otp service to your application's container.

The Otp orchestrates the method calls made to the 3 interface implementations below.

  • PasswordGeneratorManagerInterface
  • EncryptorInterface and
  • TokenInterface

You can write your service provider and register the OtpService with your version of the dependencies.

Note: Because the token class is being used with static calls, you have to send the fully qualified name of your TokenInterface implementation.

Token API:

Getting the attributes of the token:
public function authenticableId();
public function cipherText(): string;
public function plainText(): ?string; // If you have just created the token, plain text will be accessable. If you retrieved it; it won't.
public function createdAt(): Carbon;
public function updatedAt(): Carbon;
public function expiryTime(): int;
public function expiresAt(): Carbon;
public function timeLeft(): int;
public function expired(): bool;
Invalidate a token:
public function revoke(): void;
public function invalidate(): void;

e.g.

public function show(Request $request, $id) {
    if($request->input('revoke_session', false)) {
        $request->otpToken()->revoke();
    }

    return view('heaven');
}
Extend or refresh the token expiry time:
// Extend the usage time of the token for the given seconds:
public function extend(?int $seconds = null): bool;
// Make the token function like it has just been created:
public function refresh(): bool;

e.g.

$token = Otp::retrieveByCipherText(
    auth()->id(),
    $request->input('otp_token')
);

if(! $token->expired()) {
 $token->refresh();
}
Create a new token:
public static function create(
    $authenticableId,
    string $cipherText,
    ?string $plainText = null
): TokenInterface;

e.g.

$token = Token::create(1, 'foo', 'plain foo');
Retrieve a token from the storage by the given attributes:

Make sure that the attributes you provided will return a unique token.

public static function retrieveByAttributes(array $attributes): ?TokenInterface;

e.g.

$token = Token::retrieveByAttributes([
    'authenticable_id' => 1,
    'cipher_text'      => 'foo',
]);
Convert the token to a notification:
public function toNotification(): Notification;

e.g.

$user->notify($token->toNotification());

Changelog

Please see CHANGELOG for more information what has changed recently.

Testing

$ composer test

Credits

  • Hilmi Erdem KEREN
  • Berkay Güre

SymfonyInsight