ghostcompiler/secure-auth

Headless Laravel authentication security with TOTP 2FA, passkeys, trusted devices, recovery codes, and Socialite-powered social login helpers.

Maintainers

Package info

github.com/ghostcompiler/SecureAuth

pkg:composer/ghostcompiler/secure-auth

Statistics

Installs: 88

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.2 2026-04-16 21:54 UTC

This package is auto-updated.

Last update: 2026-04-16 22:02:08 UTC


README

Uploads Manager Logo

Laravel PHP Laravel Storage

SecureAuth

SecureAuth is a headless Laravel security package for production-grade authentication hardening.

It adds:

  • Passkeys (WebAuthn)
  • TOTP 2FA
  • Email OTP
  • SMS OTP
  • WhatsApp OTP
  • Trusted Devices
  • Recovery Codes
  • Rate Limiting
  • Publishable OTP templates and transport stubs

Security Notice

SecureAuth does NOT replace Laravel authentication.

It sits on top of your existing login flow and adds secure second-factor verification.

To use it safely:

  • enable enforce_2fa
  • regenerate sessions after login
  • protect verification routes with auth
  • use the built-in attempt* methods where possible

Installation

composer require ghostcompiler/secure-auth
php artisan secure-auth:install
php artisan migrate

Commands

Install

php artisan secure-auth:install

Publishes:

  • config/secure-auth.php
  • package migrations
  • OTP views
  • OTP transport stubs

Publish OTP Assets

php artisan secure-auth:otp:publish

Publishes:

  • OTP mail and message templates
  • custom SMS transport stub
  • custom WhatsApp transport stub

Use --force with either command if you want to overwrite published files.

Published Files

Config

config/secure-auth.php

What it is used for:

  • enables or disables global 2FA enforcement
  • selects WebAuthn security defaults
  • controls OTP channel length, TTL, and max attempts
  • chooses email, SMS, and WhatsApp providers
  • points custom SMS and WhatsApp channels to your application transport classes
  • defines the default views used to render OTP email and message bodies

Views

resources/views/vendor/secure-auth/mail/layout.blade.php
resources/views/vendor/secure-auth/mail/otp.blade.php
resources/views/vendor/secure-auth/messages/sms.blade.php
resources/views/vendor/secure-auth/messages/whatsapp.blade.php

What each file is used for:

  • mail/layout.blade.php wraps OTP email content in a reusable email shell
  • mail/otp.blade.php renders the actual OTP email body and is the main email template you customize
  • messages/sms.blade.php renders the plain-text SMS body sent through built-in or custom SMS providers
  • messages/whatsapp.blade.php renders the plain-text WhatsApp body sent through built-in or custom WhatsApp providers

When you should publish them:

  • publish views when you want to rebrand the email
  • publish views when you want to change the wording of SMS or WhatsApp OTP messages
  • publish views when you want to inject extra context values such as company name, support URL, device label, or request location

Transport Stubs

app/SecureAuth/SmsOtpTransport.php
app/SecureAuth/WhatsAppOtpTransport.php

What each stub is used for:

  • app/SecureAuth/SmsOtpTransport.php place your manual SMS provider API integration here when you do not want to use the built-in providers
  • app/SecureAuth/WhatsAppOtpTransport.php place your manual WhatsApp provider API integration here when you want to call your own API, gateway, or BSP

When you should publish them:

  • publish stubs when your provider is not one of the built-in transports
  • publish stubs when your provider requires a custom send endpoint and a custom verify endpoint
  • publish stubs when you want provider-specific signing, headers, payload formats, or fallback logic

Authentication Flow

Recommended flow:

Login →
  Passkey (Primary)
  OR Password →
       TOTP / Email OTP / SMS OTP / WhatsApp OTP →
           Trusted Device →
               Fully Authenticated

Auth States

SecureAuth stores the auth state in session under:

secure_auth.state

Possible values:

  • guest
  • password_verified
  • two_factor_pending
  • fully_authenticated

Helpers:

SecureAuth::state();
SecureAuth::isFullyAuthenticated();
SecureAuth::isPending2FA();
SecureAuth::isVerified();
SecureAuth::requiresTwoFactor($user);

Middleware

Route-Level Enforcement

Route::middleware(['auth', 'secure-auth.2fa'])->group(function () {
    Route::get('/billing', BillingController::class);
});

Throttling

Route::post('/security/otp/email/verify', VerifyEmailOtpController::class)
    ->middleware(['auth', 'secure-auth.throttle:otp']);

Route::post('/security/otp/sms/verify', VerifySmsOtpController::class)
    ->middleware(['auth', 'secure-auth.throttle:otp']);

Route::post('/security/otp/whatsapp/verify', VerifyWhatsAppOtpController::class)
    ->middleware(['auth', 'secure-auth.throttle:otp']);

Route::post('/security/passkeys/verify', VerifyPasskeyController::class)
    ->middleware(['auth', 'secure-auth.throttle:passkey']);

Global Enforcement

When enforce_2fa is enabled, SecureAuth automatically pushes enforcement middleware into configured middleware groups.

Configuration

Main defaults:

'enforce_2fa' => true,
'proof_ttl_seconds' => 300,

'otp_channels' => [
    'length' => 6,
    'ttl_seconds' => 300,
    'max_attempts' => 5,
],

'webauthn' => [
    'user_verification' => 'required',
],

Strict Preset

SecureAuth::preset('strict');

This enables the package's stricter defaults for 2FA enforcement, OTP limits, trusted devices, and WebAuthn verification.

Public API

TOTP / Passkeys

SecureAuth::enable2FA($user);
SecureAuth::confirmTwoFactorSetup($user, $code);
SecureAuth::disable2FA($user);
SecureAuth::verifyOTP($user, $code);
SecureAuth::generateRecoveryCodes($user);

SecureAuth::registerPasskey($user, $name = null);
SecureAuth::finishPasskeyRegistration($user, $payload, $name = null);
SecureAuth::requestPasskeyAssertion($user);
SecureAuth::verifyPasskeyAssertion($user, $payload);

SecureAuth::attemptOtp($user, $code, $rememberDevice = false, $deviceName = null);
SecureAuth::attemptPasskey($user, $payload, $rememberDevice = false, $deviceName = null);

Email OTP

SecureAuth::sendEmailOtp($user, $email = null, $context = []);
SecureAuth::verifyEmailOtp($user, $code, $email = null, $context = []);
SecureAuth::attemptEmailOtp($user, $code, $email = null, $context = []);

SMS OTP

SecureAuth::sendSmsOtp($user, $phoneNumber, $context = []);
SecureAuth::verifySmsOtp($user, $phoneNumber, $code, $context = []);
SecureAuth::attemptSmsOtp($user, $phoneNumber, $code, $context = []);

WhatsApp OTP

SecureAuth::sendWhatsAppOtp($user, $phoneNumber, $context = []);
SecureAuth::verifyWhatsAppOtp($user, $phoneNumber, $code, $context = []);
SecureAuth::attemptWhatsAppOtp($user, $phoneNumber, $code, $context = []);

State / Security Helpers

SecureAuth::state();
SecureAuth::isVerified();
SecureAuth::isFullyAuthenticated();
SecureAuth::isPending2FA();
SecureAuth::enforce();
SecureAuth::throttle('otp');
SecureAuth::clearThrottle('otp');
SecureAuth::preset('strict');

Social Login Helpers

SecureAuth::socialProviders();
SecureAuth::redirectToSocialProvider($provider, $scopes = [], $with = [], $stateless = null);
SecureAuth::resolveSocialUser($provider, $stateless = null);
SecureAuth::syncSocialAccount($user, $provider, $profile);
SecureAuth::findUserBySocialAccount($provider, $socialIdentity);
SecureAuth::linkedSocialAccounts($user);
SecureAuth::unlinkSocialAccount($user, $provider, $providerUserId = null);

Return Shapes

enable2FA()

Returns:

[
    'secret' => 'BASE32SECRET',
    'otpauth_uri' => 'otpauth://totp/...',
]

confirmTwoFactorSetup()

Returns:

[
    'recovery_codes' => [
        'ABCD-EFGH-IJKL',
        'MNOP-QRST-UVWX',
    ],
    'proof' => VerifiedFactor,
]

registerPasskey()

Returns:

[
    'challenge_id' => '...',
    'publicKey' => [
        // WebAuthn create options
    ],
]

requestPasskeyAssertion()

Returns:

[
    'challenge_id' => '...',
    'publicKey' => [
        // WebAuthn get options
    ],
]

sendEmailOtp(), sendSmsOtp(), sendWhatsAppOtp()

All channel send methods return:

[
    'channel' => 'email', // or sms / whatsapp
    'destination' => 'user@example.com',
    'expires_at' => '2026-04-17T12:00:00+00:00',
]

attempt*() methods

The safe high-level verification methods return:

  • true when the verification succeeds and the session becomes fully authenticated
  • false when the code is invalid

verify*() methods

The low-level verification methods return:

  • VerifiedFactor on success
  • null on invalid code

TOTP Usage

Start TOTP Setup

$setup = SecureAuth::enable2FA($user);

Confirm TOTP Setup

$result = SecureAuth::confirmTwoFactorSetup($user, $code);

Use TOTP in Login Verification

if (! SecureAuth::attemptOtp($user, $code)) {
    return response()->json(['message' => 'Invalid OTP'], 422);
}

Passkey Usage

Start Registration

$options = SecureAuth::registerPasskey($user, 'MacBook Touch ID');

Finish Registration

$passkey = SecureAuth::finishPasskeyRegistration($user, [
    'challenge_id' => $request->challenge_id,
    'id' => $request->id,
    'rawId' => $request->rawId,
    'type' => $request->type,
    'clientDataJSON' => data_get($request->response, 'clientDataJSON'),
    'attestationObject' => data_get($request->response, 'attestationObject'),
    'transports' => $request->transports,
], $request->name);

Start Authentication

$options = SecureAuth::requestPasskeyAssertion($user);

Verify Authentication

if (! SecureAuth::attemptPasskey($user, [
    'challenge_id' => $request->challenge_id,
    'id' => $request->id,
    'rawId' => $request->rawId,
    'type' => $request->type,
    'clientDataJSON' => data_get($request->response, 'clientDataJSON'),
    'authenticatorData' => data_get($request->response, 'authenticatorData'),
    'signature' => data_get($request->response, 'signature'),
])) {
    return response()->json(['message' => 'Invalid passkey'], 422);
}

Email OTP Usage

Send

$result = SecureAuth::sendEmailOtp($user);

You can also override the destination:

$result = SecureAuth::sendEmailOtp($user, 'user@example.com');

Verify

if (! SecureAuth::attemptEmailOtp($user, $code)) {
    return response()->json(['message' => 'Invalid email OTP'], 422);
}

Context

All channel send and verify methods accept $context = [].

That context is merged into the rendered view data, so you can pass additional values for custom templates:

SecureAuth::sendEmailOtp($user, null, [
    'appName' => config('app.name'),
    'supportEmail' => 'help@example.com',
]);

SMS OTP Usage

Send

$result = SecureAuth::sendSmsOtp($user, '+15550001111');

Verify

if (! SecureAuth::attemptSmsOtp($user, '+15550001111', $code)) {
    return response()->json(['message' => 'Invalid SMS OTP'], 422);
}

Built-In SMS Providers

Supported provider transport structure:

  • Twilio
  • Vonage
  • MessageBird
  • MSG91
  • custom SMS transport

Config example:

'otp_channels' => [
    'sms' => [
        'provider' => env('SECURE_AUTH_SMS_PROVIDER', 'twilio'),
        'custom_transport' => \GhostCompiler\SecureAuth\OTP\Transport\CustomSmsOtpTransport::class,
    ],
],

Custom SMS API Setup

Publish assets:

php artisan secure-auth:otp:publish

Edit:

app/SecureAuth/SmsOtpTransport.php

That file includes:

public function sendCode(...)
public function verifyCode(...)

Full contract:

public function sendCode(
    Authenticatable $user,
    OtpChallenge $challenge,
    string $message,
    array $context = []
): void;

public function verifyCode(
    Authenticatable $user,
    OtpChallenge $challenge,
    string $code,
    array $context = []
): bool;

What SecureAuth passes to sendCode():

  • $user the authenticated user model the OTP belongs to
  • $challenge the persisted OTP challenge row, including channel, destination, expiry, and metadata
  • $message the final rendered SMS body from messages/sms.blade.php
  • $context merged runtime context

Default $context keys passed by SecureAuth:

  • code the generated OTP code
  • destination the phone number the code is being sent to
  • any custom keys you passed into SecureAuth::sendSmsOtp($user, $destination, $context)

What verifyCode() must return:

  • true the external provider accepted the code as valid
  • false the provider rejected the code

Important behavior:

  • SecureAuth also verifies the locally stored hashed OTP before returning success
  • returning true from your provider alone is not enough to authenticate the user
  • attemptSmsOtp() marks the session as verified only when provider verification and local hash verification both pass

Point config to your application class:

'otp_channels' => [
    'sms' => [
        'provider' => 'custom',
        'custom_transport' => \App\SecureAuth\SmsOtpTransport::class,
    ],
],

WhatsApp OTP Usage

Send

$result = SecureAuth::sendWhatsAppOtp($user, '+15550002222');

Verify

if (! SecureAuth::attemptWhatsAppOtp($user, '+15550002222', $code)) {
    return response()->json(['message' => 'Invalid WhatsApp OTP'], 422);
}

Built-In WhatsApp Providers

Supported structure:

  • Twilio WhatsApp transport
  • custom WhatsApp transport

Config example:

'otp_channels' => [
    'whatsapp' => [
        'provider' => env('SECURE_AUTH_WHATSAPP_PROVIDER', 'custom'),
        'custom_transport' => \GhostCompiler\SecureAuth\OTP\Transport\CustomWhatsAppOtpTransport::class,
    ],
],

Custom WhatsApp API Setup

Publish assets:

php artisan secure-auth:otp:publish

Edit:

app/SecureAuth/WhatsAppOtpTransport.php

That file includes:

public function sendCode(...)
public function verifyCode(...)

Full contract:

public function sendCode(
    Authenticatable $user,
    OtpChallenge $challenge,
    string $message,
    array $context = []
): void;

public function verifyCode(
    Authenticatable $user,
    OtpChallenge $challenge,
    string $code,
    array $context = []
): bool;

What SecureAuth passes to sendCode():

  • $user the authenticated user model the OTP belongs to
  • $challenge the persisted OTP challenge row, including channel, destination, expiry, and metadata
  • $message the final rendered WhatsApp message from messages/whatsapp.blade.php
  • $context merged runtime context

Default $context keys passed by SecureAuth:

  • code the generated OTP code
  • destination the WhatsApp destination number
  • any custom keys you passed into SecureAuth::sendWhatsAppOtp($user, $destination, $context)

What verifyCode() must return:

  • true the external provider accepted the code as valid
  • false the provider rejected the code

Important behavior:

  • SecureAuth still validates the local hashed OTP challenge before authenticating
  • your custom provider verification is an additional gate, not the only gate
  • attemptWhatsAppOtp() marks the session as verified only when both checks pass

Point config to your application class:

'otp_channels' => [
    'whatsapp' => [
        'provider' => 'custom',
        'custom_transport' => \App\SecureAuth\WhatsAppOtpTransport::class,
    ],
],

OTP View Variables

SecureAuth renders OTP templates through OtpTemplateRenderer.

Every OTP view receives:

  • $user
  • $channel
  • $code
  • $expiresInMinutes

Any values passed through $context are also available in the view.

How values are injected:

  • SecureAuth::sendEmailOtp($user, $email, ['company' => 'Ghost Compiler']) makes $company available inside the email view
  • SecureAuth::sendSmsOtp($user, '+15550001111', ['supportUrl' => 'https://example.com/help']) makes $supportUrl available inside the SMS view
  • SecureAuth::sendWhatsAppOtp($user, '+15550002222', ['deviceLabel' => 'MacBook Pro']) makes $deviceLabel available inside the WhatsApp view

Common customization pattern:

  • keep $code and $expiresInMinutes in every message
  • add your own context values for branding or request-specific metadata
  • avoid removing the expiry warning unless your own text clearly communicates validity duration

mail/otp.blade.php

Default usage:

  • shows $code
  • shows $expiresInMinutes
  • uses the mail layout component
  • can also render any custom context keys you pass when calling sendEmailOtp()

Available variables:

  • $user
  • $channel
  • $code
  • $expiresInMinutes
  • any custom $context keys

messages/sms.blade.php

Default usage:

  • plain text SMS body
  • intended to output a single compact line that mobile carriers can deliver reliably

Available variables:

  • $user
  • $channel
  • $code
  • $expiresInMinutes
  • any custom $context keys

messages/whatsapp.blade.php

Default usage:

  • plain text WhatsApp body
  • intended to output a short readable message that can also be adapted to a template-based provider if needed

Available variables:

  • $user
  • $channel
  • $code
  • $expiresInMinutes
  • any custom $context keys

Mail Layout Variables

mail/layout.blade.php receives:

  • $subject
  • $slot

What those variables mean:

  • $subject the mail subject configured in secure-auth.otp_channels.email.subject
  • $slot the rendered inner HTML from mail/otp.blade.php

The OTP mail view passes the subject automatically from:

config('secure-auth.otp_channels.email.subject')

Output of Published Templates

Default Email Output

The email layout renders:

  • a heading
  • expiry text
  • the OTP code
  • a fallback notice if the user did not request it

Default result:

  • HTML email layout with a card-style body
  • subject line from secure-auth.otp_channels.email.subject
  • OTP code printed inside the email body for manual entry into your UI

Default SMS Output

The default SMS message renders as:

Your SecureAuth verification code is 123456. It expires in 5 minutes.

Default WhatsApp Output

The default WhatsApp message renders as:

Your SecureAuth WhatsApp verification code is 123456. It expires in 5 minutes.

Custom Transport Inputs And Outputs

Both custom transport stubs implement:

GhostCompiler\SecureAuth\Contracts\OtpTransport

Method expectations:

  • sendCode(...) should dispatch the message to your provider and throw an exception if sending fails
  • verifyCode(...) should return a boolean provider verdict and must not mutate session state directly

SecureAuth-side output after send:

[
    'channel' => 'sms',
    'destination' => '+15550001111',
    'expires_at' => '2026-04-17T18:30:00+00:00',
]

SecureAuth-side output after verify:

  • verifyEmailOtp(), verifySmsOtp(), verifyWhatsAppOtp() return a VerifiedFactor object on success or null on failure
  • attemptEmailOtp(), attemptSmsOtp(), attemptWhatsAppOtp() return true on success or false on failure and also finalize the session proof internally

Social Login Helpers

SecureAuth also includes Socialite helpers for linking providers.

Available Methods

SecureAuth::socialProviders();
SecureAuth::redirectToSocialProvider('google');
SecureAuth::resolveSocialUser('google');
SecureAuth::syncSocialAccount($user, 'google', $profile);
SecureAuth::findUserBySocialAccount('google', $profile);
SecureAuth::linkedSocialAccounts($user);
SecureAuth::unlinkSocialAccount($user, 'google');

Best Practices

  • Always enable enforce_2fa
  • Use HTTPS
  • Regenerate sessions after login
  • Keep OTP verification routes behind auth
  • Prefer attempt*() methods for production flows
  • Use published templates instead of hardcoding OTP message strings in controllers
  • Use custom transport stubs when your SMS or WhatsApp provider is not covered by the built-in transports

Testing

Current package tests cover:

  • TOTP success and failure
  • passkey success
  • replay prevention
  • trusted-device validation
  • middleware enforcement
  • email OTP
  • SMS OTP
  • custom SMS transport
  • WhatsApp OTP
  • custom WhatsApp transport

Contributing

PRs welcome.

License

MIT