Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Multimodal Php Laravel Package

betterauth/multimodal-php

Framework-agnostic PHP authentication with session, Paseto V4 token, or hybrid modes. Includes OAuth social login, OpenID Connect, magic links, TOTP 2FA, risk-based session scoring, multi-tenancy, plugins, and PDO storage.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: betterauth/multimodal-php is archived and deprecated — it was merged into betterauth/symfony-bundle v1. There is no Laravel bundle, no service provider, no config file, no facade, and no guard. Everything below is manual wiring you own. For a greenfield Laravel app, the honest recommendation is Sanctum/Fortify. The instructions below are for teams that already have it in composer.json, or that need the specific features (Paseto V4, OIDC server, adaptive session risk scoring, plugin hooks) badly enough to pay the integration tax.

Minimal steps if you proceed:

composer require betterauth/multimodal-php

Then, in order, read:

  1. src/Interfaces/ (28 interfaces) — this is the real API surface. Any repository you don't like, you replace.
  2. src/Core/Config/AuthConfig.php — the only "config file" you get. Map it from config/betterauth.php yourself.
  3. src/Core/AuthManager.php — the façade you'll bind into the container.
  4. src/Storage/Pdo/ — note the MySQL/MariaDB DDL caveat before you plan tests.

First use case — session-mode login for a Blade app:

// app/Providers/BetterAuthServiceProvider.php
public function register(): void
{
    $this->app->singleton(AuthConfig::class, fn () => AuthConfig::forHybrid(
        config('betterauth.secret') // >= 32 chars, env-backed
    ));

    $pdo = fn () => $this->app['db']->connection()->getPdo();

    $this->app->singleton(SessionAuthManager::class, fn () => new SessionAuthManager(
        new PdoUserRepository($pdo()),
        new SessionService(new PdoSessionRepository($pdo()), $this->app->make(AuthConfig::class)),
        new PasswordHasher(),
    ));

    $this->app->singleton(AuthManager::class, fn ($app) => new AuthManager(
        $app->make(AuthConfig::class),
        sessionAuth: $app->make(SessionAuthManager::class),
    ));
}
// app/Http/Controllers/Auth/LoginController.php
public function store(LoginRequest $request, AuthManager $auth)
{
    $result = $auth->signIn($request->email, $request->password); // throws/returns error on bad creds
    $request->session()->put('ba_session', $result['session']->getToken());
    $request->session()->put('ba_session_id', $result['session']->getId()); // opaque, for revocation
    $request->session()->regenerate();
    return redirect()->intended('/dashboard');
}

That's the whole first use case: bind, sign in, stash the token in the Laravel session, validate on each request. Nothing else in Laravel knows about it.

Implementation Patterns

Pick a mode per surface, not per app. forMonolith for Blade routes, forApi for /api/*, forHybrid only when the same login must serve both (it costs you a session write and a token mint on every sign-in). Bind two AuthManager instances under distinct keys rather than one hybrid instance.

Middleware is your guard. There is no Auth::user(). Write one middleware per mode:

public function handle(Request $request, Closure $next)
{
    $token = $request->session()->get('ba_session');
    if (!$token || !($user = $this->auth->validateSession($token))) {
        return redirect()->route('login');
    }
    $request->attributes->set('ba_user', $user);   // UserDto, not an Eloquent model
    return $next($request);
}

For API routes, read $request->bearerToken() and call the token-mode validator on TokenAuthManager (confirm the exact method name in src/Core/TokenAuthManager.php — the README only shows signIn/refresh). Refresh via $auth->refresh($refreshToken) — rotation is atomic by design, so never re-issue manually.

Bridge to Eloquent by ID, not by model. signIn() returns a UserDto. Don't let it leak into controllers. Resolve your own User model (User::find($dto->getId())) once in the middleware and expose that; keep the DTO inside the auth layer. This is what makes the eventual rip-out survivable.

OAuth = two routes plus a session-held state.

$authUrl = $oauth->getAuthorizationUrl('google', ['scope' => 'email profile']);
session()->put('oauth_state', $authUrl['state']);
return redirect()->away($authUrl['url']);

// callback
$result = $oauth->handleCallback('google', $request->code, $redirectUri, $request->ip(),
    $request->userAgent(), $request->state, session()->pull('oauth_state'));

Register the provider instances as singletons in the container; they're stateless and cheap to reuse.

Use Laravel Mail for magic links. Ship a class implementing MagicLinkStorageInterface-adjacent email sender over Mail::to(...)->send(...) instead of PdoEmailSender. sendMagicLink('email', $sender, $redirect) takes the sender as an argument precisely because the author knew you'd do this.

2FA is a two-phase login. Use the split primitives from 0.0.3: verifyCredentials() → redirect to a TOTP challenge → completeTwoFactorLogin($user, $totpProvider, $code). Note the 0.1.0 signature change: it now requires the TotpProvider and code. If you upgrade and your controller still calls the old signature, you get a fatal ArgumentCountError — annoying, but at least not a silent 2FA bypass.

Plugins are your best integration seam. Rather than polling library internals, register a plugin in boot() whose hooks dispatch real Laravel events and queue jobs:

$context->registerHook('user.logged_in', fn (array $data) =>
    LoginSucceeded::dispatch($data['user_id'], request()->ip()));
$context->registerHook('session.created', fn (array $data) =>
    RecordSessionJob::dispatch($data)->onQueue('audit'));

Hooks: user.created, user.logged_in, user.logged_out, token.created, session.created, oauth.linked.

Multi-tenancy by foreign key, not by adoption. The org/team/invitation repositories are a parallel data model. Let them own membership rows keyed on the library user id, and attach a tenant_id (org id) to your own Eloquent models via a global scope resolved in middleware. Don't try to make the org entity your tenant root.

Testing. RefreshDatabase doesn't know about these tables. Either invoke the repositories' createTable() in a base TestCase::setUp() (MySQL only) or maintain hand-written migrations. Add an integration test asserting validateSession() rejects after revokeSession($userId, $sessionId) — that's the security-critical path and it's cheap to cover.

Gotchas and Tips

  • Archived means no security patches. The 0.1.0 changelog is a serious audit pass (hashed ephemeral tokens, PKCE mandatory for public clients, TOTP anti-replay, refresh-token family revocation). That's the quality bar of the last build you'll ever get. Treat composer audit output on this package as permanently frozen and track it manually.
  • PDO storage is MySQL/MariaDB only. createTable/upserts are MySQL-flavored. Laravel's .env.testing default is SQLite :memory: — your test suite will fail on schema, not on logic. Either test against MySQL (slower, but honest) or implement the four or five interfaces you actually use over Eloquent.
  • PSR-6 vs PSR-16. The RateLimiter requires a PSR-6 cache; Laravel's Cache/Repository is PSR-16. Wrap it (symfony/cache Psr6CacheAdapter over Cache::store()), otherwise you're on allowInMemoryFallback: true, which under a multi-worker/queue setup is effectively no rate limiting. Same trap under Octane: per-worker memory means per-worker counters.
  • Password hashing diverges from Hash. This is Argon2id with its own cost settings and its own rehash-on-login. You cannot use Auth::attempt, Fortify, Breeze, or Jetstream. If you have legacy bcrypt users, verify in a test that the hasher's verify() accepts $2y$ hashes (it should, if it delegates to password_verify) before betting on auto-rehash migration — don't assume.
  • Paseto is not JWT. No interop with tymon/jwt-auth, Passport, or any JWT-decoding middleware. Your API clients get an opaque-looking V4 local token; there is no public claim set to read in the frontend.
  • Two sessions, one cookie jar. Laravel's session cookie and the library's session token are unrelated. Store the library token in the Laravel session (as above) and don't try to configure cookie flags in both places — the library's HttpOnly/Secure/SameSite settings only matter if you're issuing its cookie yourself, which under Laravel you are not.
  • Revoke by opaque id. revokeSession($userId, $sessionId) was changed in 0.0.3 to resolve via findById(), not the token, specifically so expired sessions stay revocable. Persist $session->getId() at login or you can't do "log out other devices".
  • Upgrading invalidates live magic links, password-reset links, email-verification links and OAuth auth codes (hashed-at-rest, no re-hash migration). Drain or expire them before deploy; your support inbox will notice before your monitoring does.
  • Don't add your own anti-enumeration messages. The library already does timing-equalized dummy password_verify() and silently succeeds on unknown emails for magic links. A friendly "no account with that email" in your controller undoes it.
  • OAuth auto-linking requires a verified email. GitHub/Microsoft/Twitter now report the real emailVerified. If you build a "link this provider to my existing account" flow, gate it on that flag — this was a documented account-takeover fix.
  • Extension points worth knowing: all 28 repository interfaces (swap in Eloquent/Redis), PluginInterface hooks (events/audit/queue), RateLimiterInterface (custom backing store), and RsaJwt signer + OidcProvider::getJwks() if you're acting as an SSO provider and need RS256 + a JWKS endpoint at /.well-known/jwks.json.
  • Plan the exit while you're integrating. The maintained successor is the Symfony bundle; there is no Laravel equivalent. Keep the blast radius to one service provider, one middleware per mode, and one AuthService that translates DTOs into your domain — then moving to Sanctum later is a week, not a rewrite.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource