m1guelpf/laravel-web3-login
Add Web3 authentication to Laravel apps. Let users sign in by signing a message with their Ethereum wallet, then verify the signature and authenticate them—no passwords required. Simple setup for wallet-based login.
This package adds Ethereum wallet authentication (Sign-In with Ethereum style) to a Laravel app: a user proves ownership of an address by signing a server-issued nonce, and the package verifies the signature and logs them in.
Minimal setup:
composer require m1guelpf/laravel-web3-loginphp artisan vendor:publish --tag=web3-login-config (and the migrations tag if you want the wallet_address column shipped with the package). Run php artisan migrate.App\Models\User (typically Web3Login) and ensure wallet_address is in $fillable.config/web3-login.php first — route prefix, middleware, nonce lifetime, and (from v1.1.0) the registration toggle are all there.window.ethereum (personal_sign via ethers.js), then POST the signature back to the verification endpoint.First real use case: gating a token-holders' area. A user connects MetaMask, signs a message, and lands on a dashboard behind auth middleware — no password ever typed.
As of v1.1.0, the first use case also includes onboarding brand-new users: if the signing address isn't attached to any user and registration is enabled, the package creates a User with the wallet address instead of rejecting the login. If you only want existing users to log in, explicitly disable that in config.
Nonce-based challenge flow. Always treat the nonce as single-use and server-side. The canonical pattern is: request nonce → server stores it (session or cache keyed to the address) → user signs → verification consumes and invalidates it. Never let the client supply the nonce in the verification request.
Wallet-first registration (v1.1.0). For apps where the wallet is the identity, make email and password nullable and unique-index wallet_address. Then set the registration option so an unknown address creates a user. Pair this with an event/listener or model observer to send a welcome notification or provision a default workspace.
Hybrid auth. Keep email/password login intact and add wallet login as an additional route. A common pattern is a "link wallet" endpoint that reuses the same signature verification but attaches the address to auth()->user() instead of logging in — useful for existing accounts.
Custom user resolution. Override the user lookup in config or via a bound contract if you have multi-tenant users, soft-deleted users, or need to scope by guard/tenant before creating one.
Route protection and middleware. Apply auth to the app routes, and rate-limit the nonce and verification endpoints (throttle:10,1) — signature verification is cheap but the endpoints are publicly reachable.
Integration tips.
hash_equals on lowercased values.The signed message must match byte-for-byte. A trailing newline, a different nonce casing, or an extra Address: line will make verification fail silently-ish. Log the exact message server-side when debugging.
Nonce replay and expiry. If your nonce lives in the session and the user opens a second tab, they'll clobber each other. Prefer a cache entry keyed by address with a short TTL (2–5 min) and delete on use.
Chain ID and domain binding. A signature is valid on any chain unless you bind it. Include the chain ID and your domain in the message so a signature harvested on a testnet or a lookalike domain can't be replayed. If you support multiple networks, validate the connected chain client-side and reject mismatches server-side.
Migration conflicts on existing apps. v1.1.0's registration path assumes wallet_address exists and is unique. On a live users table, add the column in your own migration (nullable()->unique()) rather than running the package's stub, and be aware SQLite and MySQL differ on how multiple NULL values interact with unique indexes — that's fine for MySQL/Postgres, but test on SQLite.
Registration can silently create accounts. Enabling wallet registration means anyone with a wallet can mint a User row. If your app assumes a password exists or sends email notifications, guard those paths — password will be null, and email will be null unless you collect it post-registration. Add a follow-up "complete your profile" step.
Guards and providers. The package logs in via the default guard. If you use a custom web3 guard or a separate members provider, point the config at the right guard, or the session will authenticate against the wrong model.
Extension points. The config file is the primary seam: route prefix, middleware stack, nonce TTL, registration toggle, and user model. Reach for container bindings only when config isn't enough — overridden lookup/creation logic is easier to test as a dedicated class than as a trait override.
Local development. Wallet extensions won't inject window.ethereum on plain HTTP from a non-localhost origin. Use php artisan serve on 127.0.0.1 with Hardhat/Anvil for a local chain, and test with the same account you use in your fixtures.
Debugging checklist, in order: does window.ethereum exist (HTTPS/localhost)? Is the signature the right v/s/r format? Does the recovered address match after lowercasing? Has the nonce already been consumed? Is the user found but the guard misconfigured?
How can I help you explore Laravel packages today?