gusmanwidodo / auth-kit-two-factor
Two-factor authentication (TOTP) plugin for Auth-Kit. RFC 6238 authenticator-app codes with encrypted secrets, confirm-before-activate, single-use hashed recovery codes, clock-skew window and replay protection. Zero dependencies.
Package info
github.com/gusmanwidodo/auth-kit-two-factor
pkg:composer/gusmanwidodo/auth-kit-two-factor
Requires
- php: ^8.3
- gusmanwidodo/auth-kit: ^0.1
- illuminate/contracts: ^12.0
- illuminate/database: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Two-factor authentication (TOTP) plugin for Auth-Kit. Authenticator-app codes (Google Authenticator, Authy, 1Password, ...) with encrypted secrets, confirm-before-activate enrolment, and single-use recovery codes.
Zero runtime dependencies — RFC 6238 is implemented directly and verified against the RFC's own test vectors.
Features
- TOTP (RFC 6238) implemented in-package; tested against the official RFC
test vectors — no
otphp/google2fadependency. - Encrypted secrets at rest (Laravel
encryptedcast), hidden from serialization, returned only once at provisioning time. - Confirm before activate — a pending enrolment is not active until the user proves a valid code, so nobody locks themselves out with an unscanned secret.
- Single-use recovery codes, stored hashed.
- Replay protection — a consumed time step cannot be reused inside its own validity window.
- Clock-skew window (±1 step by default).
otpauth://provisioning URI for QR codes.
See ADR 007.
Requirements
- PHP
^8.3 gusmanwidodo/auth-kit^0.1- Laravel 12
Installation
composer require gusmanwidodo/auth-kit-two-factor php artisan migrate php artisan vendor:publish --tag=auth-kit-two-factor-config
Usage
Add the trait to your user model:
use Gusmanwidodo\AuthKitTwoFactor\Concerns\HasTwoFactor; class User extends Authenticatable { use HasTwoFactor; }
1. Enrol (pending)
$result = $user->enableTwoFactor('alice@example.com'); $result['secret']; // show ONCE (or render the URI as a QR code) $result['uri']; // otpauth://totp/... -> QR code $result['recovery_codes']; // show ONCE, 8 single-use codes $user->hasTwoFactorEnabled(); // false — still pending
2. Confirm (activate)
$user->confirmTwoFactor($codeFromApp); $user->hasTwoFactorEnabled(); // true
3. Verify at login
try { $user->verifyTwoFactor($code); // TOTP or a recovery code } catch (\Gusmanwidodo\AuthKitTwoFactor\InvalidCodeException $e) { // wrong, expired, or replayed }
Manage
$user->regenerateTwoFactorRecoveryCodes(); // new set, old ones invalidated $user->disableTwoFactor();
Endpoints
| Method | URI | Body |
|---|---|---|
| POST | /auth-kit/two-factor/enable |
{ account_label? } |
| POST | /auth-kit/two-factor/confirm |
{ code } |
| POST | /auth-kit/two-factor/verify |
{ code } |
| POST | /auth-kit/two-factor/disable |
— |
| POST | /auth-kit/two-factor/recovery-codes |
— |
The acting subject is $request->user() by default. If your routes don't use
Laravel auth, bind a resolver:
use Gusmanwidodo\AuthKitTwoFactor\Http\TwoFactorController; TwoFactorController::resolveSubjectUsing(fn ($request) => $request->user('api'));
Hooks
| Event | When | Notes |
|---|---|---|
before:two-factor.enable |
Before the secret is stored | Can override secret |
after:two-factor.enable |
After enrolment is created | — |
after:two-factor.confirm |
After activation | — |
before:two-factor.verify |
Before a code is checked | Set allow=false to veto |
after:two-factor.verify |
On success | method = totp or recovery |
after:two-factor.disable |
After removal | — |
Security notes
- The TOTP secret is encrypted in the database and never re-exposed after enrolment.
- Recovery codes are hashed and single-use.
- Verification accepts a ±1 step window for clock skew, and records the consumed counter so the same code cannot be replayed while still in-window.
- 2FA only becomes active after
confirm(), preventing enrolment lockouts. - Delivering/QR-rendering the provisioning URI is the app's responsibility.
Config
config/auth-kit-two-factor.php:
'issuer' => env('AUTH_KIT_2FA_ISSUER', env('APP_NAME')), 'digits' => 6, 'period' => 30, 'algorithm' => 'sha1', 'window' => 1, // +/- time steps accepted 'recovery_code_count' => 8, 'secret_bytes' => 20, // 160-bit secret (RFC 4226)
Developing against a local core
composer config repositories.auth-kit path ../auth-kit composer require gusmanwidodo/auth-kit:@dev composer install composer test # 36 tests
Note:
composer require :@devrewrites this composer.json to@dev. Revert thegusmanwidodo/auth-kitconstraint to^0.1and remove anyrepositoriesblock before committing/tagging a release.
License
MIT © Gusman Widodo. See LICENSE.