Аутентификация API по JWT и Passport

Многие веб-приложения и API-интерфейсы используют форму аутентификации для защиты ресурсов и ограничения доступа к ним.

JSON Web Token (JWT) – это открытый стандарт, который предоставляет удобный полноценный способ безопасной передачи информации между сторонами в виде JSON объектов.

Читайте также: Как использовать JWT-токены в Express.js

В этом мануале вы узнаете, как настроить аутентификацию в API с помощью JWT и Passport, промежуточного обработчика аутентификации для Node.js.

Если вкратце, то здесь мы создадим приложение, которое работает следующим образом:

  • Пользователь регистрируется, создается его учетная запись.
  • Пользователь входит в систему, ему присваивается веб-токен JSON.
  • Пользователь отправляет этот токен отправляет при попытке доступа к определенным защищенным маршрутам.
  • После проверки токена сервер открывает пользователю доступ к маршруту.

Требования

  • Локальная установка Node.js (вы можете получить такую, следуя инструкциям в зависимости от вашей системы: mac OS, CentOS, Ubuntu, Debian).
  • Локальная установка MongoDB (которую вы можете создать, следуя официальной документации).
  • Для тестирования конечных точек API потребуется установка инструмента Postman .

Это руководство было протестировано на Node v14.2.0, npm v6.14.5 и mongodb-community v4.2.6.

1: Настройка проекта

Начнем с настройки нашего тестового проекта. С помощью терминала создайте каталог:

mkdir jwt-and-passport-auth

Перейдите в него:

cd jwt-and-passport-auth

Внутри этого каталога инициализируйте файл package.json:

npm init -y

После этого установите зависимости проекта:

npm install --save bcrypt@4.0.1 body-parser@1.19.0 express@4.17.1 jsonwebtoken@8.5.1 mongoose@5.9.15 passport@0.4.1 passport-jwt@4.0.0 passport-local@1.0.0

Пакет bcrypt понадобится нам для хеширования паролей пользователей, jsonwebtoken – для подписи токенов, passport-local – для реализации локальной стратегии аутентификации, а passport-jwt – для извлечения и проверки JWT.

Важно! При запуске этой команды установки могут возникнуть проблемы с bcrypt – это зависит от версии Node, которую вы используете. Почитайте этот README, чтобы определить совместимость пакета с вашей средой.

На данном этапе проект инициализирован, а все зависимости установлены. Теперь мы добавим базу данных для хранения информации о пользователях.

2: Настройка базы данных

Схема базы данных определяет типы данных и структуру БД. Нашей БД потребуется схема для пользователей.

Создайте каталог model:

mkdir model

В этом каталоге создайте файл model.js:

nano model/model.js

Библиотека mongoose определяет схему, которая сопоставляется с коллекцией MongoDB. Согласно схеме, пользователю потребуются адрес электронной почты и пароль. Библиотека mongoose берет схему и преобразует ее в модель. Вставьте в ваш файл model.js следующее:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
const UserModel = mongoose.model('user', UserSchema);
module.exports = UserModel;

Крайне не рекомендуем хранить пароли в виде обычного текста: если злоумышленнику удастся получить доступ к вашей базе данных, он сможет воспользоваться вашими конфиденциальными данными.

Чтобы избежать этого, мы будем использовать bcrypt, пакет для хеширования паролей и их безопасного хранения. Добавьте в файл model.js следующие строки:

// ...
const bcrypt = require('bcrypt');
// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
'save',
async function(next) {
const user = this;
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
next();
}
);
// ...
module.exports = UserModel;

Код в функции UserScheme.pre() называется пре-hook. Эта функция будет вызвана перед сохранением информации о пользователе в базе данных: она получит пароль в виде простого текста, хеширует его и сохранит в БД.

Параметр this относится к текущему документу, который нужно сохранить.

Строка await bcrypt.hash(this.password, 10) передает пароль и значение соли (salt round или cost), в данном случае это 10. Чем выше cost, тем больше итераций будет выполняться при хешировании, следовательно, тем надежнее будет результат. Недостаток параметра cost заключается в том, что он требует больших вычислительных ресурсов и может повлиять на производительность приложения.

Потом простой текстовый пароль заменяется хешем и сохраняется в БД.

next() указывает, что вы закончили и должны перейти к следующему промежуточному обработчику.

Вам также необходимо убедиться, что пользователь, пытающийся войти в систему, указывает правильные учетные данные. Добавьте в файл новый метод:

// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
// ...
});
UserSchema.methods.isValidPassword = async function(password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
// ...
module.exports = UserModel;

Пакет bcrypt хеширует отправленный пользователем пароль, а потом проверяет, совпадает ли хешированный пароль, хранящийся в базе данных, с отправленным. Он вернет true, если пароли совпадают. В противном случае он вернет false.

Итак, у нас есть схема и модель для коллекции MongoDB.

3: Настройка промежуточного обработчика регистрации

Passport – это промежуточный обработчик, используемый для аутентификации запросов.

Он позволяет разработчикам использовать различные стратегии для аутентификации пользователей, например, локальную базу данных или подключение к социальным сетям через API.

В этом мануале мы отдаем предпочтение первой стратегии – локальной БД (которая требует адрес электронной почты и пароль). Стратегия passport-local используется для создания промежуточного обработчика, который будет обслуживать регистрацию пользователей и вход в систему. Затем мы подключим его к определенным маршрутам и будем использовать для аутентификации.

Создайте каталог auth:

mkdir auth

В этом каталоге создайте файл auth.js:

nano auth/auth.js

Во-первых, давайте загрузим passport, passport-local и UserModel, которую мы создали на предыдущем шаге:

const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const UserModel = require('../model/model');

Во-вторых, нужно добавить в этот файл промежуточный обработчик Passport для обработки регистрации пользователя:

// ...
passport.use(
'signup',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.create({ email, password });
return done(null, user);
} catch (error) {
done(error);
}
}
)
);

Этот код сохраняет предоставленную пользователем информацию в базу данных, а в случае успеха отправляет эти данные о пользователе следующему промежуточному обработчику. В противном случае он сообщает об ошибке.

Затем добавьте промежуточный обработчик для входа пользователя в систему:

// ...
passport.use(
'login',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const validate = await user.isValidPassword(password);
if (!validate) {
return done(null, false, { message: 'Wrong Password' });
}
return done(null, user, { message: 'Logged in Successfully' });
} catch (error) {
return done(error);
}
}
)
);

Этот код определяет одного пользователя, связанного с указанным адресом электронной почты.

  • Если данные, которые ввел пользователь, не соответствуют ни одному набору данных в БД, возвращается ошибка “User not found”.
  • Если пароль не совпадает с тем паролем, что привязан к этому пользователю в базе данных, возвращается ошибка “Wrong Password”.
  • Если имя пользователя и пароль совпадают, возвращается сообщение “Logged in Successfully”, после чего информация о пользователе отправляется следующему промежуточному обработчику.

В противном случае обработчик сообщает об ошибке.

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

4: Создание конечной точки регистрации

Express – это веб-фреймворк, который обеспечивает маршрутизацию. На этом этапе мы можем настроить маршрут для конечной точки регистрации.

Прежде всего создайте каталог routes:

mkdir routes

В этом новом каталоге создайте файл routes.js:

nano routes/routes.js

Чтобы загрузить express и passport, вставьте в начало этого файла такие строки:

const express = require('express');
const passport = require('passport');
const router = express.Router();
module.exports = router;

Затем добавьте обработку POST-запросов на signup:

// ...
const router = express.Router();
router.post(
'/signup',
passport.authenticate('signup', { session: false }),
async (req, res, next) => {
res.json({
message: 'Signup successful',
user: req.user
});
}
);
module.exports = router;

Когда пользователь отправляет POST-запрос на этот маршрут, Passport аутентифицирует этого пользователя на основе ранее созданного промежуточного обработчика.

Теперь у вас есть конечная точка регистрации, signup. Однако нам нужна еще одна конечная точка – для входа, login.

5: Создание конечной точки входа и подпись JWT-токена

Когда пользователь входит в систему, информация о нем передается вашему обратному вызову, который, в свою очередь, создает надежный токен с этой информацией.

Сейчас мы создадим маршрут для конечной точки входа. Все нижеприведенные строки нужно добавить в файл routes/routes.js.

Во-первых, нам потребуется jsonwebtoken:

const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
// ...

Во-вторых, мы добавим обработку POST-запроса для login:

// ...
const router = express.Router();
// ...
router.post(
'/login',
async (req, res, next) => {
passport.authenticate(
'login',
async (err, user, info) => {
try {
if (err || !user) {
const error = new Error('An error occurred.');
return next(error);
}
req.login(
user,
{ session: false },
async (error) => {
if (error) return next(error);
const body = { _id: user._id, email: user.email };
const token = jwt.sign({ user: body }, 'TOP_SECRET');
return res.json({ token });
}
);
} catch (error) {
return next(error);
}
}
(req, res, next);
}
);
module.exports = router;

В токене не следует хранить конфиденциальную информацию, например, пароль пользователя.

Мы сохраняем id и email в полезной нагрузке JWT, а затем подписываем токен секретным ключом (TOP_SECRET). После всего этого токен отправляется обратно пользователю.

Примечание: Строка {session: false} позволяет не сохранять данные пользователя в сеансе. Следовательно, пользователь будет отправлять токен при каждом запросе защищенных маршрутов. В работе с API это очень полезно, однако этого не рекомендуется делать с веб-приложениями из соображений производительности.

Теперь у вас есть конечная точка login. Успешно вошедший в систему пользователь сгенерирует токен. Однако пока что приложение еще не умеет работать с токенами.

6: Проверка токенов JWT

Итак, наш следующий шаг – предоставить пользователям с токенами доступ к определенным защищенным маршрутам.

На этом этапе мы научим приложение проверять токены, чтобы убедиться, что они действительны и не изменялись.

Вернитесь в файл auth.js:

nano auth/auth.js

Добавьте в него следующие строки кода:

// ...
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
passport.use(
new JWTstrategy(
{
secretOrKey: 'TOP_SECRET',
jwtFromRequest: ExtractJWT.fromUrlQueryParameter('secret_token')
},
async (token, done) => {
try {
return done(null, token.user);
} catch (error) {
done(error);
}
}
)
);

Этот код использует обработчик passport-jwt для извлечения JWT из параметра запроса. Затем этот обработчик проверяет, был ли токен подписан с помощью секретного ключа, установленного во время входа в систему (TOP_SECRET). Если токен действителен, данные пользователя передаются следующему промежуточному обработчику.

Примечание: Если вам потребуются дополнительные или конфиденциальные сведения о пользователе, которых нет в токене, вы можете использовать доступный в токене _id для их извлечения из базы данных.

Теперь ваше приложение может подписывать токены и проверять их.

7: Создание защищенных маршрутов

Теперь давайте создадим несколько защищенных маршрутов, к которым могут получить доступ только пользователи с токенами, прошедшими проверку.

Создайте новый файл secure-routes.js:

nano routes/secure-routes.js

Добавьте в файл следующие строки кода:

const express = require('express');
const router = express.Router();
router.get(
'/profile',
(req, res, next) => {
res.json({
message: 'You made it to the secure route',
user: req.user,
token: req.query.secret_token
})
}
);
module.exports = router;

Этот код обрабатывает GET-запрос для profile. Он возвращает сообщение “You made it to the secure route”, а также информацию о пользователе и токене.

Наша цель заключается в том, чтобы этот ответ получали только пользователи с проверенным токеном.

8: Объединение всех конфигураций

Итак, теперь, когда мы создали маршруты, конечные точки и промежуточные обработчики для аутентификации, мы можем собрать все конфигурации воедино.

Создайте новый файл app.js:

nano app.js

А затем вставьте в этот файл такие строки:

const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const bodyParser = require('body-parser');
const UserModel = require('./model/model');
mongoose.connect('mongodb://127.0.0.1:27017/passport-jwt', { useMongoClient: true });
mongoose.connection.on('error', error => console.log(error) );
mongoose.Promise = global.Promise;
require('./auth/auth');
const routes = require('./routes/routes');
const secureRoute = require('./routes/secure-routes');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use('/', routes);
// Plug in the JWT strategy as a middleware so only verified users can access this route.
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);
// Handle errors.
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.json({ error: err });
});
app.listen(3000, () => {
console.log('Server started.')
});

Примечание: В зависимости от вашей версии mongoose вы можете получить такое сообщение:

WARNING: The 'useMongoClient' option is no longer necessary in mongoose 5.x, please remove it.

Это значит, что ваша версия mongoose – 5.x, а в ней параметр ‘useMongoClient’ больше не нужен. Вы также можете получить уведомления о том, что параметры useNewUrlParser, useUnifiedTopology и sureIndex (createIndexes) устарели.

Чтобы устранить эти ошибки, нужно изменить вызов метода mongoose.connect и добавить вызов метода mongoose.set:

mongoose.connect("mongodb://127.0.0.1:27017/passport-jwt", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
mongoose.set("useCreateIndex", true);

Запустите ваше приложение с помощью следующей команды:

node app.js

Вы увидите сообщение:

Server started.

Оставьте приложение запущенным – далее мы протестируем его.

9: Тестирование приложения с помощью Postman

Теперь, когда вы собрали все конфигурации вместе, вы можете использовать Postman, чтобы проверить, работает ли аутентификация API.

Примечание: Если вам нужна помощь в навигации по интерфейсу Postman, обратитесь к официальной документации.

Во-первых, нам нужно зарегистрировать в своем приложении нового пользователя, а для этого нужны адрес электронной почты и пароль.

В Postman настройте запрос к конечной точке signup, которую вы создали в файле routes.js:

POST localhost:3000/signup
Body
x-www-form-urlencoded

И отправьте в теле вашего запроса эти данные:

ключ значение
e-mail example@example.com
пароль password

Когда это будет сделано, нажмите кнопку Send, чтобы инициировать POST-запрос:

{
"message": "Signup successful",
"user": {
"_id": "[длинная строка символов, представляющая уникальный id]",
"email": "example@example.com",
"password": "[длинная строка символов, представляющая зашифрованный пароль]",
"__v": 0
}
}

Как видите, пароль отображается в виде зашифрованной строки, потому что именно так он хранится в базе данных. Это результат работы пре-hook, который мы написали в файле model.js для bcrypt и хеширования пароля.

Теперь войдите в систему с помощью ваших учетных данных и получите свой токен.

В Postman настройте запрос к конечной точке login, которую вы создали в файле routes.js:

POST localhost:3000/login
Body
x-www-form-urlencoded

А затем отправьте эти данные в теле вашего запроса:

ключ значение
e-mail example@example.com
пароль password

B нажмите кнопку Send, чтобы инициировать POST-запрос. Вы получите такой вывод:

{
"token": "[длинная строка символов, представляющая токен]"
}

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

Вы можете посмотреть, как приложение обрабатывает проверку токенов, открыв /user/profile.

Вернитесь в Postman и настройте запрос к конечной точке profile, которую вы создали в файле secure-routes.js:

GET localhost:3000/user/profile
Params

Передайте свой токен в параметре запроса, который называется secret_token.

ключ значение
secret_token [длинная строка символов, представляющая токен]

Когда вы сделаете это, нажмите Send, чтобы инициировать GET-запрос, и вы получите:

{
"message": "You made it to the secure route",
"user": {
"_id": "[длинная строка символов, представляющая уникальный id]",
"email": "example@example.com"
},
"token": "[длинная строка символов, представляющая токен]"
}

Ваш токен будет собран и проверен. Если токен действителен, вы получите доступ к защищенному маршруту. Это результат ответа, который вы создали в secure-routes.js.

Вы также можете попробовать получить доступ к этому маршруту с заведомо неправильным токеном – и запрос вернет ошибку «Unauthorized».

Заключение

В этом руководстве вы научились настраивать аутентификацию API с помощью токенов JWT и тестировать ее с помощью Postman.

Веб-токены JSON предоставляют API-интерфейсам безопасный способ аутентификации. Вы можете добавить еще один уровень безопасности, зашифровав всю информацию в токене, тем самым сделав его еще более надежным.

Если вам нужны более глубокие знания о JWT, вы можете обратиться за дополнительной информацией к этим источникам:

Tags: , , , , ,

Добавить комментарий