Authentification Web3 avec Laravel Filament et ethers.js

December 28, 2025 Tutoriel

Ajoutez un bouton 'Connect with Crypto Wallet' à votre page de connexion Filament.

Authentification Web3 avec Laravel Filament et ethers.js

Apprenez à implémenter une authentification par portefeuille crypto (MetaMask, Coinbase Wallet, etc.) dans votre application Laravel FilamentPHP. Ce guide vous accompagne pour ajouter un bouton "Connect with Crypto Wallet" sur votre page de connexion avec vérification de signature cryptographique.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Une application Laravel avec FilamentPHP v4 configuré
  • Composer installé
  • Une extension de navigateur wallet (MetaMask, Coinbase Wallet, etc.) pour tester

Installation des dépendances

composer require kornrunner/keccak simplito/elliptic-php

Ces packages permettent de vérifier les signatures Ethereum côté serveur.

Migration de la base de données

Créez une migration pour ajouter l'adresse Ethereum aux utilisateurs :

php artisan make:migration add_eth_address_to_users_table --table=users

Dans la migration :

public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('eth_address', 42)->unique()->nullable()->after('email');
    });
}

public function down(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn('eth_address');
    });
}

N'oubliez pas d'ajouter eth_address au $fillable du modèle User.

Configuration

Créez config/metamask.php :

return [
    'enabled' => env('METAMASK_AUTH_ENABLED', true),
    'auto_register' => env('METAMASK_AUTO_REGISTER', true),
    'signature_message' => env('METAMASK_SIGNATURE_MESSAGE',
        'Sign this message to authenticate with :app_name. Nonce: :nonce'),
];

Controller d'authentification

Créez app/Http/Controllers/MetamaskAuthController.php :

class MetamaskAuthController extends Controller
{
    public function signature(Request $request): JsonResponse
    {
        $nonce = Str::random(32);
        $request->session()->put('metamask_nonce', $nonce);

        $message = str_replace(
            [':app_name', ':nonce'],
            [config('app.name'), $nonce],
            config('metamask.signature_message')
        );

        return response()->json(['message' => $message, 'nonce' => $nonce]);
    }

    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'address' => 'required|string|size:42',
            'signature' => 'required|string',
        ]);

        $address = $request->input('address');
        $signature = $request->input('signature');
        $nonce = $request->session()->get('metamask_nonce');

        if (! $nonce) {
            return response()->json(['error' => 'Session expired.'], 422);
        }

        $message = str_replace(
            [':app_name', ':nonce'],
            [config('app.name'), $nonce],
            config('metamask.signature_message')
        );

        if (! $this->verifySignature($message, $signature, $address)) {
            return response()->json(['error' => 'Invalid signature.'], 422);
        }

        $request->session()->forget('metamask_nonce');

        $user = User::where('eth_address', strtolower($address))->first();

        if (! $user && config('metamask.auto_register')) {
            $user = User::create([
                'name' => substr($address, 0, 6) . '...' . substr($address, -4),
                'email' => strtolower($address) . '@wallet.local',
                'eth_address' => strtolower($address),
                'password' => bcrypt(Str::random(32)),
            ]);
        }

        if (! $user) {
            return response()->json(['error' => 'No account found.'], 422);
        }

        Filament::auth()->login($user, remember: true);

        return response()->json([
            'success' => true,
            'redirect' => Filament::getCurrentPanel()?->getUrl() ?? '/admin',
        ]);
    }

    protected function verifySignature(string $message, string $signature, string $address): bool
    {
        $msglen = strlen($message);
        $hash = Keccak::hash("\x19Ethereum Signed Message:\n{$msglen}{$message}", 256);
        $sign = ['r' => substr($signature, 2, 64), 's' => substr($signature, 66, 64)];
        $recid = ord(hex2bin(substr($signature, 130, 2))) - 27;

        if ($recid !== ($recid & 1)) {
            return false;
        }

        $ec = new EC('secp256k1');
        $pubkey = $ec->recoverPubKey($hash, $sign, $recid);
        $recoveredAddress = '0x' . substr(
            Keccak::hash(substr(hex2bin($pubkey->encode('hex')), 1), 256),
            24
        );

        return strtolower($recoveredAddress) === strtolower($address);
    }
}

Routes

Dans routes/web.php :

Route::middleware('web')->group(function () {
    Route::get('/admin/metamask-signature', [MetamaskAuthController::class, 'signature'])
        ->name('metamask.signature');
    Route::post('/admin/metamask-login', [MetamaskAuthController::class, 'login'])
        ->name('metamask.login');
});

Composant Blade avec ethers.js

Créez resources/views/filament/hooks/metamask-login-button.blade.php :

@if(config('metamask.enabled', true))
<div class="mt-4">
    <button type="button" id="metamask-login-btn" class="fi-btn w-full ...">
        <span id="metamask-btn-text">Connect with Crypto Wallet</span>
    </button>
    <p id="metamask-error" class="mt-2 text-sm text-danger-600 hidden"></p>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.umd.min.js"></script>
<script>
document.getElementById('metamask-login-btn').addEventListener('click', async function() {
    if (typeof window.ethereum === 'undefined') {
        showError('No wallet detected. Please install MetaMask.');
        return;
    }

    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const address = await signer.getAddress();

    // Get message to sign
    const res = await fetch('/admin/metamask-signature');
    const { message } = await res.json();

    // Sign the message
    const signature = await signer.signMessage(message);

    // Verify and login
    const loginRes = await fetch('/admin/metamask-login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        },
        body: JSON.stringify({ address, signature })
    });

    const result = await loginRes.json();
    if (result.success) {
        window.location.href = result.redirect;
    }
});
</script>
@endif

Intégration dans Filament

Dans AdminPanelProvider.php, ajoutez le render hook :

->renderHook(
    PanelsRenderHook::AUTH_LOGIN_FORM_AFTER,
    fn (): string => Blade::render("@include('filament.hooks.metamask-login-button')")
)

Comment ça fonctionne

  1. Demande de signature : Le serveur génère un message unique avec un nonce
  2. Signature côté client : L'utilisateur signe le message avec son wallet (MetaMask popup)
  3. Vérification serveur : Le serveur récupère l'adresse publique depuis la signature via cryptographie elliptique (secp256k1)
  4. Authentification : Si l'adresse correspond, l'utilisateur est connecté

Cette méthode est sécurisée car :

  • Le nonce empêche les attaques par rejeu
  • La signature prouve la possession de la clé privée
  • Aucun mot de passe n'est stocké ou transmis

Conclusion

En quelques étapes, vous avez ajouté une authentification Web3 à votre application Filament :

  • Connexion sans mot de passe via signature cryptographique
  • Support de tous les wallets compatibles EIP-1193 (MetaMask, Coinbase, WalletConnect...)
  • Auto-registration optionnelle des nouveaux utilisateurs
  • Désactivation facile via configuration

L'authentification par wallet offre une expérience utilisateur moderne et sécurisée, particulièrement adaptée aux applications Web3 et DeFi.