【前言】
諸君日安,大魔王要出現啦!接下來要說的是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);
其中 signature
與 publicAddress
會從前端匯入。因為匯入後資料庫裏面此時應該已經擁有當前登入者的 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
。
驗證成功之後就產生一個新的 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();
})
...
那如果失敗呢?
最後建設一個 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)
);
};
【小結】
這幾天真的有超多新東西,包含Express router
、eth-sig-util
、jsonwebtoken
。光是把程式碼裡面的每一步看懂都很困難嗚嗚,但真的很感謝網路上提供各式各樣教學資源的大神!
【參考資料】
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