LoginSignup
2
0

importmap-railsを使ってMetaMask認証をしてみる。

Last updated at Posted at 2023-12-16

あいさつ

こんにちは、ryosk7です。

この記事は、Ruby on Rails Advent Calendar 2023 16日目の記事です。

はじめに

技術書典15で出展した「Rubyでつくる NFTマーケットプレイス」を参考に、MetaMask認証機能を作っていきます。

元々、NFTマーケットプレイスを実装する際に、こちらの記事を参考にしていました。

ただし、こちらの記事ではJavaScript(以下JS)をゴリゴリ書いており、筆者はそこまでJSを書きたくなかったので、なにかいい手はないかと考えたところ、一つの解決策に辿り着きました。
それが、『importmap-rails』 です。

importmap-rails

Rails 7.0から標準搭載となった機能です。
こちらの記事がとてもわかりやすいです。

要約すると、

  • JS の Bundling が不要
    • ブラウザが必要なファイルを取得してくれる
  • HTTP/2で並列処理
    • 複数ファイルを並列して処理してくれる
  • Bundlingが不要になったため、変更のないファイルはキャッシュを利用可能
    • 新たにダウンロードするファイルも最小限に済む

フロントエンドの開発体験がかなりよくなりました。

NFTをはじめとした、Web3界隈の技術は、RubyよりもJSの方が盛んな印象です。
Railsを使って開発する時も、importmap-railsを使えば妥協することなく、JSの技術を利用することができます。

実践編

まずはRailsアプリを用意しましょう。importmap-railsはすでに使える状態でインストールされます。

Railsをセットアップ

rails new myapp
cd myapp

Userモデルを作成

続いて、Userモデルを作成します。

rails g model user eth_address:string eth_nonce:string username:string
rails db:migrate

それぞれ説明します。

  • eth_address: ユーザーのEthereumアドレスです。アプリケーションでの認証に使用されます。
  • eth_nonce: 署名のなりすましを防ぐために使用されます。
  • username: ユーザー名です。任意の名前が入ります。

作成されたapp/models/user.rbに以下のコードを追加してください。

app/models/user.rb
class User < ApplicationRecord
  validates :eth_address, presence: true, uniqueness: true
  validates :eth_nonce, presence: true, uniqueness: true
  validates :username, presence: true, uniqueness: true
end

続いて、users_controller.rbを作成しましょう。必要なのはcreateアクションのみです。

rails g controller users create

route.rbを以下のように書き換えます。

config/routes.rb
Rails.application.routes.draw do

   resources :users, only: [:create]

   # 省略

importmap-railsを使って、Web3.jsをインストール

MetaMask 認証のような、ウォレットの認証は Web3.js を使うのが便利です。
以下のコマンドでインストールします。

bin/importmap pin web3

モジュールはconfig/importmap.rbに追加されたと思います。確認しましょう。

config/importmap.rb
# <--- 省略 --->
pin_all_from "app/javascript/controllers", under: "controllers"
pin "web3", to: "https://..."
pin "@adraffy/ens-normalize", to: "https://..."
pin "@ethereumjs/rlp", to: "https://..."
pin "@noble/curves/secp256k1", to: "https://..."
pin "@noble/hashes/_assert", to: "https://..."
pin "@noble/hashes/crypto", to: "https://..."
pin "@noble/hashes/hmac", to: "https://..."
pin "@noble/hashes/pbkdf2", to: "https://..."
pin "@noble/hashes/scrypt", to: "https://..."
pin "@noble/hashes/sha256", to: "https://..."
# <--- 省略 --->
pin "web3-eth-contract", to: "https://..."
pin "web3-eth-ens", to: "https://..."
pin "web3-eth-iban", to: "https://..."
pin "web3-eth-personal", to: "https://..."
pin "web3-net", to: "https://..."
pin "web3-providers-http", to: "https://..."
pin "web3-providers-ws", to: "https://..."
pin "web3-rpc-methods", to: "https://..."
pin "web3-types", to: "https://..."
pin "web3-utils", to: "https://..."
pin "web3-validator", to: "https://..."
pin "zod", to: "https://..."

ファイルの中に、pin_all_from というのがあると思います。これは、app/javascript/controllers 配下に配置した場合に import で読み込めるようにできます。

config/importmap.rb
# <--- 省略 --->

pin_all_from "app/javascript/controllers", under: "controllers"

# <--- 省略 --->

Web3.jsと eth gemで認証機能を作る。

ウォレットに接続するための機能を実装していきます。先ほどインストールしてWeb3.jsを利用します。
そのためにはま ず、app/javascript/controllers 配下に新たにコントローラーを追加します。 metamask コントローラーという名前のコントローラーをつくり、ここで接続処理を記述しましょう。以下のコマンドを打ってください。

bin/rails g stimulus metamask

app/javascript/ 配下に metamask.js が追加されました。

コマンドで利用した『Stimulus』は、 Rails上でJSを扱うためのレールのような存在です。
ファイルを開き、Web3.js を import しましょう。

app/javascript/controllers/metamask_controller.js
import { Controller } from "@hotwired/stimulus"
import Web3 from "web3" // 追加

// Connects to data-controller="metamask"
export default class extends Controller {
    connect() {}
}

application.html.erbを編集して、ウォレット接続ボタンを追加します。

application.html.erb
<div data-controller="metamask">
    <a data-action="click->metamask#signin" href="#">
        ウォレット接続
    </a>
</div>

先ほど作成した、metamask.jssignin()を追加します。

app/javascript/controllers/metamask_controller.js
async signin() {
    const provider = window.ethereum || window.web3?.provider || null

    if (!provider) {
      console.error('!provider')
      return
    }

    const web3 = new Web3(provider)
    const account = await web3.eth.requestAccounts();
    const nonce = account[0]
    const eth_address = account[1]
    const customTitle = "Ethereum on Rails";
    const requestTime = new Date().getTime();
    const eth_message = customTitle + "," + requestTime + "," + nonce;
    const password = ''
    const eth_signature = await web3.eth.personal.sign(
                             eth_message, eth_address, password
                          )

    const csrfToken = document
      .querySelector("meta[name='csrf-token']")
      .getAttribute("content");

    fetch("http://localhost:3000/users/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": csrfToken,
      },
      body: JSON.stringify({eth_message, eth_address, eth_signature}),
    }).then(response => response.json())
      .then(data => {
        if (data.connected) {
          this.element.style.display = 'none';
        }
      });
  }

Web3.js のインスタンスを作成し、MetaMask ウォレットに対してリクエストを投げます。そして、署名を作成してください。これは、サーバー側で認証をさせるために必要になります。
fetch 関数を使い、サーバーに対して、アクセスしています。JSON 形式で渡したいため、JSON.stringify で加工しています。

eth gemをインストール

サーバー側で、イーサリアムを扱うために、eth gem という gem を使用します。Gemfile に追加し、bundle install してください。

Gemfile
gem "eth"
bundle install

最後にusers_controller.rbに以下を追加します。これは、元々参考にしていた、こちらのセクションを参考にしています。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    user = User.find_by(eth_address: params[:eth_address])
    if user.present?
      if params[:eth_signature]
        message = params[:eth_message]
        signature = params[:eth_signature]
        # フォームからではなく、データベースからのユーザー アドレスとノンスを使用する
        user_address = user.eth_address
        user_nonce = user.eth_nonce
        custom_title, request_time, signed_nonce = message.split(",")
        request_time = Time.at(request_time.to_f / 1000.0)
        expiry_time = request_time + 300
        if request_time && Time.current < expiry_time
          # 署名されたノンスが記録にあるものであることを強制する
          if signed_nonce == user_nonce
            # 署名からアドレスを復元する
            signature_pubkey = Eth::Signature.personal_recover message, signature
            signature_address = Eth::Util.public_key_to_address signature_pubkey
            if user_address.downcase == signature_address.to_s.downcase
              session[:user_id] = user.id
              user.eth_nonce = SecureRandom.uuid
              user.save
              redirect_to root_path, notice: "Logged in successfully!"
            end
          end
        end
      end
    end
  end
end

これで一通りのMetaMaskログインが完成です。

おわりに

いかがでしたでしょうか。
元々の記事と比較すると、少ないJSの記述で同じ認証基盤を構築できたことがわかります。
importmap-railsにより、気軽にnpmライブラリを利用できるので、今回の事例で扱ったWeb3.jsに限らず、みなさんもぜひ様々なライブラリで試してみてください!

最後に紹介となりますが、株式会社KINECAで、エンタメマッチングアプリ『pato』の開発を行っています。

本サービスもRails7を使っており、常にモダンな環境にするためアップデートメンテナンスを絶やさず行っています。

気になる方、一緒に働きたい方、おしゃべりしたい方はぜひ気軽にXにDMをお願いします🙌

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0