Создаём REST API сервер на Hapi часть 5: Авторизация

Авторизацией в hapi занимаются отдельные модули, в нпм репозитории их куча, на любой вкус. Мне приходилось работать только с bearer токенами, про них я и расскажу.

Для начала нужно установить модули:

npm i hapi-auth-bearer-token crypto

Нам нужна будет табличка, в которой будем хранить токены, добавляем новую модель:

'use strict';

const Crypto = require('crypto');

module.exports = (sequelize, DataTypes) => {
  const accessToken = sequelize.define('access_tokens', {
    id: {
      allowNull: false,
      autoIncrement: true,
      primaryKey: true,
      type: DataTypes.INTEGER
    },
    token: {
      type: DataTypes.STRING,
      allowNull: false
    },
    user_id: {
      type: DataTypes.INTEGER,
      allowNull: false
    },
    expires_at: {
      type: DataTypes.DATE,
      allowNull: false
    },
  });
  
  accessToken.generateAccessTokenString = () => {
    // eslint-disable-next-line no-undef
    return Crypto.createHmac('md5', Crypto.randomBytes(512).toString()).update([].slice.call(arguments).join(':')).digest('hex');
  };
  
  accessToken.createAccessToken = (user) => {
    const options = {
      user_id: user.get('id'),
      expires_at: (new Date(new Date().valueOf() + (30 * 24 * 60 * 60 * 1000))),
      access_token: accessToken.generateAccessTokenString(user.get('id'), user.get('email'), new Date().valueOf())
    };
    
    return accessToken.create(options);
  };
  
  accessToken.dummyData = [
    {
      id: 1,
      token: 'a47fa9fead309305dddf17bdde0d75b8',
      user_id: 1,
      expires_at: new Date() + 1000*60*60*24*7 // 1 week
    }
  ];
  
  return accessToken;
};

Затем нужно зарегистрировать модуль в нашем сервере:

...
const AuthBearer = require('hapi-auth-bearer-token');
...
  await server.register([
    ...
    AuthBearer,
    hapiBoomDecorators,
    Inert,
    ...
  ]);

После, нужно добавить стратегию авторизации

  server.auth.strategy('token', 'bearer-access-token', { // Создаём новую стратегию с именем 'token'
    allowQueryToken: false,
    unauthorized: bearerValidation.unauthorized, // вешаем функцию-обработчик не авторизованных запросов
    validate: bearerValidation.validate // а вот тут будем решать авторизирован запрос или нет
  });

Ну а теперь нужно написать сами обработчики, наш новый модуль bearerValidation:

// ./src/libs/bearerValidation.js

const Boom = require('boom');
const Op = require('sequelize').Op;

async function unauthorized () { // Пока эта функция ничего не делает, только возвращает стандартный ответ
  throw Boom.unauthorized(); // Но сюда можно добавить много чего интересного
}

async function validate (request, token) { // а вот тут уже начинаем проверку запроса
  // в request лежит всё то же самое что и в обычном руте, включая модели
  // а в token - сам наш токен, который прислал клиент
  
  const accessToken = request.getModel(request.server.config.db.database, 'access_tokens');
  const users = request.getModel(request.server.config.db.database, 'users');
  
  // Херачим запрос в бд, и смотрим есть ли такой токен и валиден ли он
  let dbToken = await accessToken.findOne({ where: {
      token: token,
      expires_at: {
        [ Op.gte ]: new Date()
      }
    }
  });
  
  if( !dbToken ) {
    // Нет такого токена, либо он просрочен
    return {
      isValid: false,
      credentials: {}
    };
  }
  
  // Ищем юзера
  let curUser = await users.findOne({ where: { id: dbToken.dataValues.user_id } });
  
  if( !curUser ) {// Если юзера нет, то говорим, что неавторизованы
    // Нужно удалить невалидный токен
    accessToken.destroy({ where: { user_id: dbToken.dataValues.user_id } });
    return {
      isValid: false,
      credentials: {}
    };
  }
  
  // Если же юзер есть, то 
  return {
    isValid: true,
    credentials: {
      role: 'admin' // Сюда фигачим роль юзера
    },
    artifacts: { // а вот сюда фигачим любые данные которые нам могут пригодиться внутри рута
      token: token,
      user: curUser.dataValues
    }
  };
}

module.exports = {
  validate: validate,
  unauthorized: unauthorized
}; 

Ну и теперь нам нужно написать пару роутов чтобы это всё заработало.

Авторизация:

//./src/routes/auth/post.js

const Joi = require('joi');
const Boom = require('boom');

async function response(request) {
  
  // Подключаем модельки
  const accessTokens = request.getModel(request.server.config.db.database, 'access_tokens');
  const users = request.getModel(request.server.config.db.database, 'users');
  
  // Ищем пользователя по мылу
  let userRecord = await users.findOne({ where: { email: request.query.login } });

  // если не нашли, говорим что не авторизованы
  if( !userRecord ) {
    throw Boom.unauthorized();
  }
  
  // Проверяем совподают ли пароли
  if( !userRecord.verifyPassword(request.query.password) ) {
    throw Boom.unauthorized();// если нет, то опять ж говорим, что не авторизованы
  }
  
  // Иначе, создаём новый токен
  let token = await accessTokens.createAccessToken(userRecord);
  
  // и возвращаем его
  return {
    meta: {
      total: 1
    },
    data: [token.dataValues]
  };
}

// А тут описываем схему ответа
const tokenScheme = Joi.object({
  id: Joi.number().integer().example(1),
  user_id: Joi.number().integer().example(2),
  expires_at: Joi.date().example('2019-02-16T15:38:48.243Z'),
  token: Joi.string().example('4443655c28b42a4349809accb3f5bc71'),
  updatedAt: Joi.date().example('2019-02-16T15:38:48.243Z'),
  createdAt: Joi.date().example('2019-02-16T15:38:48.243Z')
});

const responseScheme = Joi.object({
  meta: Joi.object({
    total: Joi.number().integer().example(3)
  }),
  data: Joi.array().items(tokenScheme)
});

module.exports = {
  method: 'GET',
  path: '/auth',
  options: {
    handler: response,
    tags: ['api'], // Necessary tag for swagger
    validate: {
      query: {
        login: Joi.string().required(),
        password: Joi.string().required()
      }
    },
    response: { schema: responseScheme } // если схема ответа не будет совподать с тем что реально отдаётся
    // сервер отдаст 500 ошибку
  }
};

И теперь, в любом нашем руте, всего лишь нужно добавить тег: "auth: 'token'", чтобы он стал доступен только по авторизации. Например переделаем POST /messages

// ./src/routes/mesages/post.js

const Joi = require('joi');

async function response(request) {

  const messages = request.getModel(request.server.config.db.database, 'messages');
  let newMessage = await messages.create(request.payload);

  let count = await messages.count();

  return {
    meta: {
      total: count
    },
    data: [ newMessage ]
  };
}

const messageSchema = Joi.object({
  id: Joi.number().integer().example(1),
  user_id: Joi.number().integer().example(2),
  message: Joi.string().example('Lorem ipsum')
});

const responseScheme = Joi.object({
  meta: Joi.object({
    total: Joi.number().integer().example(3)
  }),
  data: Joi.array().items(messageSchema)
});

module.exports = {
  method: 'POST',
  path: '/messages',
  options: {
    handler: response,
    tags: ['api'], // Necessary tag for swagger
    auth: 'token', // >>>> Это необходимый тег для включения авторизации
    validate: {
      payload: {
        user_id: Joi.number().integer().required().example(1),
        message: Joi.string().min(1).max(100).required().example('Lorem ipsum')
      }
    },
    response: { schema: responseScheme } 
  }
};

Проверяем:

user@Thinik:~$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d 'ololo ololo' 'http://localhost:3030/messages'
{"statusCode":401,"error":"Unauthorized","message":"Unauthorized"}

Всё ок, доступ без токена закрыт, теперьь то же самое но с токеном:

user@Thinik:~$ curl -X GET --header 'Accept: application/json' 'http://localhost:3030/auth?login=pupkin%40gmail.com&password=12345'
{"meta":{"total":1},"data":[{"id":8,"user_id":1,"expires_at":"2019-02-16T15:54:42.521Z","token":"669979fad3f109282177c6fc8896fed8","updatedAt":"2019-01-17T15:54:42.523Z","createdAt":"2019-01-17T15:54:42.523Z"}]}

// копипастим "token":"669979fad3f109282177c6fc8896fed8" в следующий запрос

user@Thinik:~$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' 'http://localhost:3030/messages' -H 'Authorization: Bearer 669979fad3f109282177c6fc8896fed8' --data '{"user_id": 1, "message": "olololo"}'
{"meta":{"total":9},"data":[{"id":9,"user_id":1,"message":"olololo","updatedAt":"2019-01-17T16:02:24.875Z","createdAt":"2019-01-17T16:02:24.875Z"}]}

Работает)

Как обычно, все исходники на гитхабе: https://github.com/hololoev/api_hapi_example_5



Обзор HP envy 13 2018г

Т.к. беда не приходит одна, купили мы бук ещё и для жены. И естестно для девочки бук должен быть няшненький.

Как использовать ORM Sequelize

Sequelize - это ORM библиотека для nodejs. Sequelize поддерживает PostgreSQL, MySQL, SQLite и MSSQL диалекты.


(0) Комментариев