Laravel : Login & Register User using Metamask on REST API

Its been quite long since Metamask has been really popular these day because of its ability that enables users to access the Web 3 ecosystem of decentralized applications (dapps). In this article i want to write down how do i manage to integrate Metamask in the Laravel project.
First thing first, we need to customize the users table and model in the default Laravel installation. This are the migration that we will use at the Users table.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->string('email')->unique()->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable();
$table->string('wallet_address')->nullable()
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
Basically we just going to make the default user attributes to null like name, email and password since we’re here going to use Metamask, we added some new attributes called “wallet_address” as a string to capture the wallet address of the users.
Next, for the model basically we just need to make the “wallet_address” fillable so it can be recoreded during the Register.
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasFactory, HasApiTokens;
protected $fillable = [
'name',
'email',
'password',
'wallet_address',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
}
Next for the API authentication, we’re gonna make it real unique. Combining using Bearer Token and Nonce for authentication. The idea behind it are seems a bit complicated but i think you know what the idea is. To make it easier for you, here are some cheatsheet .
- Nonce : Is the method for validating from Laravel to Metamask and reverse after confirm the signature.
- Bearer Token : Is the token after user succcessfully authenticate to our application using Metamask.
So now, lets create a new controller and in this part lets called it AuthController.php inside the Api directory on the Controllers.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class AuthController extends Controller
{
public function register(Request $request)
{
// The register method are here.
}
public function login(Request $request)
{
// The login method are here
}
}
After that, we need to install some package to make and verify the signature from the Metamask. This package are to generate SHA-3 or Keccak in our project.
composer require kornrunner/keccak
After that, we also need to install Javascript ECC library in our project. There are package for doing this, so we don’t need to bother for creating from the first.
composer require simplito/elliptic-php
I believe since this post are fresh, you guys still can install that package in Laravel 9 at the moment.
Lets spice things up by creating a new model to store the Nonce once its been called on on the first time the user hit Login/Register. We’re going to make a new model called Nonce.php to do that, we’re going to use artisan command in our root project directory.
php artisan make:model Nonce -m
Now, we’re going to edit that migration file since we’re using -m argument while we’re creating the model before.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('nonces', function (Blueprint $table) {
$table->id();
$table->longText('nonce');
$table->longText('content');
$table->enum('type', ['register', 'login'])->default('login');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('nonces');
}
};
And for the model, we just put the fillable variable in there.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Nonce extends Model
{
use HasFactory;
protected $fillable = [
'nonce', 'content', 'type'
];
}
Now, we have all we need to do the Login or Register thing inside our application. Lets edit the AuthController.php that we’ve created earlier.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Nonce;
use Illuminate\Support\Facades\Hash;class AuthController extends Controller
{
public function nonce(Request $request, $type = 'register')
{
if ($type === 'register') {
$slug = $request->wallet_address.'-register-'.str_slug(env('APP_KEY'));
$tp = 'register';
} else {
$slug = $request->wallet_address.'-login-'.str_slug(env('APP_KEY'));
$tp = 'login';
}
$make = Hash::make($slug);
$nonce = Nonce::create([
'nonce' => $make,
'content' => $slug,
'type' => $tp
]);
if ($nonce) {
return response()->json([
'nonce' => $nonce->nonce,
])
}
} public function register(Request $request)
{
// The register method are here.
}
public function login(Request $request)
{
// The login method are here
}
}
We’re gonna make the nonce are a bit unique by combining the APP_ENV variable in the .env file, the type of authentication and the wallet address of the user. We also Hash it using Laravel facades so the user will not going to able guessing what the nonce is about. And we stored that in our Nonce model that we have created before.
After that, lets edit that file again and put the verifySignature function in order for our Login and Register function able to verify the Nonce we’ve create.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Nonce;
use Illuminate\Support\Facades\Hash;
use kornrunner\Keccak;class AuthController extends Controller
{
public function nonce(Request $request, $type = 'register')
{
if ($type === 'register') {
$slug = $request->wallet_address.'-register-'.str_slug(env('APP_KEY'));
$tp = 'register';
} else {
$slug = $request->wallet_address.'-login-'.str_slug(env('APP_KEY'));
$tp = 'login';
}
$make = Hash::make($slug);
$nonce = Nonce::create([
'nonce' => $make,
'content' => $slug,
'type' => $tp
]);
if ($nonce) {
return response()->json([
'nonce' => $nonce->nonce,
])
}
} public function verifySignature($signature, $address, $type = 'register')
{
$content = $address.'-'.$type.'-'.str_slug(env('APP_KEY'));
$nonce = Nonce::where('content', $content)->where('type', $type)->latest()->first();
if ($nonce) {
$hash = Keccak::hash(sprintf("\x19Ethereum Signed Message:\n%s%s", strlen($nonce->nonce), $nonce->nonce), 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;
}
$pubkey = (new EC('secp256k1'))->recoverPubKey($hash, $sign, $recid);
$derived_address = '0x' . substr(Keccak::hash(substr(hex2bin($pubkey->encode('hex')), 1), 256), 24);
$nonce->delete();
return (Str::lower($address) === $derived_address);
}
} public function register(Request $request)
{
// The register method are here.
}
public function login(Request $request)
{
// The login method are here
}
}
In the verifySignature function, we’re gonna search the nonce by passing the address, the type of authentication and the signature using the package that we’ve installed before (Keccak and Javascript ECC ). The idea behind it, we’re going to match the data that we have on the Nonce was it exists in our end or not. If its exists and the value hashed of the Signature was the same, then we’re going to send TRUE. if its not match, then we send it FALSE.
Next, we’re going to edit our Login and Register function can able to verify it before it registered or logged in the user.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Nonce;
use Illuminate\Support\Facades\Hash;
use kornrunner\Keccak;
use Illuminate\Support\Str;
use Elliptic\EC;
use App\Models\User;class AuthController extends Controller
{
public function nonce(Request $request, $type = 'register')
{
if ($type === 'register') {
$slug = $request->wallet_address.'-register-'.str_slug(env('APP_KEY'));
$tp = 'register';
} else {
$slug = $request->wallet_address.'-login-'.str_slug(env('APP_KEY'));
$tp = 'login';
}
$make = Hash::make($slug);
$nonce = Nonce::create([
'nonce' => $make,
'content' => $slug,
'type' => $tp
]);
if ($nonce) {
return response()->json([
'nonce' => $nonce->nonce,
])
}
} public function verifySignature($signature, $address, $type = 'register')
{
$content = $address.'-'.$type.'-'.str_slug(env('APP_KEY'));
$nonce = Nonce::where('content', $content)->where('type', $type)->latest()->first();
if ($nonce) {
$hash = Keccak::hash(sprintf("\x19Ethereum Signed Message:\n%s%s", strlen($nonce->nonce), $nonce->nonce), 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;
}
$pubkey = (new EC('secp256k1'))->recoverPubKey($hash, $sign, $recid);
$derived_address = '0x' . substr(Keccak::hash(substr(hex2bin($pubkey->encode('hex')), 1), 256), 24);
$nonce->delete();
return (Str::lower($address) === $derived_address);
}
} public function register(RegisterRequest $request)
{
$verify = $this->verifySignature($request->signature, $request->wallet_address, 'register');
if ($verify) {
$user = User::create([
'wallet_address' => $request->wallet_address
]);
$token = $user->createToken('api')->plainTextToken;
return $this->success_response([
'message' => 'Registration successful',
'token' => $token,
'address' => $request->wallet_address,
]);
} else {
Nonce::where('content', $request->address.'-register')->where('type', 'register')->delete();
return response()->json([
'message' => 'Invalid address or signature'
], 400);
}
}
public function login(Request $request)
{
$verify = $this->verifySignature($request->signature, $request->wallet_address, 'login');
if ($verify) {
$user = User::where('wallet_address', $request->wallet_address)->first();
if ($user) {
$token = $user->createToken('api')->plainTextToken;
return response()->json([
'message' => 'Login successful',
'token' => $token,
'address' => $request->wallet_address,
]);
}
} else {
Nonce::where('content', $request->address.'-register')->where('type', 'register')->delete();
return response()->json([
'message' => 'There is no user with that wallet address'
], 400);
}
}
}
There you go, we half to the end of this article. Now lets add our controller in the api.php route.
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::prefix('auth')->group(function () {
Route::post('/register', [\App\Http\Controllers\Api\AuthController::class, 'register']);
Route::post('/login', [\App\Http\Controllers\Api\AuthController::class, 'login']);
Route::get('/nonce/{type?}', [\App\Http\Controllers\Api\AuthController::class, 'nonce']);
});
And for the sake of testing purposes, we’re going to create a new view. Lets called it auth3.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MetaMask Login</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12 text-center mt-10">
<h1>Login with Metamask</h1>
<button class="btn btn-primary mt-5" onclick="web3Register();">Register with MetaMask</button>
<button class="btn btn-primary mt-5" onclick="web3Login();">Login with MetaMask</button>
</div>
</div>
</div>
<script>
async function authHandler(type)
{
if (!window.ethereum) {
alert('MetaMask not detected. Please install MetaMask first.');
return;
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
try {
await provider.send("eth_requestAccounts", []);
const address = await provider.getSigner().getAddress();
let response = await fetch('{{ url('/api/auth/nonce/') }}/'+type+'?wallet_address=' + address);
const json = await response.json();
const message = json.data.nonce;
const signature = await provider.getSigner().signMessage(message);
console.log(signature);
response = await fetch('{{ url('/api/auth/') }}/' + type, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'wallet_address': address,
'signature': signature,
})
});
if (response.status == 200) {
const data = await response.json();
console.log(data);
}
} catch (err) {
console.log('Error : ' + err.code);
}
}
async function web3Login() {
await authHandler('login');
}
async function web3Register() {
await authHandler('register');
}
</script>
</body>
</html>
All of those endpoint are get from the route API that we’ve define earlier. So i think you know the rough idea how to do the Authentication in Laravel using Metamask. I think you know how to add the things for that view above able to see in your end tho.
Note : You may need to delete the Nonce once a daily to prevent junk data inside your database.