PHPackages                             benbjurstrom/otpz - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. benbjurstrom/otpz

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

benbjurstrom/otpz
=================

First Factor One-Time Passwords for Laravel (Passwordless OTP Login)

v0.6.0(6mo ago)28010.8k↑62.5%16[1 PRs](https://github.com/benbjurstrom/otpz/pulls)MITPHPPHP ^8.2CI passing

Since Dec 10Pushed 5mo ago2 watchersCompare

[ Source](https://github.com/benbjurstrom/otpz)[ Packagist](https://packagist.org/packages/benbjurstrom/otpz)[ Docs](https://github.com/benbjurstrom/otpz)[ GitHub Sponsors](https://github.com/benbjurstrom)[ RSS](/packages/benbjurstrom-otpz/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (9)Dependencies (13)Versions (10)Used By (0)

 [![OTPz Screenshot](https://github.com/benbjurstrom/otpz/raw/main/art/email.png?raw=true)](https://github.com/benbjurstrom/otpz/blob/main/art/email.png?raw=true)

First Factor One-Time Passwords for Laravel
===========================================

[](#first-factor-one-time-passwords-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/b15c850504b9e8e814d993cc1b541e4bcbf7291ffc02f196c00b6e7efd16df93/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f62656e626a75727374726f6d2f6f74707a2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/benbjurstrom/otpz)[![GitHub Tests Action Status](https://camo.githubusercontent.com/1b11d680fba10c68c4f9269b5fd6b892b87defdb3fa31e2e383dad4c05f1bca0/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f62656e626a75727374726f6d2f6f74707a2f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/benbjurstrom/otpz/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/2c646a6917696fbff375502d96a24887d66a75e84ce99b1d7cded0f76428391b/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f62656e626a75727374726f6d2f6f74707a2f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/benbjurstrom/otpz/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)

This package provides secure first factor one-time passwords (OTPs) for Laravel applications. Users enter their email and receive a one-time code to sign in—no passwords required.

Features
--------

[](#features)

- ✅ **Session-locked** - OTPs only work in the browser session that requested them
- ✅ **Rate-limited** - Configurable throttling with multi-tier limits
- ✅ **Time-based expiration** - Default 5 minutes, fully configurable
- ✅ **Invalidated after first use** - One-time use only
- ✅ **Attempt limiting** - Invalidated after 3 failed attempts
- ✅ **Signed URLs** - Cryptographic signature validation
- ✅ **Detailed error messages** - Clear feedback for users
- ✅ **Customizable templates** - Bring your own email design
- ✅ **Auditable** - Full event logging via Laravel events

---

Quick Start
-----------

[](#quick-start)

### Prerequisites

[](#prerequisites)

OTPz works best with the official [Laravel starter kits](https://laravel.com/starter-kits):

- **React** (Inertia.js)
- **Vue** (Inertia.js)
- **Livewire** (Volt)

> OTPz's frontend components are designed to work out of the box with the Laravel starter kits and make use of their existing UI components (Button, Input, Label, etc.). Because these components are installed into your application you are free to customize them for any Laravel application using React, Vue, or Livewire.

---

Installation
------------

[](#installation)

### 1. Install the Package

[](#1-install-the-package)

```
composer require benbjurstrom/otpz
```

### 2. Run Migrations

[](#2-run-migrations)

```
php artisan vendor:publish --tag="otpz-migrations"
php artisan migrate
```

### 3. Add Interface and Trait to User Model

[](#3-add-interface-and-trait-to-user-model)

```
// app/Models/User.php
namespace App\Models;

use BenBjurstrom\Otpz\Models\Concerns\HasOtps;
use BenBjurstrom\Otpz\Models\Concerns\Otpable;
// ...

class User extends Authenticatable implements Otpable
{
    use HasFactory, Notifiable, HasOtps;

    // ...
}
```

---

Framework-Specific Setup
------------------------

[](#framework-specific-setup)

Choose your frontend framework:

### React (Inertia.js)

[](#react-inertiajs)

#### 1. Publish Components

[](#1-publish-components)

```
php artisan vendor:publish --tag="otpz-react"
```

This copies the following files to your application:

- `resources/js/pages/auth/otpz-login.tsx` - Email entry page
- `resources/js/pages/auth/otpz-verify.tsx` - OTP code entry page
- `app/Http/Controllers/Auth/OtpzController.php` - Self-contained controller handling all OTP logic

> **Note:** These components import shadcn/ui components (`Button`, `Input`, `Label`, `Checkbox`), layout components (`AuthLayout`), and use wayfinder for route generation from the Laravel React starter kit. If you're not using the starter kit, you may need to adjust these imports or create these components.

#### 2. Add Routes

[](#2-add-routes)

Add to `routes/web.php`:

```
use App\Http\Controllers\Auth\OtpzController;

Route::middleware('guest')->group(function () {
    Route::get('otpz', [OtpzController::class, 'index'])
        ->name('otpz.index');
    Route::post('otpz', [OtpzController::class, 'store'])
        ->name('otpz.store');
    Route::get('otpz/{id}', [OtpzController::class, 'show'])
        ->name('otpz.show')
        ->middleware('signed');
    Route::post('otpz/{id}', [OtpzController::class, 'verify'])
        ->name('otpz.verify')
        ->middleware('signed');
});
```

That's it! The controller handles all the OTP logic for you.

---

### Vue (Inertia.js)

[](#vue-inertiajs)

#### 1. Publish Components

[](#1-publish-components-1)

```
php artisan vendor:publish --tag="otpz-vue"
```

This copies the following files to your application:

- `resources/js/pages/auth/OtpzLogin.vue` - Email entry page
- `resources/js/pages/auth/OtpzVerify.vue` - OTP code entry page
- `app/Http/Controllers/Auth/OtpzController.php` - Self-contained controller handling all OTP logic

> **Note:** These components import layout components (`AuthLayout`), and use wayfinder for route generation from the Laravel Vue starter kit. If you're not using the starter kit, you may need to adjust these imports or create these components.

#### 2. Add Routes

[](#2-add-routes-1)

Add to `routes/web.php`:

```
use App\Http\Controllers\Auth\OtpzController;

Route::middleware('guest')->group(function () {
    Route::get('otpz', [OtpzController::class, 'index'])
        ->name('otpz.index');
    Route::post('otpz', [OtpzController::class, 'store'])
        ->name('otpz.store');
    Route::get('otpz/{id}', [OtpzController::class, 'show'])
        ->name('otpz.show')
        ->middleware('signed');
    Route::post('otpz/{id}', [OtpzController::class, 'verify'])
        ->name('otpz.verify')
        ->middleware('signed');
});
```

That's it! The controller handles all the OTP logic for you.

---

### Livewire (Volt)

[](#livewire-volt)

#### 1. Publish Components

[](#1-publish-components-2)

```
php artisan vendor:publish --tag="otpz-livewire"
```

This copies the following files to your application:

- `resources/views/livewire/auth/otpz-login.blade.php` - Email entry page
- `resources/views/livewire/auth/otpz-verify.blade.php` - OTP code entry page
- `app/Http/Controllers/Auth/PostOtpController.php` - Self-contained controller handling OTP verification

> **Note:** These Volt components use Flux UI components and layout components from the Laravel Livewire starter kit. If you're not using the starter kit, you may need to adjust the component markup and styling.

#### 2. Add Routes

[](#2-add-routes-2)

Add to `routes/web.php`:

```
use App\Http\Controllers\Auth\PostOtpController;
use Livewire\Volt\Volt;

Route::middleware('guest')->group(function () {
    Volt::route('otpz', 'auth.otpz-login')
        ->name('otpz.index');

    Volt::route('otpz/{id}', 'auth.otpz-verify')
        ->middleware('signed')
        ->name('otpz.show');

    Route::post('otpz/{id}', PostOtpController::class)
        ->middleware('signed')
        ->name('otpz.verify');
});
```

---

Replacing Fortify Login (Optional)
----------------------------------

[](#replacing-fortify-login-optional)

The latest Laravel starter kits use [Laravel Fortify](https://laravel.com/docs/12.x/fortify) for authentication. If you want to replace the default username/password login with OTPz:

**For React:**

In `app/Providers/FortifyServiceProvider.php`, update the `loginView` method:

```
Fortify::loginView(fn (Request $request) => Inertia::render('auth/otpz-login', []));
```

**For Vue:**

In `app/Providers/FortifyServiceProvider.php`, update the `loginView` method:

```
Fortify::loginView(fn (Request $request) => Inertia::render('auth/OtpzLogin', []));
```

**For Livewire:**

In `app/Providers/FortifyServiceProvider.php`, comment out the default login view:

```
// Fortify::loginView(fn () => view('livewire.auth.login'));
```

Then in `routes/web.php`, update the OTPz route to use `login`:

```
Volt::route('login', 'auth.otpz-login')
    ->name('login'); // Changed path and name from 'otpz'
```

Now when users visit `/login` or are redirected to the login page, they'll see the OTPz email entry form instead of the traditional username/password form.

---

Configuration
-------------

[](#configuration)

### Publish Configuration File (Optional)

[](#publish-configuration-file-optional)

```
php artisan vendor:publish --tag="otpz-config"
```

This is the contents of the published config file:

```
