iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 17
0
Modern Web

30 天打造 MERN Stack Boilerplate系列 第 17

Day 17 - Infrastructure - Permission Control

API Server 通常需要做到 2 種權限控制,第一是檢查使用者是否已經登入(Authentication),另一種是對已登入的使用者檢查是否具有足夠權限(Authorization),以 Express 的特性來看,在 Middleware 上面實作權限控制是最理想的,所以接下來將舉例兩個 Boilerplate 中實際用到的 Middlewares。

authRequired

這個 Middleware 大家應該已經在前面的章節看過一兩次了,我們使用它來檢查 Request 中是否夾帶 JWT,如果有,則透過 Passport 的 JWT Strategy 取出目前 Request 的使用者:

const authRequired = (req, res, next) => {
  passport.authenticate(
    'jwt',
    { session: false },
    handleError(res)((user, info) => {
      handlePassportError(res)((user) => {
        if (!user) {
          res.pushError(Errors.USER_UNAUTHORIZED);
          return res.errors();
        }
        req.user = user;
        next();
      })(info, user);
    })
  )(req, res, next);
};

完整程式碼:src/server/middlewares/authRequired.js

未經認證卻想要存取資源的使用者將會收到 Errors.USER_UNAUTHORIZED;經過認證的使用者的 Mongoose Model Instance 會被掛到 req 物件上,方便後續 Middleware 存取使用者資訊。

roleRequired

這個 Middleware 相依於 authRequired,使用 roleRequired 之前一定要先使用 authRequired,因為我們要依賴 authRequired 掛到 req 上的 user 物件。

roleRequired 比對目前使用者的角色 req.user.role 是否包含在指定的角色 requiredRoles 中:

const roleRequired = (requiredRoles) => (req, res, next) => {
  if ((
    requiredRoles instanceof Array &&
    requiredRoles.indexOf(req.user.role) >= 0
  ) || (
    req.user.role === requiredRoles
  )) {
    next();
  } else {
    return res.errors([Errors.PERMISSION_DENIED]);
  }
};

權限不足者會收到 Errors.PERMISSION_DENIED;權限正確的使用者不作任何處理,直接呼叫 next 進入後續 Middlewares。

使用範例

有了以上權限控制的 Middlewares,就可以針對不同 API 實作不同的權限控管,例如只有已登入的 Admin 使用者才能存取 /api/users 這項資源:

import authRequired from '../middlewares/authRequired';
import roleRequired from '../middlewares/roleRequired';

// ...
app.get('/api/users',
  authRequired,
  roleRequired([Roles.ADMIN]),
  userController.list
);

完整程式碼:src/server/routes/api.js

Nonce

在我們 Boilerplate 中還使用了一個有趣的東西 Nonce,這是網路安全領域中的術語,用來防止 Replay Attack。

Web Service 在某些情況下必須散播一次性的 Token,例如信箱驗證或是重設密碼,通常是讓使用者打開一串夾帶著 Token 的超連結,一旦驗證完成或是重設完成,該連結就應該要失效,也就是說該連結夾帶的 Token 是一次性的,不能被重複使用,所以我們在 Boilerplate 中必須要實作防護機制,這個機制正是利用 Nonce 來完成的。

以驗證信箱的功能為例,我們在資料庫中設有 nonce.verifyEmail 欄位,並且提供 toVerifyEmailToken 這個 Instance Method 產生驗證信箱的 Token,Token 中會包入 Nonce:

let UserSchema = new mongoose.Schema({
  // ...
  nonce: {
    verifyEmail: Number,
  },
});

UserSchema.methods.toVerifyEmailToken = function(cb) {
  const user = {
    _id: this._id,
    nonce: this.nonce.verifyEmail,
  };
  const token = jwt.sign(user, configs.jwt.verifyEmail.secret, {
    expiresIn: configs.jwt.verifyEmail.expiresIn,
  });
  return token;
};

完整程式碼:src/server/models/User.js
註:Instance Method 不能使用 Arrow Function,因為我們要在內部使用到 this

產生 Nonce

當使用者註冊時,將 nonce.verifyEmail 設定為一個隨機數:

const user = User({
  name: req.body.name,
  // ...
  nonce: {
    verifyEmail: Math.random(),
  },
});

完整程式碼:src/server/controllers/user.js

再依據此 Nonce 產生 Token 寄送驗證連結至註冊信箱:

let token = user.toVerifyEmailToken();

nodemailerAPI()
  .sendMail({
    to: user.email.value,
    subject: 'Email Verification',
    html: renderToString(
      <VerifyEmailMail token={token} />
    ),
  })
  .catch(err => { /* ... */ })
  .then(info => { /* ... */ });

完整程式碼:src/server/controllers/mail.js

驗證 Nonce

當使用者打開驗證連結後,要如何驗證 Token 是否被重複使用呢?我們只要將 Nonce 從 Token 中取出,並且和資料庫中儲存的 Nonce 比對就可以知道 Token 是第一次使用,還是被重複使用了:

const verifyUserNonce = (req, res, next) => {
  let { _id, nonce } = req.decodedPayload;
  User.findById(_id, handleDbError(res)((user) => {
    if (nonce !== user.nonce.verifyEmail) {
      return res.errors([Errors.TOKEN_REUSED]);
    }
    user.nonce.verifyEmail = -1;
    next();
  }));
};

完整程式碼:src/server/middlewares/validate.js

驗證成功後,我們直接把 Nonce 清除為 -1(任何 Math.random() 無法產生的值皆可),如此一來,下一次如果收到夾帶同樣 Token 的 Request 時,Nonce 將會比對失敗,也就達到了防止 Replay Attack 的效果。


上一篇
Day 16 - Infrastructure - Authentication
下一篇
Day 18 - Infrastructure - i18n
系列文
30 天打造 MERN Stack Boilerplate30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言