Authentification MetaMask
C’est vraiment ton adresse?
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 😉