libinkk / oneauth
OneAuth is a modular Laravel authentication framework with session, Sanctum, JWT, OTP, social login, email verification, password reset, two-factor authentication, session management, and device security through one unified API.
Package info
pkg:composer/libinkk/oneauth
Requires
- php: ^8.0.2
- firebase/php-jwt: ^6.10
- illuminate/auth: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/console: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/encryption: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/events: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/hashing: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/mail: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/notifications: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/validation: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.5.10|^10.0|^11.0|^12.0
Suggests
- firebase/php-jwt: Useful for JWT token creation and verification.
- laravel/sanctum: Required for the Sanctum authentication driver.
- laravel/socialite: Useful for social OAuth authentication providers.
This package is auto-updated.
Last update: 2026-07-31 07:21:49 UTC
README
OneAuth is a modular, API-first authentication package for Laravel 9, 10, 11, 12, and 13.
It provides one public API for session authentication, Laravel Sanctum, JWT access and refresh tokens, email OTP and OTP login, anonymous login, email verification, password management with history and expiration, two-factor authentication (TOTP, email OTP, SMS OTP with a custom provider), social login via Socialite, session tracking, device tracking with trusted-device policies, account locks, IP and country access rules, suspicious-login events, and audit logs.
OneAuth does not install a frontend, modify your User model, or force one authentication driver. Your Laravel application controls its UI, guards, User model, mail transport, and external providers.
Important
OneAuth 1.x is under active development. Read Current implementation status before using it in production. SMS and WhatsApp delivery remain extension stubs until you bind real providers.
Contents
- Why OneAuth
- Features
- Requirements
- Installation
- Application setup
- Authentication drivers
- Facade API tutorial
- HTTP API tutorial
- OTP authentication
- Email verification
- Password management
- Two-factor authentication
- Social authentication
- Sessions and devices
- Anonymous login
- Security controls
- Middleware
- Events
- Configuration reference
- Database tables
- Extension points
- Artisan commands
- Testing
- Security checklist
- Troubleshooting
- Current implementation status
- Contributing
- Support
- License
Why OneAuth
Laravel applications often combine multiple packages for session login, Sanctum, JWT, OTP, social login, verification, password reset, and 2FA. OneAuth organizes these concerns behind:
- A unified
OneAuthfacade - Replaceable authentication drivers
- Replaceable OTP and OAuth providers
- Repository contracts for sessions and devices
- Configurable JSON API routes
- Package-owned migrations with the
oneauth_prefix - Laravel events for authentication outcomes
- Safe, repeatable installation commands
Features
Available in the package
- Email, username, or phone identifier lookup
- Registration with configurable password rules
- Session authentication driver
- Sanctum token driver
- JWT access tokens
- Opaque JWT refresh tokens with rotation
- Email OTP generation, delivery, hashing, expiration, cooldown, and attempt limits
- Numeric and alphanumeric OTP types
- Guest OTP send and OTP login
- Optional anonymous (guest) login
- Email verification tokens and temporary signed URLs
- Laravel password broker integration
- Password change, password history, and password expiration
- TOTP, email OTP, and SMS OTP two-factor methods (SMS delivery needs a custom provider)
- Single-use recovery codes and trusted-device 2FA skip
- Socialite providers: Google, Apple, GitHub, Facebook, Microsoft, LinkedIn, Twitter, Discord
- Active session records with revoke and logout-other-devices
- Device records with user-agent parsing, trust flags, country, and timezone
- Login attempt records, rate limiting, and persistent account locks
- IP and country allow/block lists
- Suspicious login detection for new devices
- Audit log writes for authentication events
- JSON API routes and exception envelope
- Facade-based API
- Middleware aliases
- Authentication events
- Install, publish, doctor, and cleanup commands
- GitHub Actions test matrix for Laravel 9 through 13
Integration required
- Sanctum requires
laravel/sanctumandHasApiTokens - Social login requires Laravel Socialite and provider credentials
- SMS and WhatsApp OTP or notifications require custom provider implementations
- Session API routes require session middleware
- JWT and Sanctum request authentication require guard or middleware wiring in the host application
- Password reset requires Laravel's password broker, reset token storage, mail, and User provider configuration
- Country restrictions rely on
CF-IPCountry,X-Country-Code, request input, orONEAUTH_DEFAULT_COUNTRY
Not included
- Blade, Vue, React, Livewire, Tailwind, or Bootstrap UI
- Automatic changes to the application User model
- Passport, LDAP, SAML, enterprise SSO, or OAuth server
- Magic links or passkeys
- Role and permission management
Requirements
- Composer 2
- PHP
^8.0.2 - Laravel 9, 10, 11, 12, or 13
- A configured database connection
- A Laravel authenticatable User model
The actual PHP requirement also depends on your Laravel major version. Composer resolves the compatible PHP and Illuminate dependency set.
Installation
Install the package:
composer require libinkk/oneauth
Publish configuration and migrations:
php artisan oneauth:install
Run migrations:
php artisan migrate
You can install and migrate in one command:
php artisan oneauth:install --migrate
The install process is safe to run again:
- Existing
config/oneauth.phpis skipped - Existing published OneAuth migration files are skipped
- Each package migration checks whether its table already exists
- Existing OneAuth tables are not recreated
Use --force only when you intentionally want to overwrite published files:
php artisan oneauth:install --force php artisan oneauth:publish --force
Check your installation:
php artisan oneauth:doctor
The doctor command reports every OneAuth table as [exists] or [missing] and exits with a failure code when a table is missing.
Application setup
User model
OneAuth does not edit your User model. Your configured model must:
- Extend Laravel's authenticatable model
- Have a password attribute
- Have at least one configured identifier field
- Allow the registration attributes you want OneAuth to create
- Hide passwords and other secrets from JSON output
Example:
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Libinkk\OneAuth\Traits\HasOneAuth; class User extends Authenticatable { use HasOneAuth; protected $fillable = [ 'name', 'email', 'username', 'phone', 'password', 'email_verified_at', ]; protected $hidden = [ 'password', 'remember_token', ]; protected $casts = [ 'email_verified_at' => 'datetime', ]; }
The HasOneAuth trait is optional. It provides:
$user->oneauthSessions(); $user->oneauthDevices(); $user->oneauthSocialAccounts();
Use a different model through .env:
ONEAUTH_USER_MODEL=App\Models\Customer
Identifier fields
The default identifier fields are:
'identifier_fields' => ['email', 'username', 'phone'],
Remove fields your users table does not contain. If your database has no username or phone column, leaving these defaults can produce SQL errors during login lookup.
Email OTP, verification, and password reset need a configured Laravel mail transport:
MAIL_MAILER=smtp MAIL_HOST=smtp.example.com MAIL_PORT=587 MAIL_USERNAME=your-username MAIL_PASSWORD=your-password MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=no-reply@example.com MAIL_FROM_NAME="${APP_NAME}"
Routes
OneAuth routes are enabled by default under /oneauth.
ONEAUTH_ROUTES_ENABLED=true ONEAUTH_ROUTE_PREFIX=oneauth
Disable package routes when using only the facade or your own controllers:
ONEAUTH_ROUTES_ENABLED=false
Authentication drivers
Choose one driver:
ONEAUTH_DRIVER=session
Supported values are session, sanctum, and jwt.
Session driver
The session driver uses Laravel's configured Auth guard:
ONEAUTH_DRIVER=session
The default package route middleware is api, which usually does not start a session. For session-based HTTP authentication, publish the config and change:
'routes' => [ 'enabled' => true, 'prefix' => 'oneauth', 'middleware' => ['web'], ],
For SPA setups, configure Laravel's session cookies, CSRF protection, stateful domains, and CORS for your application.
Sanctum driver
Install Sanctum:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Add HasApiTokens to your User model:
use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens; }
Select the driver:
ONEAUTH_DRIVER=sanctum
Login returns a Sanctum plain-text token:
{
"user": {},
"token": "1|plain-text-token",
"refresh_token": null
}
Your application must configure Sanctum middleware or guards so Bearer tokens populate Laravel's current Auth user on protected requests.
JWT driver
firebase/php-jwt is installed with OneAuth. Configure a dedicated secret:
ONEAUTH_DRIVER=jwt ONEAUTH_JWT_SECRET=replace-with-a-long-random-secret ONEAUTH_JWT_TTL=60 ONEAUTH_JWT_REFRESH_TTL=10080
Generate a secret:
php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
JWT login returns:
{
"user": {},
"token": "access-jwt",
"refresh_token": "opaque-refresh-token"
}
Refresh the token pair:
curl -X POST http://localhost/oneauth/refresh \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"refresh_token":"opaque-refresh-token"}'
Refresh tokens are stored as SHA-256 hashes and rotated after use.
Note
OneAuth issues and rotates JWTs, authenticates Bearer access tokens in oneauth.auth, and revokes refresh tokens plus recently issued access tokens on logout and credential revoke.
Facade API tutorial
Import the facade:
use Libinkk\OneAuth\Facades\OneAuth;
Register
$result = OneAuth::register([ 'name' => 'Taylor Doe', 'email' => 'taylor@example.com', 'password' => 'Password123', ]); $user = $result['user'];
Registration does not automatically log in the user.
The default password policy requires:
- At least 8 characters
- One uppercase letter
- One lowercase letter
- One number
Login
Login with email:
$result = OneAuth::login([ 'email' => 'taylor@example.com', 'password' => 'Password123', ]);
Login with username:
$result = OneAuth::login([ 'username' => 'taylor', 'password' => 'Password123', ]);
Login with phone:
$result = OneAuth::login([ 'phone' => '+15551234567', 'password' => 'Password123', ]);
You can also use the generic identifier key:
$result = OneAuth::login([ 'identifier' => 'taylor@example.com', 'password' => 'Password123', ]);
The result shape is driver-dependent:
[
'user' => $user,
'token' => $accessTokenOrNull,
'refresh_token' => $refreshTokenOrNull,
]
OTP login
Send a login OTP (guest or authenticated):
OneAuth::sendOtp([ 'email' => 'taylor@example.com', 'purpose' => 'login', 'channel' => 'email', 'target' => 'taylor@example.com', ]);
Verify the code and establish the active driver session or tokens:
$result = OneAuth::loginWithOtp([ 'email' => 'taylor@example.com', 'target' => 'taylor@example.com', 'code' => '123456', ]);
Verify without logging in (sets session flag when a session exists):
$verified = OneAuth::verifyOtp([ 'email' => 'taylor@example.com', 'purpose' => 'login', 'target' => 'taylor@example.com', 'code' => '123456', ]);
Anonymous login
Requires ONEAUTH_ANONYMOUS_LOGIN=true:
$result = OneAuth::anonymousLogin();
Current user
$user = OneAuth::user();
Logout
OneAuth::logout();
Refresh
$result = OneAuth::refresh();
For JWT, the current request must contain refresh_token.
Email verification
OneAuth::sendEmailVerification(['email' => 'taylor@example.com']); $verified = OneAuth::verifyEmail([ 'email' => 'taylor@example.com', 'token' => $tokenFromEmail, ]);
Password management
OneAuth::forgotPassword(['email' => 'taylor@example.com']); OneAuth::resetPassword([ 'email' => 'taylor@example.com', 'token' => $brokerToken, 'password' => 'NewPassword123', ]); OneAuth::changePassword([ 'current_password' => 'Password123', 'new_password' => 'NewPassword123', ]);
Two-factor authentication
$setup = OneAuth::enableTwoFactor(['method' => 'totp']); // or email / sms OneAuth::verifyTwoFactor(['code' => $code]); OneAuth::disableTwoFactor(['password' => $currentPassword]); try { $result = OneAuth::login($credentials); } catch (\Libinkk\OneAuth\Exceptions\TwoFactorRequiredException $e) { $result = OneAuth::completeTwoFactorLogin([ 'challenge_token' => $e->getChallengeToken(), 'code' => $totpOrEmailCode, ]); }
Social login
$result = OneAuth::socialLogin('google', [ 'access_token' => $providerAccessToken, ]);
Supported provider names: google, apple, github, facebook, microsoft, linkedin, twitter, discord.
Sessions and devices
$sessions = OneAuth::sessions(); OneAuth::revokeSession($sessionId); OneAuth::logoutOtherSessions(); $devices = OneAuth::devices(); OneAuth::trustDevice($fingerprint, true);
Account lock helpers
OneAuth::lockAccount('taylor@example.com', 600, 'manual'); OneAuth::unlockAccount('taylor@example.com');
Select a driver at runtime
$jwt = OneAuth::driver('jwt'); $result = $jwt->login([ 'email' => 'taylor@example.com', 'password' => 'Password123', ]);
Driver contract methods:
attempt(array $credentials)establish(mixed $user)login(array $credentials)logout()refresh()user()check()guest()token()
Prefer OneAuth::login() for application flows. It runs the shared login pipeline (rate limits, locks, access policy, verified email, 2FA, devices, sessions) before the driver establishes credentials.
HTTP API tutorial
All package endpoints return JSON. The default base path is:
/oneauth
Send these headers:
Accept: application/json
Content-Type: application/json
Public endpoints
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/oneauth/register |
Register a user |
POST |
/oneauth/login |
Authenticate credentials |
POST |
/oneauth/login/otp |
Verify a login OTP and authenticate |
POST |
/oneauth/login/anonymous |
Create and authenticate a guest user (opt-in) |
POST |
/oneauth/refresh |
Refresh driver credentials (JWT refresh token) |
POST |
/oneauth/social/{provider}/login |
Authenticate with a Socialite provider token |
POST |
/oneauth/2fa/challenge |
Complete login after a 2FA challenge token |
GET |
/oneauth/email/verify/signed |
Process a temporary signed verification URL |
POST |
/oneauth/password/forgot |
Send a Laravel password reset link |
POST |
/oneauth/password/reset |
Reset a password with broker token |
POST |
/oneauth/otp/send |
Send an OTP (guest or authenticated; resolve user by identifier) |
POST |
/oneauth/otp/verify |
Verify an OTP without establishing login |
Protected endpoints
These routes use oneauth.auth:
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/oneauth/logout |
Log out |
GET |
/oneauth/user |
Return current user |
POST |
/oneauth/email/send-verification |
Send verification token and link |
POST |
/oneauth/email/verify |
Verify email token |
POST |
/oneauth/2fa/enable |
Start 2FA setup (totp, email, or sms) |
POST |
/oneauth/2fa/verify |
Confirm setup or verify a code for the session |
POST |
/oneauth/2fa/disable |
Disable 2FA (requires password or 2FA code) |
GET |
/oneauth/sessions |
List tracked sessions |
DELETE |
/oneauth/sessions/{sessionId} |
Revoke one tracked session |
POST |
/oneauth/sessions/logout-others |
Revoke all sessions except the current one |
GET |
/oneauth/devices |
List tracked devices |
POST |
/oneauth/devices/{fingerprint}/trust |
Mark a device trusted or untrusted |
POST |
/oneauth/password/change |
Change current password |
Register request
curl -X POST http://localhost/oneauth/register \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Taylor Doe", "email": "taylor@example.com", "password": "Password123" }'
Login request
curl -X POST http://localhost/oneauth/login \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "email": "taylor@example.com", "password": "Password123" }'
OTP login request
curl -X POST http://localhost/oneauth/otp/send \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "email": "taylor@example.com", "purpose": "login", "channel": "email", "target": "taylor@example.com" }' curl -X POST http://localhost/oneauth/login/otp \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "email": "taylor@example.com", "target": "taylor@example.com", "code": "123456" }'
Anonymous login request
curl -X POST http://localhost/oneauth/login/anonymous \
-H "Accept: application/json"
Requires ONEAUTH_ANONYMOUS_LOGIN=true.
Current user request
curl http://localhost/oneauth/user \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_TOKEN"
The Authorization header works only after your selected guard or custom middleware authenticates the token into Laravel Auth.
OTP authentication
The default provider sends OTP codes through Laravel Mail.
ONEAUTH_OTP_PROVIDER=email ONEAUTH_OTP_TYPE=numeric ONEAUTH_OTP_LENGTH=6 ONEAUTH_OTP_EXPIRES_IN=300 ONEAUTH_OTP_MAX_ATTEMPTS=5 ONEAUTH_OTP_COOLDOWN=30
Send an OTP with the facade. Authenticated users can omit the identifier. Guests can send a login OTP by email, username, or phone:
$challenge = OneAuth::sendOtp([ 'email' => 'taylor@example.com', 'purpose' => 'login', 'channel' => 'email', 'target' => 'taylor@example.com', ]);
Verify an OTP:
$verified = OneAuth::verifyOtp([ 'email' => 'taylor@example.com', 'purpose' => 'login', 'target' => 'taylor@example.com', 'code' => '123456', ]);
Complete an OTP login (verifies the code and establishes the configured driver session or tokens):
$result = OneAuth::loginWithOtp([ 'email' => 'taylor@example.com', 'target' => 'taylor@example.com', 'code' => '123456', ]);
HTTP requests:
curl -X POST http://localhost/oneauth/otp/send \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"email":"taylor@example.com","purpose":"login","channel":"email","target":"taylor@example.com"}' curl -X POST http://localhost/oneauth/login/otp \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"email":"taylor@example.com","target":"taylor@example.com","code":"123456"}'
OTP security behavior:
- Codes use
random_intfor numeric OTPs or secure alphanumeric generation - Codes are stored with Laravel Hash
- Expiration is enforced
- Attempt limits are enforced
- Send cooldown is enforced
- A verified code cannot be reused
Custom SMS or WhatsApp provider
The built-in SMS and WhatsApp provider classes are stubs. Bind your own implementation of OTPProviderInterface:
<?php namespace App\Auth; use Libinkk\OneAuth\Contracts\OTPProviderInterface; class TwilioOtpProvider implements OTPProviderInterface { public function send( string $channel, string $to, string $code, array $context = [] ): void { // Send the code through your provider. } }
Register it in an application service provider:
use App\Auth\TwilioOtpProvider; use Libinkk\OneAuth\Contracts\OTPProviderInterface; public function register(): void { $this->app->bind(OTPProviderInterface::class, TwilioOtpProvider::class); }
The configured ONEAUTH_OTP_PROVIDER selects the bound package provider. The channel request field does not dynamically replace the container binding.
Email verification
Send a verification message:
$result = OneAuth::sendEmailVerification([ 'email' => 'taylor@example.com', ]);
The response contains:
[
'id' => 1,
'expires_at' => $date,
'signed_url' => $url,
]
Verify a token:
$verified = OneAuth::verifyEmail([ 'email' => 'taylor@example.com', 'token' => 'token-from-email', ]);
The package:
- Stores only a hash of the verification token
- Uses a 30-minute expiration
- Sends a raw email containing the token and temporary signed URL
- Updates
email_verified_atwhen that attribute exists - Dispatches
EmailVerified
Require email verification during login:
ONEAUTH_REQUIRE_VERIFIED_EMAIL=true
Note
Guest verification with a token is supported through OneAuth::verifyEmail and the signed URL route. Authenticated users can also request a fresh verification email.
Password management
Forgot password
curl -X POST http://localhost/oneauth/password/forgot \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"email":"taylor@example.com"}'
This delegates to Laravel's configured password broker.
Reset password
curl -X POST http://localhost/oneauth/password/reset \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "email": "taylor@example.com", "token": "reset-token", "password": "NewPassword123", "password_confirmation": "NewPassword123" }'
Change password
curl -X POST http://localhost/oneauth/password/change \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "current_password": "Password123", "new_password": "NewPassword123" }'
Password resets and changes record the resulting hash in oneauth_password_history, reject reuse against the configured history_limit, prune older history rows, and revoke OneAuth sessions plus Sanctum/JWT credentials.
Facade helpers:
OneAuth::forgotPassword(['email' => 'user@example.com']); OneAuth::resetPassword([ 'email' => 'user@example.com', 'token' => $brokerToken, 'password' => 'NewPassword123', ]); OneAuth::changePassword([ 'current_password' => 'Password123', 'new_password' => 'NewPassword123', ]);
Set password_policy.require_symbol to true when symbol characters are required.
Optional password expiration (days since last recorded password change; 0 disables):
ONEAUTH_PASSWORD_EXPIRES_DAYS=90
Expired passwords block login until the user resets or changes their password.
Two-factor authentication
Enable TOTP 2FA (stores a pending secret until confirmed):
$setup = OneAuth::enableTwoFactor([ 'method' => 'totp', ]);
Enable email OTP 2FA (sends a confirmation OTP; SMS needs a custom OTP provider):
$setup = OneAuth::enableTwoFactor([ 'method' => 'email', ]);
The TOTP setup result returns the secret and plaintext recovery codes once:
[
'method' => 'totp',
'secret' => 'BASE32SECRET',
'recovery_codes' => [
'RECOVERY01',
'RECOVERY02',
],
'confirmed' => false,
'otpauth_uri' => 'otpauth://totp/OneAuth:user@example.com?secret=...',
'issuer' => 'OneAuth',
]
Render a QR code from otpauth_uri in your application UI. Secrets are RFC 4648 base32 for authenticator-app compatibility.
Confirm setup with a package TOTP or email/SMS OTP code (this sets enabled and fires TwoFactorEnabled):
$verified = OneAuth::verifyTwoFactor([ 'code' => '123456', ]);
Verify and consume a recovery code:
$verified = OneAuth::verifyTwoFactor([ 'recovery_code' => 'RECOVERY01', ]);
Disable 2FA (requires current password or a valid TOTP/recovery code):
OneAuth::disableTwoFactor([ 'password' => $currentPassword, ]);
The encrypted secret and recovery codes are cleared when disabled.
When login requires 2FA, OneAuth throws TwoFactorRequiredException with a short-lived challenge_token. Complete the login:
try { $result = OneAuth::login($credentials); } catch (\Libinkk\OneAuth\Exceptions\TwoFactorRequiredException $e) { $result = OneAuth::completeTwoFactorLogin([ 'challenge_token' => $e->getChallengeToken(), 'code' => $totpCode, ]); }
HTTP login returns 403 with two_factor_required and challenge_token. Finish with POST /oneauth/2fa/challenge.
curl -X POST http://localhost/oneauth/2fa/enable \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"method":"totp"}' curl -X POST http://localhost/oneauth/2fa/verify \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"code":"123456"}' curl -X POST http://localhost/oneauth/2fa/challenge \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "challenge_token": "challenge-from-login", "code": "123456" }'
Skip 2FA on trusted devices when enabled:
ONEAUTH_2FA_SKIP_TRUSTED_DEVICE=true
Mark a device trusted after login with OneAuth::trustDevice($fingerprint) or POST /oneauth/devices/{fingerprint}/trust.
Warning
Pending 2FA secrets created before v1.2 used a package-specific secret format. Re-enable 2FA after upgrading if an older pending setup cannot be confirmed in an authenticator app.
Social authentication
Install Socialite:
composer require laravel/socialite
Configure credentials in config/services.php.
Google example:
'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT_URI'), ],
Environment:
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_REDIRECT_URI=
Exchange a provider access token:
$result = OneAuth::socialLogin('google', [ 'token' => $providerAccessToken, ]);
HTTP:
curl -X POST http://localhost/oneauth/social/google/login \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"token":"provider-access-token"}'
Supported provider names (configure Socialite + config/services.php for each):
googleapplegithubfacebookmicrosoftlinkedintwitterdiscord
Providers are resolved from oneauth.social.providers through a shared Socialite adapter.
The social flow:
- Resolves the remote user through Socialite
userFromToken - Finds an existing
oneauth_social_accountslink - Optionally links a verified email match when
social.link_by_emailis true (defaultfalse) - Creates a user when
social.create_user_if_missingis true and no conflicting email exists - Links the provider account
- Dispatches
SocialLogin - Runs the shared login pipeline (verified-email gate, 2FA challenge, then
establishfor the active driver)
Apple may require a compatible Socialite provider adapter and Laravel event registration because Apple is not included in every Socialite installation.
Sessions and devices
Every successful login records:
- One row in
oneauth_sessions - One device row matched by a SHA-1 fingerprint of user agent and IP when no fingerprint is supplied
- One successful login attempt
List sessions:
$sessions = OneAuth::sessions();
List devices:
$devices = OneAuth::devices();
HTTP:
GET /oneauth/sessions
GET /oneauth/devices
Session records use ONEAUTH_IDLE_TIMEOUT for their expiry timestamp.
Device fingerprints and IP data are signals only. Do not treat them as proof of identity.
Device rows store parsed browser, OS, and device name when oneauth.devices.parse_user_agent is true. Trust a device:
OneAuth::trustDevice($fingerprint, true);
curl -X POST http://localhost/oneauth/devices/{fingerprint}/trust \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"trusted":true}'
Revoke a session or other devices:
OneAuth::revokeSession($sessionId); OneAuth::logoutOtherSessions();
Anonymous login
Opt-in guest accounts (disabled by default):
ONEAUTH_ANONYMOUS_LOGIN=true
$result = OneAuth::anonymousLogin();
curl -X POST http://localhost/oneauth/login/anonymous \
-H "Accept: application/json"
Security controls
ONEAUTH_MAX_LOGIN_ATTEMPTS=5 ONEAUTH_LOCKOUT_SECONDS=300 ONEAUTH_DETECT_SUSPICIOUS_LOGIN=true ONEAUTH_ALLOWED_IPS= ONEAUTH_BLOCKED_IPS= ONEAUTH_ALLOWED_COUNTRIES= ONEAUTH_BLOCKED_COUNTRIES= ONEAUTH_DEFAULT_COUNTRY=
After too many failed attempts OneAuth creates a row in oneauth_account_locks. Manual helpers:
OneAuth::lockAccount('taylor@example.com', 600, 'manual'); OneAuth::unlockAccount('taylor@example.com');
New-device logins dispatch SuspiciousLoginDetected when detection is enabled. Password expiration:
ONEAUTH_PASSWORD_EXPIRES_DAYS=90
Set to 0 to disable.
Middleware
OneAuth registers:
| Alias | Behavior |
|---|---|
oneauth.auth |
Requires the selected driver to report an authenticated Laravel user |
oneauth.verified |
Requires an authenticated user with a populated email_verified_at |
oneauth.otp |
Requires oneauth.otp_verified in the session |
oneauth.twofactor |
Requires oneauth.twofactor_verified in the session |
Apply middleware to application routes:
Route::middleware(['oneauth.auth', 'oneauth.verified'])->group(function () { Route::get('/account', AccountController::class); });
OTP and two-factor middleware currently use session flags. They are not token-scoped for pure JWT APIs.
Events
Listen to OneAuth events in your application:
use Libinkk\OneAuth\Events\UserLoggedIn; class RecordLoginAnalytics { public function handle(UserLoggedIn $event): void { $user = $event->user; } }
Dispatched events:
UserRegisteredUserLoggedInUserLoggedOutOTPSentOTPVerifiedEmailVerifiedPasswordResetPasswordChangedTwoFactorEnabledTwoFactorDisabledSocialLoginSuspiciousLoginDetectedAccountLocked
All event objects expose:
$event->user; $event->context;
Do not place tokens, OTP codes, passwords, recovery codes, or provider secrets in event context.
Configuration reference
Publish configuration:
php artisan oneauth:publish
Available environment variables:
# Driver and user ONEAUTH_DRIVER=session ONEAUTH_USER_MODEL=App\Models\User # Routes ONEAUTH_ROUTES_ENABLED=true ONEAUTH_ROUTE_PREFIX=oneauth # Security ONEAUTH_REQUIRE_VERIFIED_EMAIL=false ONEAUTH_MAX_LOGIN_ATTEMPTS=5 ONEAUTH_LOCKOUT_SECONDS=300 ONEAUTH_PASSWORD_EXPIRES_DAYS=0 ONEAUTH_DETECT_SUSPICIOUS_LOGIN=true ONEAUTH_ALLOWED_IPS= ONEAUTH_BLOCKED_IPS= ONEAUTH_ALLOWED_COUNTRIES= ONEAUTH_BLOCKED_COUNTRIES= # OTP ONEAUTH_OTP_PROVIDER=email ONEAUTH_OTP_TYPE=numeric ONEAUTH_OTP_LENGTH=6 ONEAUTH_OTP_EXPIRES_IN=300 ONEAUTH_OTP_MAX_ATTEMPTS=5 ONEAUTH_OTP_COOLDOWN=30 ONEAUTH_OTP_RESEND_LIMIT=3 # Sessions ONEAUTH_IDLE_TIMEOUT=7200 # JWT ONEAUTH_JWT_SECRET= ONEAUTH_JWT_TTL=60 ONEAUTH_JWT_REFRESH_TTL=10080 # Two factor ONEAUTH_2FA_ENABLED=true ONEAUTH_TOTP_ISSUER="${APP_NAME}" ONEAUTH_2FA_SKIP_TRUSTED_DEVICE=false # Anonymous ONEAUTH_ANONYMOUS_LOGIN=false # Devices ONEAUTH_DEFAULT_COUNTRY= ONEAUTH_DEFAULT_TIMEZONE= # Logging ONEAUTH_AUDIT_LOG_ENABLED=true
Values in seconds:
ONEAUTH_LOCKOUT_SECONDSONEAUTH_OTP_EXPIRES_INONEAUTH_OTP_COOLDOWNONEAUTH_IDLE_TIMEOUT
Values in minutes:
ONEAUTH_JWT_TTLONEAUTH_JWT_REFRESH_TTL
The full PHP configuration is in config/oneauth.php.
Database tables
OneAuth uses morph columns instead of a foreign key to a specific users table.
| Table | Purpose |
|---|---|
oneauth_otps |
Hashed OTP challenges, attempts, and expiration |
oneauth_devices |
Device metadata and fingerprint |
oneauth_sessions |
Tracked application sessions |
oneauth_social_accounts |
OAuth provider account links |
oneauth_email_verifications |
Hashed email verification tokens |
oneauth_two_factor |
Encrypted 2FA secret and hashed recovery codes |
oneauth_login_attempts |
Successful and failed login attempts |
oneauth_password_history |
Historical password hashes |
oneauth_refresh_tokens |
Hashed JWT refresh tokens |
oneauth_audit_logs |
Authentication and security audit events |
oneauth_account_locks |
Persistent identifier locks after brute-force or manual lock |
Migrations are safe to run more than once because each migration checks Schema::hasTable() before creating its table.
Extension points
OneAuth provides these contracts:
Libinkk\OneAuth\Contracts\AuthenticationDriverInterface Libinkk\OneAuth\Contracts\OTPProviderInterface Libinkk\OneAuth\Contracts\OAuthProviderInterface Libinkk\OneAuth\Contracts\NotificationProviderInterface Libinkk\OneAuth\Contracts\SessionRepositoryInterface Libinkk\OneAuth\Contracts\DeviceRepositoryInterface
Override a binding in your application service provider:
use App\Auth\DatabaseSessionRepository; use Libinkk\OneAuth\Contracts\SessionRepositoryInterface; public function register(): void { $this->app->bind( SessionRepositoryInterface::class, DatabaseSessionRepository::class ); }
Custom authentication driver
Implement:
interface AuthenticationDriverInterface { public function attempt(array $credentials): mixed; public function establish(mixed $user): array; public function login(array $credentials): array; public function logout(): void; public function refresh(): array; public function user(): mixed; public function check(): bool; public function guest(): bool; public function token(): ?string; }
The current manager resolves only the built-in names session, sanctum, and jwt. To add a named driver, extend or replace OneAuthManager in the container.
NotificationProviderInterface defaults to the email implementation. SMS remains a stub until you bind a real provider.
Artisan commands
Install
php artisan oneauth:install php artisan oneauth:install --migrate php artisan oneauth:install --force
Publish
php artisan oneauth:publish php artisan oneauth:publish --force
Diagnose
php artisan oneauth:doctor
Cleanup
php artisan oneauth:cleanup
Cleanup deletes:
- Expired OTP challenges
- Expired refresh tokens
- Revoked refresh tokens
- Expired tracked sessions
Schedule cleanup in routes/console.php or your console kernel:
use Illuminate\Support\Facades\Schedule; Schedule::command('oneauth:cleanup')->hourly();
For older Laravel applications:
protected function schedule(Schedule $schedule): void { $schedule->command('oneauth:cleanup')->hourly(); }
Testing
Install development dependencies:
composer install
Run tests:
vendor/bin/phpunit
On Windows:
vendor\bin\phpunit
The repository currently includes:
- A Testbench session registration and login feature test
- A TOTP generation and verification unit test
- An in-memory SQLite test database
When contributing, add focused tests for every changed security flow and run the relevant Laravel compatibility matrix.
Security checklist
Before production use:
- Set a dedicated, strong
ONEAUTH_JWT_SECRET - Use HTTPS
- Configure trusted proxies correctly
- Configure Laravel mail and queues
- Hide
passwordandremember_tokenin the User model - Restrict mass assignment on the User model
- Remove identifier fields that do not exist in your users table
- Configure session cookies, CSRF, CORS, and stateful domains
- Use
oneauth.authfor JWT Bearer protection; wire Sanctum middleware when using the Sanctum driver - Keep
social.link_by_emaildisabled unless you accept verified-email auto-linking - Bind production SMS or WhatsApp providers before selecting them
- Add application-level request validation and exception mapping
- Schedule
oneauth:cleanup - Back up data before package upgrades
Never log:
- Passwords
- Raw OTP codes
- Access tokens
- Refresh tokens
- Recovery codes
- OAuth provider secrets
- TOTP secrets
Troubleshooting
Table already exists
OneAuth migrations skip tables that already exist. Run:
php artisan oneauth:doctor php artisan migrate
If a table exists with an incompatible schema, back it up and reconcile the schema manually. A table-name match does not validate its columns.
Table is missing
php artisan oneauth:publish php artisan migrate php artisan oneauth:doctor
Session login does not persist
The default route middleware is api. Publish the config and change route middleware to web, or add your application session middleware.
Unauthenticated with a valid JWT
oneauth.auth accepts a Bearer JWT access token when the JWT driver (or a valid OneAuth JWT) is present. Confirm ONEAUTH_JWT_SECRET matches the issuing app and that the token has not expired.
Sanctum is not installed
composer require laravel/sanctum
Add HasApiTokens to the User model and complete Sanctum's installation.
Socialite is not installed
composer require laravel/socialite
Then configure config/services.php.
SMS or WhatsApp OTP throws an exception
Those providers are extension stubs. Bind your own OTPProviderInterface implementation, switch oneauth.otp.provider to email, or run php artisan oneauth:doctor to confirm the configuration.
Current implementation status
The following details are important for honest production evaluation:
- Email OTP and OTP login are implemented; SMS and WhatsApp OTP remain extension stubs (doctor fails if stubs are selected)
- Session, Sanctum, and JWT drivers issue credentials with Bearer auth, refresh, and revoke flows
- TOTP uses base32 secrets with
otpauth_uri; email 2FA uses package OTP; SMS 2FA needs a custom OTP provider - Trusted devices can skip 2FA when
ONEAUTH_2FA_SKIP_TRUSTED_DEVICEis true - Password expiration, account locks, IP/country allow and block lists, and suspicious-login events are enforced from config
- Device rows parse browser, OS, and device name from the user agent
- Anonymous login is opt-in via
ONEAUTH_ANONYMOUS_LOGIN - Audit logs are written automatically when
ONEAUTH_AUDIT_LOG_ENABLEDis true - OneAuth exceptions return a JSON envelope (
message,error, context) on package/API routes - Social login supports Google, Apple, GitHub, Facebook, Microsoft, LinkedIn, Twitter, and Discord via Socialite
- Session revoke and logout-other-devices APIs are available on the facade and HTTP routes
- GitHub Actions runs a Laravel 9–13 PHPUnit matrix on push and pull requests
These notes are documented so developers can make an informed decision and contribute improvements.
Contributing
Contributions are welcome.
- Fork the repository
- Create a feature branch
- Keep changes modular and driver-independent
- Add or update tests
- Run
vendor/bin/phpunit - Update documentation
- Open a pull request with a clear description and test plan
Contribution rules:
- Support Laravel 9, 10, 11, 12, and 13
- Do not add frontend dependencies
- Do not modify the application User model automatically
- Keep secrets out of logs, events, and exceptions
- Use contracts across package module boundaries
- Preserve backward compatibility or include upgrade notes
Repository:
https://github.com/libin-k-k/oneauth
Issues:
https://github.com/libin-k-k/oneauth/issues
Support
- Website: https://www.libinkk.in
- Email: contact@libinkk.in
- Author: Libin K K
- Author email: libinkk1999@gmail.com
Please use GitHub Issues for reproducible bugs and feature requests. Do not post credentials, tokens, OTP codes, or private application data.
License
OneAuth is open-source software licensed under the MIT License.