Authentification MetaMask

C’est vraiment ton adresse?

Photo de Worldspectrum

Authentifier depuis Metamask

Introduction

Une équipe travaillant sur un projet Web3 m’a contacté pour refaire un petit site Web… CTFd. C’est une solution OpenSource, sous Flask. Cerise sur le gateau, les identifiants classiques ne sont pas autorisés. Il faut oublier le courriel, pseudonyme et mot de passe. Il faut passer par MetaMask.

Je vais donc présenter ici la partie Front-end/JS et Back-end/Python3 pour se connecter avec ce plugin.

MetaMask

Mais avant, commençons avec MetaMask! Qu’est ce que c’est? Pour mon histoire, c’est simplement un SSO qui se base sur la blockchain Ethereum.

Il est vrai que c’est un peu plus que ça mais je laisse les experts en Cryptomonnaie vous l’expliquer.

Le Code

Pour authentifier un utilisateur il faut du JavaScript, que j’ai trouvé sur le site freecodecamp. Il m’a quand même fallu un peu l’adapter pour le serveur Flask, mais rien de compliqué. Pour le Back-end, c’était un peu plus complexe, mais rien de sorcier non plus.

Front-end

Vous trouvez ici, un code assez similaire à celui de FreeCodeCamp:

    <!-- on met les lib-->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/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>


    <!-- on a un bouton pour se connecter à MetaMask-->
		<button id="connectWallet" onclick="">Connection MetaMask</button>
	
    <!-- Les petites fonctions qui vont interroger MetaMask -->
		<script type="text/javascript">
		  window.userWalletAddress = null
		  const connectWallet = document.getElementById('connectWallet')
		  const walletAddress = document.getElementById('walletAddress')
	
    //on a un bouton pour se connecter à MetaMask-->
		  function checkInstalled() {
        if (typeof window.ethereum == 'undefined') {
          connectWallet.innerText = 'MetaMask isnt installed, please install it'
          connectWallet.classList.remove()
          connectWallet.classList.add()
          return false
        }
        connectWallet.addEventListener('click', connectWalletwithMetaMask)
		  }
	
		  async function connectWalletwithMetaMask() {
        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
        .catch((e) => {
        console.error(e.message)
        return
        })
    
        if (!accounts) { return }
    
        window.userWalletAddress = accounts[0]
        walletAddress.innerText = window.userWalletAddress
    
        connectWallet.innerText = 'Sign Out'
        connectWallet.removeEventListener('click', connectWalletwithMetaMask)
        setTimeout(() => {
          connectWallet.addEventListener('click', signOutOfMetaMask)
        }, 200)
    
        const provider = new ethers.providers.Web3Provider(window.ethereum);
    
        //on va recevoir un message aléatoire du serveur pour éviter une réutilisation de la signature
        let response = await fetch('/api/message');
        const message = await response.text();
    
        await provider.send("eth_requestAccounts", []);
        //on recup l'addresse Ethereum
        const address = await provider.getSigner().getAddress();
        //on signe
        const signature = await provider.getSigner().signMessage(message);

        //la c'est du custom, des qu'on a les informations, on rempli le formulaire
        document.getElementById("message").value = message;
        document.getElementById("address").value = address;
        document.getElementById("signature").value = signature;
        document.getElementById("_submit").value = "You can submit";
        document.getElementById("_submit").style = "background: green;";

        elementToSubmit=document.getElementById("_submit");
        elementToSubmit.removeAttribute("disabled");
		  }
	
	
		  function signOutOfMetaMask() {
        window.userwalletAddress = null
        walletAddress.innerText = ''
        connectWallet.innerText = 'Connect Wallet'
        document.getElementById("message").value = '';
        document.getElementById("address").value = '';
        document.getElementById("signature").value = '';
        document.getElementById("_submit").value = "You can't submit";
        document.getElementById("_submit").style = "background: grey;";
    
        connectWallet.removeEventListener('click', signOutOfMetaMask)
        setTimeout(() => {
          connectWallet.addEventListener('click', connectWalletwithMetaMask)
        }, 200  )
		  }
		  window.addEventListener('DOMContentLoaded', () => {
			  checkInstalled()
		  })
	
		</script>

    <!-- à vous de custom le formulaire-->
		<form method="post" accept-charset="utf-8" autocomplete="off">
      <input id="pseudo" name="pseudo" value="pseudo">
      <input type="hidden" id="message" name="message" value="message">
      <input type="hidden" id="address" name="address" value="address">
      <input type="hidden" id="signature" name="signature" value="signature">
      <input type="hidden" id="recaptchaResponse" name="recaptcha-response">
      <input type="submit" value="Subscribe!">
    </form>

Back-end

Pour le code présent ici, c’était beaucoup plus commenté pour faciliter la lecture.

import requests, secrets, json
from web3 import Web3
from flask import current_app as app
from flask import Blueprint, abort, redirect, render_template, request, session, url_for


from eth_account.messages import encode_defunct

@app.route('/api/message')
def message():
    message = get_MessageUnique() # Attention ce n'est pas parce qu'on genère un message aléatoire que c'est suffisant. Il faudra contrôler par la suite qu'il n'a pas été réutilisé.
    return message

@auth.route("/login", methods=["POST", "GET"]) # c[] METAMASK ADD LINE
@check_registration_visibility
@ratelimit(method="POST", limit=10, interval=5)
def register():
    errors = get_errors()
    if current_user.authed():
        return redirect(url_for("page.home"))
    if request.method == "GET":
        errors.append(
                    "Authentification uniquement avec MetaMask"
        )
        return render_template("login.html", errors=errors)

    if request.method == "POST":
        try:
            post_pseudonyme = request.form.get('pseudonyme')
            post_address = request.form.get('address')
            post_message = request.form.get('message')
            post_signature = request.form.get('signature')
        except:
            errors.append(
                        "Un élément est manquant"
            )      
            return render_template("login.html", errors=errors)

        # Récupérer la clé secrète de reCAPTCHA
        secret_key = '1234567890ABCDE' # à vous de mettre la votre

        #Controle sécurité des variables reçu
        ctr_pseudonyme = post_pseudonyme.controle_avec_une_fonction_perso()
        ctr_address = post_address.controle_avec_une_fonction_perso()
        ctr_message = post_message.controle_avec_une_fonction_perso() # Le message doit être unique pour éviter une utilisation d'un ancien message.
        ctr_signature = post_signature.controle_avec_une_fonction_perso()

        if ctr_pseudonyme or ctr_address or ctr_message or ctr_signature: #Dans mon cas si la fonction controle_avec_une_fonction_perso renvoie true c'est qu'il y a une embrouille
            errors.append(
              "Un élément n'est pas conforme"
            )
            return render_template("login.html", errors=errors)



        w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/xxxxxxxxxxxxxx')) ## Je passe par Infura comme Tier de confiance
        address = post_address
        message = post_message
        signature = post_signature

        #C'est ici que le mécanisme de sécurité contrôle que la signature est bien la bonne
        message_encoded = encode_defunct(text=message)
        recovered_address = w3.eth.account.recover_message(message_encoded, signature=signature)

        if recovered_address.lower() == address.lower():
            user = sql_function(address.lower()) # On récupère si l'utilisateur est déjà dans la bdd
            # Si oui
            if user:
                #on génère la session

            # Sinon, 
            else:
                #on crée le compte dans la BDD
        errors.append(
          "La signature n'est pas valide... c'est normal ça?"
        )
        return render_template("login.html", errors=errors)

Conclusion

Le projet est annulé mais vu le peu d’article sur le sujet de l’authentification avec MetaMask, je pense que mon travail mérite d’être partagé.

Par ailleurs, j’ai pensé à télécharger la chaine Ethereum en local pour éviter le tier de confiance… mais… c’est lourd…

Petit conseil, si vous ne gardez pas tous les messages à signer il faut modifier la structure du message à chaque suppression.

Si vous avez des questions vous pouvez me contacter par mail 😉

David Perez
David Perez
Auditeur de sécurité technique

#auditor #cyber #insider