хуйу нас не матерятся
Оглавление
Авторизацией в 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