Search by

gusmanwidodo / auth-kit-two-factor

gusman

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

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-09-08 11:41 UTC

This package is auto-updated.

Last update: 2026-09-08 11:45:36 UTC


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.

Tests License: MIT

Features

  • TOTP (RFC 6238) implemented in-package; tested against the official RFC test vectors — no otphp/google2fa dependency.
  • Encrypted secrets at rest (Laravel encrypted cast), 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 :@dev rewrites this composer.json to @dev. Revert the gusmanwidodo/auth-kit constraint to ^0.1 and remove any repositories block before committing/tagging a release.

License

MIT © Gusman Widodo. See LICENSE.