How to Build a One-Click Sign in Using MetaMask with PHP's Laravel

December 29, 2021

Overview

Social logins: we have all seen them, we have all used them. "Login with Facebook". "Login with Github".

If you have been around the Web3 community you may have come across a more recent player in the game: "Login with MetaMask". For instance, this is how you may sign up for OpenSea the biggest NFT marketplace for Ethereum and Polygon.

MetaMask is a crypto wallet for the Ethereum blockchain that also allows you to interact with dApps (decentralized apps). Specifically it allows dApps to verify that you are the owner of a certain Ethereum address which in turn will serve as your online identity.

This tutorial will show you how to implement a one-click MetaMask login using web3.js and a PHP backend. While we will be using Laravel and Vue in this tutorial, the principles of:

  • Signing a message in Javascript using web3.js, and
  • Verifying the signature in a PHP backend

…is absolutely transferable to any JS and PHP framework of your choice.

Prerequisites:
In order to follow along the steps in this tutorial you will need:

  • MetaMask installed as a browser extension with at least 1 account (no ETH needed!). If you do not already have MetaMask installed you may take a look at their website. — If you want to configure your QuickNode RPC to MetaMask, we have you covered there too.
  • A local development environment that allows you to run a fresh Laravel installation. Take a look at the excellent Laravel documentation to get started. In this tutorial I will be using the Installation Via Composer installation method, but feel free to use Laravel Sail (Docker) should you prefer. When using Laravel Sail you will need prefix all commands with ./vendor/bin/sail (ie ./vendor/bin/sail composer install instead of composer install); please refer to the official Laravel Sail documentation for more information on how to execute commands.
  • General knowledge and familiarity of running terminal commands (NPM/Composer installs)

We have the source code here as well for you to look at.

Setting Up the Project

Alright, let us get going!

First off, we will create a new Laravel project. As previously mentioned I will be using the composer create-project method. This works great if you already have PHP and Composer installed on your local machine. Checkout the official Laravel documentation for more available installation options.

Run the following command in your terminal to generate the project:

setting up the project

Copy
composer create-project laravel/laravel metamask-demo-app

Install Jetstream

To get a head start on the frontend, we will pull in the official Laravel package "Jetstream" which gives us a nice pre-baked dashboard that includes a login form!

Inside your newly created folder metamask-demo-app you may run:

setting up the project

Copy
composer require laravel/jetstream

Once installed, we will tell Jetstream to scaffold our application with the Inertia (Vue3) preset. This will include a stack of Vue 3 and Tailwind CSS.

setting up the project

Copy
php artisan jetstream:install inertia

Finally, let us install the newly added NPM dependencies and compile the assets:

setting up the project

Copy
npm install && npm run dev

Up and running

In order to get the new Laravel app up and running, we will need to add a database connection. Normally this would be a MySQL or PostgreSQL database, but for our demo purpose we can use a SQLite database by creating an empty file called database.sqlite in our database directory.

To do so, run the following command from the root of your project:

setting up the project

Copy
touch database/database.sqlite

We will also need to update our .env file to use the sqlite connection and comment out or remove the unused variables (important):

setting up the project

Copy
DB_CONNECTION=sqlite
#DB_HOST=
#DB_PORT=
#DB_DATABASE=
#DB_USERNAME=
#DB_PASSWORD=

Finally, we can migrate our database migrations and serve our Laravel application to the browser!

setting up the project

Copy
php artisan migrate
php artisan serve

The last command should output something like:

Starting Laravel development server: http://127.0.0.1:8000

Opening that URL in your browser, you should be met with a default Laravel welcome screen.

Navigate to http://127.0.0.1:8000/login and you should see a Jetstream login form.


Congratulations 🎉 — we are now ready to start coding!

Preparing the Frontend

First things first — let us tweak the frontend a bit by adding our "Login with MetaMask button".

Open the file resources/js/Pages/Auth/Login.vue and add the following HTML after the logo template part (around line 5).

 between the root <template> tags, after the <template #logo>...</template> part

preparing the frontend

Copy
// resources/js/Pages/Auth/Login.vue

<div class="text-center pt-4 pb-8 border-b border-gray-200">
    <jet-button @click="loginWeb3">
        Login with MetaMask
    </jet-button>
</div>
<div class="py-6 text-sm text-gray-500 text-center">
    or login with your credentials…
</div>

As you may have noticed the button already has a click handler specified, for now add a new empty method called loginWeb3 after the existing submit method:

at the bottom between the <script> tags, add async loginWeb3 method

preparing the frontend

Copy
// resources/js/Pages/Auth/Login.vue
methods: {
    submit() {
        // ...
    },
    async loginWeb3() {
        // Our Meta Mask integration goes here
    }
}

If you compile it by running npm run dev in your terminal the result should look something like this:

Creating a Signature Request

Right now the button does not do anything. Let us fix that!

We will start by installing the NPM package web3 which we will need:

creating a signature request

Copy
npm install web3

Now we are ready to fill in a bit of logic.

First, add these imports at the top of the <script> section:

At the top of the <script> section

creating a signature request

Copy
// resources/js/Pages/Auth/Login.vue
import Web3 from 'web3/dist/web3.min.js'
import { useForm } from '@inertiajs/inertia-vue3'

Next update the empty loginWeb3 function so it looks like the following:

Replace the previously added async loginWeb3 method

creating a signature request

Copy
// resources/js/Pages/Auth/Login.vue
async loginWeb3() {
    if (! window.ethereum) {
        alert('MetaMask not detected. Please try again from a MetaMask enabled browser.')
    }

    const web3 = new Web3(window.ethereum);

    const message = [
        "I have read and accept the terms and conditions (https://example.org/tos) of this app.",
        "Please sign me in!"
    ].join("\n")

    const address = (await web3.eth.requestAccounts())[0]
    const signature = await web3.eth.personal.sign(message, address)

    return useForm({ message, address, signature }).post('/login-web3')
}

Here is what we're doing in the above code:

  1. Ensure MetaMask is present in the current browser by checking the window.ethereum property is present. Otherwise alert the user. (lines 3-5)
  2. Prepare the message we want the user to sign. We might as well make this a bit useful, such as have the user accepting the Terms & Conditions for using the app. However it is absolutely up to you what you'd like to have the user sign. (lines 9-12)
  3. Request the user's accounts. This is the first popup our users will see, and they have to select which address to sign in with. (line 14)
  4. Make the user sign our message - in this case accept our T&C. (line 15)
  5. Finally, send the message, address & signature to the backend. (line 17)

With a little bit of luck, after compilation you should be able to see the following flow in you browser:

Quick aside:

When it comes to the signature message, we have found that there is a little quirk to beware of. At the time of writing, if you enter any message of exactly 32 characters, the message will be presented in HEX in MetaMask. As such the message "Hello world but a lil bit longer" becomes:


In reality it is not a huge problem, but in my case it ended up costing me a bit of extra hours and hair-pulling figuring why my message was not showing in plain text. Now you know!

Verifying the Signature

We have our signature ready, and we are sending it to the backend. Now we need to:

  • Verify the signature is authentic
  • Check if the address matches an existing user in our database or otherwise create a new user
  • Log the user in and redirect to the app dashboard

Let us get started.

Install dependencies

Before we get to the actual coding part, we will need to install a few dependencies.

verifying the signature

Copy
composer require kornrunner/keccak --ignore-platform-reqs
composer require simplito/elliptic-php --ignore-platform-reqs

We are adding the --ignore-platform-reqs flag as composer would otherwise throw an error stating that the required ext-gmp extension is missing.

For this demo to work, the GMP extension is not required, and we can safely ignore it.

However, should you for any reason wish to install GMP anyway, you may find this gist helpful.

Prepare the users table

Open the database/migrations/2014_10_12_000000_create_users_table.php file and replace the up method with the following code:

verifying the signature

Copy
# database/migrations/2014_10_12_000000_create_users_table.
public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('eth_address')->nullable();
        $table->string('name')->nullable();
        $table->string('email')->unique()->nullable();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password')->nullable();
        $table->rememberToken();
        $table->foreignId('current_team_id')->nullable();
        $table->string('profile_photo_path', 2048)->nullable();
        $table->timestamps();
    });
}

The changes we have made here are:

  • Added a eth_address field which will hold the user's ethereum address
  • Made name , email , email_verified_at and password nullable

Remember to refresh the database afterwards using:

verifying the signature

Copy
php artisan migrate:fre

The login logic

Open the routes/web.php file and add the following line:

Add in the bottom of the file - after the Route::middleware(['auth:sanctum', 'verified'])->get(...) part

verifying the signature

Copy
# routes/web.
Route::post('login-web3', \App\Actions\LoginUsingWeb3::class);

Note: We are using the routes/web.php file and not api.php because we will need access to the sesssion / cookie state in order to log in the user once authenticated.

Next let us go on and create the app/Actions/LoginUsingWeb3.php file which will hold the actual login logic. Copy / paste the following code into it: 

verifying the signature

Copy
# app/Actions/LoginUsingWeb3.
<?

namespace App\Actions;

use App\Models\User;
use Illuminate\Http\Request;
use Elliptic\EC;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use kornrunner\Keccak;

class LoginUsingWeb3
{
    public function __invoke(Request $request)
    {
        if (! $this->authenticate($request)) {
            throw ValidationException::withMessages([
                'signature' => 'Invalid signature.'
            ]);
        }

        Auth::login(User::firstOrCreate([
            'eth_address' => $request->address
        ]));

        return Redirect::route('dashboard');
    }

    protected function authenticate(Request $request): bool
    {
        return $this->verifySignature(
            $request->message,
            $request->signature,
            $request->address,
        );
    }

    protected function verifySignature($message, $signature, $address): bool
    {
        $messageLength = strlen($message);
        $hash = Keccak::hash("\x19Ethereum Signed Message:\n{$messageLength}{$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;
        }

        $publicKey = (new EC('secp256k1'))->recoverPubKey($hash, $sign, $recId);

        return $this->pubKeyToAddress($publicKey) === Str::lower($address);
    }

    protected function pubKeyToAddress($publicKey): string
    {
        return "0x" . substr(Keccak::hash(substr(hex2bin($publicKey->encode("hex")), 1), 256), 24);
    }
}

There is a few things going on here, so let us break it down step-by-step.

The _invoke method

This is the entry-point for the route that we registered, and will receive the POST request sent from our frontend.

The actual logic is quite straight forward as we:

  • Validate the signature sent from the frontend
  • Find or create new user based on the user's address. When creating a new user, we will make sure to store the address in our dedicatedeth_address field.
  • Log the user in, and redirect to the Jetstream dashboard

The verifySignature method

This is a standardized way of cryptographically validating that an Ethereum signature matches the corresponding message and address that signed it.

It is functionally equivalent to web3.eth.personal.ecRecover which returns the signing address of a message + signature.

We will not fully go into the nitty gritty details of this code, (I am not a mathematician, and you do not need to be one either) but the high level explanation is that we are:

  • Reconstructing a hash of the message
  • Extracting the public key from the signature and hashed message
  • Extracting the address from the public key
  • Checking that the address sent from the frontend actually matches the address that signed the message

And voilá! We now have a functioning login!

If you go back to your browser and go through the login flow, you should now be redirected to the dashboard.


Bonus tip: disable Jetstream registration

If you actually intend to use Jetstream for your app and want to make sure your users always register with MetaMask the first time they login, you may wish to disable the default /register route that Jetstream ships with.

You can open config/fortify.php and comment out the "registration feature" line:

verifying the signature

Copy
# config/fortify.
'features' => [
    // Features::registration(),
    Features::resetPasswords(),
    [...]
],

Your users will now only be able to register using the MetaMask login.

Conclusion

That is it folks!

In this tutorial we have built a simple login flow using Vue, Web3, MetaMask and Laravel.

I hope it has conveyed the basic workflow of how to integrate with a wallet, and how to use signatures to securely authenticate a user's identity server-side as well.

Given the decentralized nature of Web3, you will probably want to support more wallets that just MetaMask. For instance adding WalletConnect would allow using mobile-wallets to sign in by scanning a QR code.

For further exploration into supporting more wallets, you may wish check out these resources:


Subscribe to our newsletter for more articles and guides on Ethereum. If you have any feedback, feel free to reach out to us via Twitter. You can always chat with us on our Discord community server, featuring some of the coolest developers you will ever meet :)

Related articles 54

How to Send a Transaction On Solana Using JavaScript
Apr 13, 2022

Hello reader! Today is an exhilarating day because we are going on an expedition to the Solana Blockchain. Solana is an up-and-coming blockchain seeking to improve upon the current ecosystem's solutions to the complex problem of providing a secure, scalable, decentralized...

Continue reading
How to do a non-custodial transaction with QuickNode
Apr 12, 2022

Private keys are one of the most sensitive pieces of data when it comes to cryptography and the blockchain. However, there has always been debate/confusion about choosing between custodial wallets (where the wallet provider has custody of the user’s private key) and...

Continue reading
How to connect to Ethereum using .NET (Nethereum)
Apr 12, 2022

Dotnet or .NET is very popular for the development of desktop applications, most Windows desktop applications are built using .NET, and it also contributes largely to web application’s tech stack. In this guide, let’s see how we can connect to Ethereum using .NET and

Continue reading
How to Set Up a Near Project from Scratch
Jan 27, 2022

In this tutorial we will look at how we can setup a basic NEAR project from scratch, installing and configuring dependencies and customizing the project to work well with AssemblyScript.We will first start by initializing our project with a package.json file using...

Continue reading
Como crear y lanzar un ERC-721 (NFT)
Dec 29, 2021

Coleccionables digitales que son compatibles con ERC-721 se han vuelto muy populares desde el lanzamiento de Cryptokitties y han ganado adopción masiva en los últimos meses. Esta guía cubrirá la parte de creación y lanzamiento...

Continue reading
How to connect to Ethereum network using Java / Web3j
Apr 12, 2022

We can say that Java is one of the most versatile languages out there, and it continues to be relevant in today's time. Java is so popular because of its massive user base and use cases. In this guide/tutorial, we'll learn how to connect to the Ethereum Blockchain network...

Continue reading
How to integrate IPFS with Ethereum
Apr 12, 2022

It can be costly to store massive files on a blockchain mainnet, and this is where decentralized file storing systems like IPFS can come in handy. Sometimes, NFTs use IPFS as well. In this guide, we’ll cover how we can integrate IPFS with...

Continue reading
How to Connect to the Ethereum Network using Ruby
Jun 13, 2022

The Ruby programming language has a huge fanbase. Ruby was developed by its creator with an intention to invent a language developers can enjoy learning and using. Ruby has been largely accepted by developers all around the world since its launch, in fact, the biggest...

Continue reading
How to connect to Ethereum network with ethers.js
Apr 12, 2022

When someone thinks of developing a dApp the first tool that comes to their mind is web3.js which is pretty common because of its popularity in the community and wide use cases, dApp development has been consistently growing and there are a lot of developers who want to...

Continue reading
How to Mint an NFT on Solana
Apr 12, 2022

Updated at: April 10, 2022Welcome to another QuickNode guide on Solana - the up-and-coming blockchain that seeks to solve the scalability issues of Ethereum. We will be walking through step-by-step how to create an NFT on Solana. NFT, short for Non Fungible Token,...

Continue reading
The Web3 Developer Stack
Apr 12, 2022

A developer stack is a bag of technologies a developer possesses. For example, MEAN (MongoDB, Express.js, AngularJS/Angular, and Node.js) and MERN (MongoDB, Express.js, React, and Node.js) are common web developer stacks. Similarly, today we will learn more about the web3...

Continue reading
How to deploy a smart contract with Brownie
Apr 12, 2022

Python is one of the most versatile programming languages; from researchers running their test models to developers using it in heavy production environments, it has use cases in every possible technical field. In today's guide, we will learn about Brownie, a Python-based...

Continue reading
How to Get All Tokens Held by a Wallet in Solana
Jun 24, 2022

Hello readers! To kick off Solana Summer and the current whitelist meta, we thought it would be helpful to dig into all of the token accounts you and your users have using the getParsedProgramAccounts method. This tool is convenient for querying different...

Continue reading
Introduction to Scaffold-ETH 🏗
Dec 29, 2021

Developing applications involves juggling several moving pieces like front-ends, back-ends, and databases. But developing a decentralized application on a blockchain adds a few more elements like smart contracts and nodes that allow you to connect to the...

Continue reading
Como crear un NFT en SOLANA
Dec 29, 2021

¡Hola querido lector! Bienvenidos a una nueva guía de Solana.Solana es una blockchain que promete mucho a la hora de intentar resolver los problemas de escalabilidad que podemos apreciar en otras blockchains, como Ethereum por...

Continue reading
How to Get Transaction Logs on Solana
Jun 28, 2022

Ever need to pull all the transactions associated with a Wallet? Want to see all of the mint transactions associated with a Candy Machine? Or maybe see transaction history of an NFT? Solana's getSignaturesForAddress method is a versatile tool that makes...

Continue reading
How to Send an EIP-1559 Transaction
Apr 12, 2022

While Ethereum has been trying to scale, it has encountered some gas price issues. Many layer 2 solutions and sidechains sprang into existence to solve this problem, but Ethereum is the main chain, and at some point, it has to be improved. EIP-1559 was introduced to...

Continue reading
How to Create an Address in Solana using JavaScript
Apr 12, 2022

Hello reader! Welcome to QuickNode's first Solana guide. Solana is an up-and-coming blockchain that seeks to solve the scalability issues that Ethereum has been handling. You will walk through step-by-step how to create a Solana address using the @solana/web3.js...

Continue reading
How to create your own DAO with Aragon
Apr 12, 2022

Blockchain provides us with the power of decentralization. Decentralization means the transfer of power to users/members rather than having a single centralized authority governing everything; it enables various use cases in finance, governance, voting, fundraising, etc....

Continue reading
How to Connect to Terra with JavaScript using Terra.js
Apr 12, 2022

Stablecoins have been bridging the gap between traditional currencies and blockchains. Stablecoins offer stable price tokens pegged by a reserve asset which is often a fiat current like USD, EUR, GBP. The Terra protocol provides a framework to work with stablecoins. This...

Continue reading
How to connect to Ethereum network using Go
Apr 12, 2022

Go helps you make faster scalable backends and this guide will show you how to connect your backend to Ethereum (and make it even faster, more reliable, and globally accessible, all thanks to QuickNode’s global infrastructure). What is...

Continue reading
How To Fork Ethereum Mainnet with Hardhat
Apr 12, 2022

Forking the chain at an older block of the blockchain is helpful if you want to simulate the blockchain’s state at that block; Hardhat has this functionality built in. In this guide, let’s go through the process of forking the Ethereum Mainnet at an older...

Continue reading
How to connect to Ethereum using PHP
Apr 12, 2022

PHP is a very popular choice among developers and has a vast community due to its long presence in web development. In this guide, we’ll cover how to connect to Ethereum with PHP using the web3.php...

Continue reading
How to use Subspace with QuickNode
Apr 12, 2022

In this guide, we'll understand a bit about reactive development and how to use Subspace with QuickNode.JavaScript is the programming language behind most of the internet apps and websites. JavaScript today has become one of the most used programming languages,...

Continue reading
How to Connect Your Dapp With MetaMask Using Ethers.js
Dec 29, 2021

In our dApp, we will have a simple react user interface that has a material button asking the user to connect to MetaMask. And if they do not have an account, they can create one or log in to their account. They will then view their wallet balance and address displayed on...

Continue reading
How to generate a new Ethereum address in Go
Dec 29, 2021

Golang is very popular among backend developers for building infrastructures and microservices. Go is a procedural programming language. Developed in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson at Google, then launched in 2009 as...

Continue reading
How to generate a new Ethereum address in Python
Dec 29, 2021

Python is one of the most versatile programming languages out there with an abundance of use cases; We can build many applications with Python from client-side to back end. In this guide, we will cover creating an Ethereum address in Python using the

Continue reading
How to Lazy Mint an NFT on Rarible with Rarepress
Apr 12, 2022

NFTs are great for creators to monetize their artwork and for people to get ownership of an item. But since gas prices are usually high given the highly in-demand space on Ethereum, minting an NFT or an NFT collection can become costly for a creator. Lazy minting solves...

Continue reading