iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Modern Web

All In One NFT Website Development系列 第 13

Day 13【連動 MetaMask - Back-End Services】這顯然是廠商的疏失

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/29c81562-03a2-4308-86fc-273390239ea8/d0c0ebb6433fcf9aedcd48e98080e571.png

【前言】
諸君日安,大魔王要出現啦!接下來要說的是Nonce 的使用、前後端連動,以及帳戶驗證。今天的內容大部份都參考來自 Amaury Martiny 的 One-click Login with Blockchain: A MetaMask Tutorial!欸不過居然會提前用到 express,我根本不知道要學,這顯然是廠商的疏失。

【Back-End Services - User
首先使用 express.Router() 來定義路由,這樣未來在前端就可以讓這個路徑被賦予不同的方法。

import * as controller from './controller';

export const userRouter = express.Router();

/** GET /api/users */
userRouter.route('/').get(controller.find);

/** GET /api/users/:userId */
/** Authenticated route */
userRouter.route('/:userId').get(jwt(config), controller.get);

/** POST /api/users */
userRouter.route('/').post(controller.create);

/** PATCH /api/users/:userId */
/** Authenticated route */
userRouter.route('/:userId').patch(jwt(config), controller.patch);

接下來一一介紹路徑內部的方法:

首先是基本宣告,並且要求使用者的 publicAddress 的過程。

import { NextFunction, Request, Response } from 'express';

import { User } from '../../models/user.model';

export const find = (req: Request, res: Response, next: NextFunction) => {
	// If a query string ?publicAddress=... is given, then filter results
	const whereClause =
		req.query && req.query.publicAddress
			? {
					where: { publicAddress: req.query.publicAddress },
			  }
			: undefined;

	return User.findAll(whereClause)
		.then((users: User[]) => res.json(users))
		.catch(next);
};

...

在資料庫裏面查找 User

...

export const get = (req: Request, res: Response, next: NextFunction) => {
	// AccessToken payload is in req.user.payload, especially its `id` field
	// UserId is the param in /users/:userId
	// We only allow user accessing herself, i.e. require payload.id==userId
	if ((req as any).user.payload.id !== +req.params.userId) {
		return res
			.status(401)
			.send({ error: 'You can can only access yourself' });
	}
	return User.findByPk(req.params.userId) // 查找指定主鍵的單一活動記錄。
		.then((user: User | null) => res.json(user))
		.catch(next);
};

...

在沒有登入過的情況時,新建一個使用者。

...

export const create = (req: Request, res: Response, next: NextFunction) =>
	User.create(req.body)
		.then((user: User) => res.json(user))
		.catch(next);

...

【Back-End Services - Auth
Auth 的服務中一樣使用 express.Router() 來定義路由。

import express from 'express';

import * as controller from './controller';

export const authRouter = express.Router();

/** POST /api/auth */
authRouter.route('/').post(controller.create);

其中 signaturepublicAddress 會從前端匯入。因為匯入後資料庫裏面此時應該已經擁有當前登入者的 publicAddress ,因此要找出來。如果找不到就要回報錯誤,表示沒有接收到資料。

import { recoverPersonalSignature } from 'eth-sig-util';
import { bufferToHex } from 'ethereumjs-util';
import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';

import { config } from '../../config';
import { User } from '../../models/user.model';

export const create = (req: Request, res: Response, next: NextFunction) => {
	const { signature, publicAddress } = req.body;
	if (!signature || !publicAddress)
		return res
			.status(400)
			.send({ error: 'Request should have signature and publicAddress' });

	return (
		User.findOne({ where: { publicAddress } })
			.then((user: User | null) => {
				if (!user) {
					res.status(401).send({
						error: `User with publicAddress ${publicAddress} is not found in database`,
					});

					return null;
				}

				return user;
			})
			...
	);
};

找到使用者之後,宣告要釋出 sign-in Message,也就是登入的 nonce 是多少,或想說的話,並且作驗證,驗證的方法是察看當前登入地址與 signature 解讀之後的 publicAddress 是否相符。

	
			...
			.then((user: User | null) => {
				if (!(user instanceof User)) {
					// Should not happen, we should have already sent the response
					throw new Error(
						'User is not defined in "Verify digital signature".'
					);
				}

				const msg = `I am signing my one-time nonce: ${user.nonce}`;

				// We now are in possession of msg, publicAddress and signature. We
				// will use a helper from eth-sig-util to extract the address from the signature
				const msgBufferHex = bufferToHex(Buffer.from(msg, 'utf8'));
				const address = recoverPersonalSignature({
					data: msgBufferHex,
					sig: signature,
				});

				// The signature verification is successful if the address found with
				// sigUtil.recoverPersonalSignature matches the initial publicAddress
				if (address.toLowerCase() === publicAddress.toLowerCase()) {
					return user;
				} else {
					res.status(401).send({
						error: 'Signature verification failed',
					});

					return null;
				}
			})
			...

比較需要注意的是這邊使用到了 eth-sig-util 中的 recoverPersonalSignature 來解讀 signature

eth-sig-util

驗證成功之後就產生一個新的 nonce

	
			...
			.then((user: User | null) => {
				if (!(user instanceof User)) {
					// Should not happen, we should have already sent the response

					throw new Error(
						'User is not defined in "Generate a new nonce for the user".'
					);
				}

				user.nonce = Math.floor(Math.random() * 10000);
				return user.save();
			})
			...

那如果失敗呢?

thumb_your-old-password-cant-be-your-new-password-69045654.png

最後建設一個 JWT 來做數位簽章之中的授權,使每一次向伺服器端送出的需求都是獨立的。其中 config 這個物件的 algorithms: ['HS256' as const], secret: 'shhhh'

	
			...
			.then((user: User) => {
				return new Promise<string>((resolve, reject) =>
					// https://github.com/auth0/node-jsonwebtoken
					jwt.sign(
						{
							payload: {
								id: user.id,
								publicAddress,
							},
						},
						config.secret,
						{
							algorithm: config.algorithms[0],
						},
						(err, token) => {
							if (err) {
								return reject(err);
							}
							if (!token) {
								return new Error('Empty token');
							}
							return resolve(token);
						}
					)
				);
			})
			.then((accessToken: string) => res.json({ accessToken }))
			.catch(next)
	);
};

jsonwebtoken

【小結】
這幾天真的有超多新東西,包含Express routereth-sig-utiljsonwebtoken。光是把程式碼裡面的每一步看懂都很困難嗚嗚,但真的很感謝網路上提供各式各樣教學資源的大神!

T7iqjjGh.jpg

【參考資料】
One-click Login with Blockchain: A MetaMask Tutorial
How do I recover the address from message and signature generated with web3.personal.sign?
Web3.js : eth.sign() vs eth.accounts.sign() -- producing different signatures?
Express.js 4.0 的路由(Router)功能用法教學 - G. T. Wang
JWT.IO


上一篇
Day 12【連動 MetaMask - Backend & Init】277353
下一篇
Day 14【連動 MetaMask - Front-End Request and Fetch】Modern problems require modern solutions
系列文
All In One NFT Website Development30

尚未有邦友留言

立即登入留言