PHPackages                             aymakan/laravel-mfa - 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. aymakan/laravel-mfa

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

aymakan/laravel-mfa
===================

API-only Multi-Factor Authentication enforcement for Laravel (TOTP, middleware-driven)

v1.0.1(4mo ago)01.1k—2.6%MITPHPPHP ^8.4

Since Feb 5Pushed 4mo agoCompare

[ Source](https://github.com/aymakan/laravel-mfa)[ Packagist](https://packagist.org/packages/aymakan/laravel-mfa)[ Docs](https://github.com/aymakan/laravel-mfa)[ RSS](/packages/aymakan-laravel-mfa/feed)WikiDiscussions main Synced yesterday

READMEChangelogDependencies (10)Versions (3)Used By (0)

Laravel MFA Package
===================

[](#laravel-mfa-package)

A production-grade, API-only Multi-Factor Authentication (MFA) enforcement package for Laravel. This package provides a clean, middleware-driven MFA layer that works **after** successful email/password login.

Features
--------

[](#features)

- **TOTP-based MFA** using RFC 6238 (compatible with Google Authenticator, Authy, etc.)
- **API-only** — no Blade views, no redirects; JSON responses only
- **Middleware-driven** — does not replace Laravel authentication
- **Pluggable architecture** — ready for future SMS/Email/Push drivers
- **Multiple verification stores** — session, cache, or database (see [Verification state storage](#verification-state-storage))
- **Security-first** — OTP replay protection, rate limiting, fail-closed design
- **Config-driven** — fully customizable via published config
- **Enforcement options** — global (`required` / `optional`) or per-user (e.g. `mfa_required` attribute)

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 10+ / 11+ / 12+
- Laravel Sanctum (or similar API auth with Bearer token)

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

[](#installation)

```
composer require aymakan/laravel-mfa
```

Publish config and migrations:

```
php artisan vendor:publish --tag=mfa-config
php artisan vendor:publish --tag=mfa-migrations
php artisan migrate
```

Basic setup
-----------

[](#basic-setup)

### 1. Add the HasMfa trait to your User model

[](#1-add-the-hasmfa-trait-to-your-user-model)

```
use Aymakan\Mfa\Traits\HasMfa;

class User extends Authenticatable
{
    use HasMfa;

    // Optional: add to $fillable if using per-user enforcement
    protected $fillable = ['email', 'password', 'mfa_required'];

    protected $casts = [
        'mfa_required' => 'boolean',
    ];
}
```

### 2. Apply the middleware to protected routes

[](#2-apply-the-middleware-to-protected-routes)

```
// routes/api.php (or your API route file)

Route::middleware(['auth:sanctum', 'mfa.verified'])->group(function () {
    Route::get('/user', [UserController::class, 'show']);
    Route::get('/dashboard', [DashboardController::class, 'index']);
    // ... other protected routes
});
```

MFA routes (status, verify, enroll) are **not** protected by `mfa.verified`; they only require auth. Keep your main app routes behind `mfa.verified`.

API endpoints
-------------

[](#api-endpoints)

The package registers these routes under the configured prefix (default: `mfa`, so full paths are e.g. `/mfa/status`):

MethodPathDescriptionGET`/{prefix}/status`MFA status for authenticated userPOST`/{prefix}/verify`Verify MFA code (login flow)POST`/{prefix}/enroll/start`Start MFA enrollmentPOST`/{prefix}/enroll/confirm`Confirm enrollment with OTPDELETE`/{prefix}/enroll/cancel`Cancel pending enrollmentDELETE`/{prefix}/disable`Disable MFA (requires OTP)`/verify` and `/enroll/confirm` and `/disable` use the `throttle:mfa` rate limiter.

Usage examples
--------------

[](#usage-examples)

### Check MFA status

[](#check-mfa-status)

```
GET /mfa/status
Authorization: Bearer {token}
```

Response:

```
{
    "data": {
        "mfa_enabled": true,
        "mfa_verified": true,
        "mfa_type": "totp",
        "mfa_required": true,
        "verified_at": "2026-02-02T12:39:42+00:00"
    }
}
```

`verified_at` is present only when `mfa_verified` is true.

### Enroll in MFA

[](#enroll-in-mfa)

**Step 1: Start enrollment**

```
POST /mfa/enroll/start
Authorization: Bearer {token}
```

Response:

```
{
    "data": {
        "provisioning_uri": "otpauth://totp/MyApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp",
        "type": "totp",
        "message": "Scan the QR code with your authenticator app, then confirm with a code."
    }
}
```

**Step 2:** Generate a QR code from `provisioning_uri` (e.g. with a frontend library or `https://api.qrserver.com`).

**Step 3: Confirm enrollment**

```
POST /mfa/enroll/confirm
Authorization: Bearer {token}
Content-Type: application/json

{"code": "123456"}
```

Response:

```
{
    "data": {
        "enabled": true,
        "message": "MFA has been enabled successfully."
    }
}
```

### Verify MFA (login flow)

[](#verify-mfa-login-flow)

After login, if protected routes return `403` with `MFA_REQUIRED`, call verify with the user’s OTP:

```
POST /mfa/verify
Authorization: Bearer {token}
Content-Type: application/json

{"code": "123456"}
```

Response:

```
{
    "data": {
        "verified": true,
        "message": "MFA verification successful."
    }
}
```

### Disable MFA

[](#disable-mfa)

```
DELETE /mfa/disable
Authorization: Bearer {token}
Content-Type: application/json

{"code": "123456"}
```

Response:

```
{
    "data": {
        "disabled": true,
        "message": "MFA has been disabled."
    }
}
```

Middleware behaviour
--------------------

[](#middleware-behaviour)

When `mfa.verified` is applied:

1. If MFA is globally disabled (`enabled` = false) → pass through.
2. If the user is not authenticated → pass through (let auth middleware handle it).
3. **Enforcement:**
    - If the user **must** complete MFA (`enforcement` = `required` or per-user attribute e.g. `mfa_required` = true): they must have MFA enabled and verified; otherwise → **403** `MFA_REQUIRED`.
    - If MFA is **optional** for this user: only users who already have MFA enabled must verify; others pass.
4. If the user has MFA enabled and is verified → pass through.
5. If the user has MFA enabled but not verified → **403** `MFA_REQUIRED`.

The `mfa_required` field in `GET /mfa/status` indicates whether this user must complete MFA (enroll and/or verify).

Error response when MFA is required but not satisfied:

```
{
    "error": {
        "code": "MFA_REQUIRED",
        "message": "Multi-factor authentication is required."
    }
}
```

Verification state storage
--------------------------

[](#verification-state-storage)

Verification state (“this user has passed MFA for this session / period”) can be stored in three ways:

StoreConfig valueUse when**Session**`session` (default)Frontend sends session cookies (stateful Sanctum). Verification lives for the session.**Cache**`cache`Bearer-only API; no session. Verification is keyed by user id and TTL (e.g. 8 hours).**Database**`database`Bearer-only API; survives cache flush. Uses `mfa_verifications` table and TTL.Set in config:

```
'verification' => [
    'store' => env('MFA_VERIFICATION_STORE', 'session'), // 'session' | 'cache' | 'database'

    // Session (when store === 'session')
    'session_key' => 'mfa_verified_at',
    'lifetime' => null, // null = session lifetime

    // Cache (when store === 'cache')
    'key_prefix' => 'mfa_verified_at',
    'lifetime' => 480, // minutes (e.g. 8 hours)

    // Database (when store === 'database'); run mfa migrations
    'table' => 'mfa_verifications',
    'lifetime' => 480,
],
```

For SPAs using only Bearer tokens (no session cookies), use `cache` or `database` so verification persists across requests.

Configuration options
---------------------

[](#configuration-options)

```
// config/mfa.php

return [
    'enabled' => env('MFA_ENABLED', true),

    // 'optional' = only users who have MFA enabled must verify
    // 'required' = every user must enroll and verify
    'enforcement' => env('MFA_ENFORCEMENT', 'optional'),

    // Per-user attribute (e.g. mfa_required). When true, that user must complete MFA
    // even when enforcement is 'optional'. Set to null to disable.
    'enforcement_per_user_attribute' => env('MFA_ENFORCEMENT_PER_USER_ATTRIBUTE'),

    'default' => env('MFA_DRIVER', 'totp'),

    'drivers' => [
        'totp' => [
            'issuer' => env('MFA_TOTP_ISSUER', config('app.name')),
            'digits' => 6,
            'period' => 30,
            'algorithm' => 'sha1',
            'window' => 1,
        ],
    ],

    'routes' => [
        'prefix' => 'mfa',
        'middleware' => ['api', 'auth:sanctum'],
    ],

    'middleware_alias' => 'mfa.verified',

    'errors' => [
        'mfa_required' => [
            'status' => 403,
            'code' => 'MFA_REQUIRED',
            'message' => 'Multi-factor authentication is required.',
        ],
        'mfa_invalid' => [
            'status' => 422,
            'code' => 'MFA_INVALID',
            'message' => 'Invalid verification code.',
        ],
        'mfa_rate_limited' => [
            'status' => 429,
            'code' => 'MFA_RATE_LIMITED',
            'message' => 'Too many verification attempts. Please try again later.',
        ],
    ],

    'rate_limit' => [
        'max_attempts' => 5,
        'decay_minutes' => 5,
    ],

    'verification' => [
        'store' => env('MFA_VERIFICATION_STORE', 'session'),
        'session_key' => 'mfa_verified_at',
        'lifetime' => null,
        'key_prefix' => 'mfa_verified_at',
        'table' => 'mfa_verifications',
    ],

    'user' => [
        'model' => null, // or \App\Models\User::class
        'foreign_key' => 'user_id',
    ],
];
```

Using the facade
----------------

[](#using-the-facade)

```
use Aymakan\Mfa\Facades\Mfa;

$hasMfa = Mfa::userHasMfaEnabled($user);
$valid = Mfa::verify($user, '123456');
$uri = Mfa::getProvisioningUri($user);
Mfa::disable($user);
```

Security
--------

[](#security)

- **No OTP reuse** — replay protection for verification attempts.
- **Rate limiting** — `throttle:mfa` on verify, confirm, and disable (configurable in `rate_limit`).
- **Fail closed** — if MFA state is unclear, access is denied.
- **Logout** — verification state is cleared on `Illuminate\Auth\Events\Logout`.

Testing
-------

[](#testing)

```
composer test
```

License
-------

[](#license)

MIT License

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance74

Regular maintenance activity

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

148d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7eac134b6f9a938e65bc70448bf19971d1cf82ed9b6345316a29da82cbde21c8?d=identicon)[aymakan](/maintainers/aymakan)

---

Top Contributors

[![maged-ahmed-raslan](https://avatars.githubusercontent.com/u/190361152?v=4)](https://github.com/maged-ahmed-raslan "maged-ahmed-raslan (4 commits)")

---

Tags

middlewarelaraveltotpAuthenticationMFA

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/aymakan-laravel-mfa/health.svg)

```
[![Health](https://phpackages.com/badges/aymakan-laravel-mfa/health.svg)](https://phpackages.com/packages/aymakan-laravel-mfa)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58171.4k14](/packages/api-platform-laravel)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M131](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M131](/packages/roots-acorn)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k29.9M146](/packages/laravel-cashier)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77022.3M151](/packages/laravel-mcp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
