Введение
В этой статье мы поговорим о том, как работают JSON Web Tokens, в чем их преимущества, какова их структура и как использовать их для базовой аутентификации и авторизации в Express.
Вам не обязательно иметь опыт работы с JSON Web Tokens (JWT), поскольку мы будем говорить об этом с нуля.
Для раздела, посвященного реализации, будет предпочтительнее, если у вас есть опыт работы с Express, Javascript ES6 и REST-клиентами.
Что такое веб-токены json?
JSON Web Tokens (JWT) были введены в качестве метода безопасного обмена данными между двумя сторонами. Он был представлен в спецификации RFC 7519, разработанной рабочей группой инженеров Интернета (IETF).
Authentication integration completed
That’s it! In this tutorial, you learned how Passport.js works, how to configure it, and how to integrate it with Node.js and Auth0 to add authentication to web applications. You also learned about security and identity best practices and how an identity platform such as Auth0 lets you delegate to a team of experts the giant responsibility of keeping logins secure.
All that is left is for you to continue building this application as you may like. Feel free to dive deeper into the Auth0 Documentation to learn more about how Auth0 helps you save time on implementing and managing identity. Use the comments below this blog post to ask questions or join the Auth0 Community to connect with other developers like yourself.
Thank you for your time!
I ran into an issue
Configure express-session
🛠️️ Next, open index.js and add the following under the Required External Modules section:
const express =require("express");const path =require("path");const expressSession =require("express-session");const passport =require("passport");const Auth0Strategy =require("passport-auth0");require("dotenv").config();
You are adding imports for express-session, passport, and passport-auth0, which you’ll configure in the next sections.
🛠️️ Between the App Variables and App Configuration sections, create two new sections, Session Configuration and Passport Configuration:
const app =express();const port = process.env.PORT||"8000";
Order matters in Express. Please ensure that you add the sections in the right order.
🛠️️ Under Session Configuration, configure expressSession as follows:
const session ={
secret: process.env.SESSION_SECRET,
cookie:{},
resave:false,
saveUninitialized:false};if(app.get("env")==="production"){
session.cookie.secure =true;}
expressSession takes a configuration object, session, that defines what options to enable in a session. Here, you are configuring the following options:
Configure passport with the application settings
In this section, you’ll focus on wiring up Passport.js with your Express app.
🛠️️ With Auth0Strategy already imported, proceed to define this strategy under the Passport Configuration section in index.js:
const strategy =newAuth0Strategy({
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL: process.env.AUTH0_CALLBACK_URL},function(accessToken, refreshToken, extraParams, profile, done){returndone(null, profile);});
Here, the Auth0Strategy method takes your Auth0 credentials and initializes the strategy. It’s essential to understand what the Auth0Strategy is doing for you:
Create custom middleware with express
🛠️️ In index.js, update the App Configuration section to enhance the response object, res, with data from the authentication server. Do this right above the mounting of the authentication router:
Log into a node.js express app
🛠️️ Head to the browser tab where your application is running and click the login button to test that it is communicating correctly with Auth0 and that you can get authenticated.
🛠️️ If you’ve set up everything correctly, the application redirects you to the Universal Login page.
Node.js и express авторизация:
Для начала скачаем все нужные нам компоненты, все их три, это mongoose, для работы с базой данных MongoDB, passport, для создания авторизации, passport-jwt, для проверки токена, и jsonwebtokenну для создания JWT токена, и конечно не забывайте про сам Express, но если вы читаете эту статью, то думаю в вашем проекте он уже работает, и как его установить я не буду показывать, но если вам интересно, то посмотрите статью «Express.js быстрый старт».
Установка нужных компонентов:
После того как вы всё установили, то можете начать разработку.
Теперь перейдите в файл «auth.js», который должен находиться в папке «controller», первым делом импортируем все нужные компоненты в него:
Как видите мы импортировали модель пользователя, и библиотеку для создания JWT токена.
Дальше создадим специальную функцию, которая будет отвечать за создание JWT токена, вот как она будет выглядеть:
Тут мы создали стрелочную функцию, в неё передаём id пользователя и его Email, следующие делаем объект, в котором будем хранить так же id пользователя и его Email, это нужно для того, чтобы когда будем получать этот токен, мы могли идентифицировать пользователя с токеном.
Последние тут мы создаём токен, делаем это с помощью метода jwt.sign(), первым параметром принимает объект который мы сделали выше, вторым слово которая нужно будет использовать потом, чтобы расшифровать токен и получить данные из него, третий это объект с различными параметрами.
После того как мы сделали функцию для создания токена, переходим внутри этого файла в класс authController, который мы ещё создали в статье по регистрации ссылкой выше, внутри класса создаём метод login(), который будет отвечать за вход на сайт.
Вот как он работает:
Первое что вы тут замечаете, это то, что во всём коде мы будем отлавливать его ошибки, и в той части, где мы отлавливаем ошибки, мы берём email и пароль, находим пользователя по его email, и если нет, то отправляем ошибку.
Set up real-world authentication for node.js
This tutorial’s core objective is to teach you how to set up real-world authentication in a Node.js Express app. For that reason, you’ll start by setting up Auth0 to interact with a real authentication server throughout the tutorial. Otherwise, Passport.js gives you a ton of error messages in the terminal, and the app won’t run.
Auth0 is a global leader in Identity-as-a-Service (IDaaS). Its extensible platform seamlessly authenticates and secures more than 2.5 billion logins per month, making it loved by developers and trusted by global enterprises.
The best part of the Auth0 platform is how streamlined it is to get started by following these three easy steps.
Step 1: sign up and create an auth0 application
🛠️️ If you are new to Auth0, sign up for a free Auth0 account here. A free account offers you:
🛠️️ During the sign-up process, you’ll create something called a Tenant, representing the product or service to which you are adding authentication — more on this in a moment.
🛠️️ Once you are signed in, you are welcomed into the Auth0 Dashboard. In the left sidebar menu, click on “Applications”.
Step 3: add auth0 configuration variables to node.js
🛠️️ Under the project directory, create a hidden file called .env to store configuration variables and secrets that your app needs.
touch .env
Make sure to add this file to .gitignore so that it isn’t committed to version control.
🛠️️ Add the following to .env:
AUTH0_CLIENT_ID=AUTH0_DOMAIN=AUTH0_CLIENT_SECRET=
🛠️️ Head back to your Auth0 application Settings tab and populate each property of the .env hidden file with its corresponding Auth0 application value:
Token steal
Вишенкой на торте в данной картине механизма авторизации/аутентификации стал результат изучения проблемы кражи токенов. Ради минимизации рисков от попадания в такую ситуацию, было решено улучшить механизмы работы с авторизационным токеном. Во время исследования данной темы наткнулся на статью на хабре – Зачем нужен Refresh Token, если есть Access Token?. Очень советую ознакомиться, но если кратко, то вот результирующая цитата:
Таким образом, схема refresh access токен ограничивает время, на которое атакующий может получить доступ к сервису. По сравнению с одним токеном, которым злоумышленник может пользоваться неделями и никто об этом не узнает.
Однако, у нас уже есть два токена:
Use middleware for authentication
As explained in the “Using middleware” section of the Express docs, an Express application is essentially a series of middleware function calls that execute during the request-response cycle. Each function can modify the request and response objects as needed and then either pass control to the next middleware function or end the request-response cycle.
What you will build
You’ll secure the login portal for a restaurant named WHATABYTE using Passport.js with Auth0:
We tested this tutorial using Node.js v12.16.0 and npm v6.13.
Аутентификация в приложениях node.js с помощью passport
Реализация надежных систем аутентификации для любого приложения может стать непростой задачей, приложение Node.js здесь не исключение.
В этой статье мы создадим с нуля Node.js приложение и используем относительно новое, но уже очень популярное связующее программное обеспечение – Passport, чтобы разобраться с проблемами аутентификации.
Документация Passport описывает его как “простое, компактное связующее приложение аутентификации для Node” и это верно.
Позиционируясь как связующее программное обеспечение, Passport отлично справляется с тем чтобы выделить среди других элементов веб-приложений именно аспекты аутентификации.
Это позволяет Passport легко настроить любое веб-приложение на базе Express, так же, как мы можем просто настроить другой связующий Express-софт, например, logging, body-parsing, cookie-parsing, session-handling и т.д.
В этой статье мы приведем обзор базовых возможностей фреймворков Node.js и Express, при этом делая упор на вопросы аутентификации. Хотя само приложение Express мы создадим с нуля и расширим его, добавив маршруты и аутентификацию некоторых из этих маршрутов.
Passport предлагает нам на выбор свыше 140 механизмов аутентификации. Вы можете проводить аутентификацию с помощью локального/удаленного экземпляра объекта базы данных или использовать единый вход с использованием OAuth, предоставляемый Facebook, Twitter, Google и т.д., для аутентификации в ваших аккаунтах социальных медиа.
Или вы можете выбрать из обширного списка провайдеров, которые поддерживают аутентификацию с помощью Passport и предоставляют для него модуль узла.
Но не беспокоитесь: Вам не нужно включать все стратегии / механизмы, которые вашему приложению и не нужны. Все эти стратегии являются независимыми друг от друга и упакованы в виде отдельных модулей узлов, которые не включаются по умолчанию при установке связующего программного обеспечения Passport: npm install passport.
В этой статье мы будем использовать локальную стратегию аутентификации Passport и аутентификацию пользователей с помощью локально настроенного экземпляра объекта Монго DB, хранящего информацию о пользователе в базе данных.
Для использования стратегии локальной аутентификации, мы должны установить модуль passport-local: npm install passport-local.
Но подождите минутку: перед тем, как вы запустите терминал и начнете выполнение этих команд, давайте все же рассмотрим построение приложения Express с нуля и добавление к нему некоторых маршрутов (для авторизации, регистрации и главной страницы), а затем добавим связующий софт для аутентификации.
Обратите внимание, что в этой статье мы будем использовать Express 4, но с некоторыми незначительными отличиямиPassport одинаково хорошо работает с Express 3.
Если вы еще этого не сделали, то установите Express и express-generator для создания шаблонных приложений. Для этого нужно просто выполнить в терминале команду express passport-mongo. Сформированная структура приложения должна выглядеть следующим образом:
Давайте удалим некоторый функционал по умолчанию, который нам вовсе необязательно использовать – удалите маршрут users.js и уберите все его ссылки из файла app.js.
Откройте файл package.json и добавьте в него зависимости для passport и модуля passport-local:
Так как мы будем сохранять информацию о пользователе в MongoDB, мы будем использовать Mongoose в качестве инструмента моделирования данных объекта. Также установить и сохранить зависимость для package.json можно с помощью команды:
package.json должен выглядеть следующим образом:
Теперь установите все зависимости и запустите шаблонное приложение, выполнив команду npm install && npm start. Она загружает и устанавливает все зависимости и запускает сервер узла. Вы можете проверить установку приложения Express, перейдя по адресу: http://localhost:3000/, но там вы пока еще ничего особенного не увидите.
Очень скоро мы это изменим, создав полноценное Express приложение, которое запрашивает вывод страницы регистрации для нового пользователя, обрабатывает вход в систему зарегистрированного пользователя и аутентифицирует зарегистрированного пользователя, используя Passport.
Так как мы будем сохранять информацию о пользователях в Mongo, давайте создадим модель пользователя в Mongoose и сохраним ее в файле models/user.js нашего приложения:
Мы создаем модельMongoose, с помощью которой мы можем выполнять CRUD операции в основной базе данных.
Если у вас нет установленного локально Mongo, мы рекомендуем использовать облачные сервисы баз данных, такие как Modulus или MongoLab.
Создание рабочего экземпляра MongoDB с их помощью не только бесплатно, но кроме того это можно сделать всего лишь в несколько кликов.
После создания базы данных на одном из этих сервисов, вы получите URI базы данных, наподобие mongodb://:@novus.modulusmongo.net:27017/, который можно использовать для выполнения CRUD-операций с базой данных.
Я рекомендую сохранить конфигурацию базы данных в отдельный файл, который можно будет подтянуть, когда это будет необходимо. Таким образом, мы создаем модуль узла db.js, который выглядит следующим образом:
Теперь мы используем эту конфигурацию в app.js и подключаемся к ней с помощью Mongoose API:
Passport только обеспечивает механизм для обработки аутентификации, не заботясь о реализации обработки своей же сессии, для этого мы будем использовать express-session.
Откройте файл app.js и вставьте перед настройкой маршрутов приведенный ниже код:
Это необходимо, поскольку мы хотим, чтобы сеансы пользователей были стабильными. Перед запуском приложения мы должны установить express-session и добавить его в список зависимостей файла package.json. Для этого наберите в командной строке – npm install –save express-session.
Для Passport также необходима сериализация и десериализация экземпляра объекта пользователя из сессии сохранения в целях поддержки текущей сессии, так чтобы каждый последующий запрос не содержал учетные данные пользователя. Для этого предназначены два метода serializeUser и deserializeUser:
Теперь мы определим стратегии Passport для обработки авторизации и регистрации. Каждая из них будет экземпляром стратегии локальной аутентификации Passport и будет создаваться при помощи функции passport.use().
Мы используем connect-flash, что поможет нам в обработке ошибок, предоставляя флэш-сообщения, которые могут выводиться пользователю при возникновении ошибки.
Стратегия авторизации выглядит следующим образом:
// passport/login.js
passport.use('login', new LocalStrategy({
passReqToCallback : true
},
function(req, username, password, done) {
// проверка в mongo, существует ли пользователь с таким логином
User.findOne({ 'username' : username },
function(err, user) {
// В случае возникновения любой ошибки, возврат с помощью метода done
if (err)
return done(err);
// Пользователь не существует, ошибка входа и перенаправление обратно
if (!user){
console.log('User Not Found with username ' username);
return done(null, false,
req.flash('message', 'User Not found.'));
}
// Пользователь существует, но пароль введен неверно, ошибка входа
if (!isValidPassword(user, password)){
console.log('Invalid Password');
return done(null, false,
req.flash('message', 'Invalid Password'));
}
// Пользователь существует и пароль верен, возврат пользователя из
// метода done, что будет означать успешную аутентификацию
return done(null, user);
}
);
}));
Первый параметр passport.use() является именем стратегии, которое будет использоваться для идентификации этой стратегии при последующем применении. Вторым параметром является тип стратегии, которую вы хотите создать, здесь мы используем username-password или LocalStrategy.
Следует отметить, что по умолчанию LocalStrategy ищет учетные данные пользователя в параметрах username и password, но мы можем также использовать любые другие проименованные параметры.
Переменная конфигурации passReqToCallback позволяет нам получить доступ к объекту request в функции обратного вызова, благодаря чему, в свою очередь, мы имеем возможность использовать любой параметр, связанный с запросом.
Далее, мы используем Mongoose API, чтобы найти пользователя в нашей основной базе пользователей и проверить, является ли он доверенным пользователем или нет.
Последний параметр в нашем обратном вызове done указывает на используемый метод, с помощью которого мы сообщаем модулю Passport об успешном выполнении действия или ошибке.
Чтобы идентифицировать сбой либо первый параметр должен содержать ошибку, либо второй параметр должен содержать значение false. Для обозначения успешного прохождения действия первый параметр должен иметь значение null, а второй – truthy, в этом случае объект request становится доступен.
Поскольку пароли по своей природе являются уязвимым местом, мы всегда должны шифровать их перед сохранением в базу данных. Для этого мы используем bcrypt-nodejs, который помогает шифровать и расшифровывать пароли:
Если вам неудобно работать с отдельными фрагментами кода, и вы предпочитают видеть полный код в действии, вы можете просмотреть его здесь.
Теперь мы определяем следующую стратегию, которая будет обрабатывать регистрацию нового пользователя и создавать его учетную запись в основной базе данных Mongo DB:
passport.use('signup', new LocalStrategy({
passReqToCallback : true
},
function(req, username, password, done) {
findOrCreateUser = function(){
// поиск пользователя в Mongo с помощью предоставленного имени пользователя
User.findOne({'username':username},function(err, user) {
// В случае любых ошибок - возврат
if (err){
console.log('Error in SignUp: ' err);
return done(err);
}
// уже существует
if (user) {
console.log('User already exists');
return done(null, false,
req.flash('message','User Already Exists'));
} else {
// если пользователя с таки адресом электронной почты
// в базе не существует, создать пользователя
var newUser = new User();
// установка локальных прав доступа пользователя
newUser.username = username;
newUser.password = createHash(password);
newUser.email = req.param('email');
newUser.firstName = req.param('firstName');
newUser.lastName = req.param('lastName');
// сохранения пользователя
newUser.save(function(err) {
if (err){
console.log('Error in Saving user: ' err);
throw err;
}
console.log('User Registration succesful');
return done(null, newUser);
});
}
});
};
// Отложить исполнение findOrCreateUser и выполнить
// метод на следующем этапе цикла события
process.nextTick(findOrCreateUser);
});
);
Здесь мы снова использовали Mongoose API, чтобы выяснить, существует ли пользователь с данным именем пользователя или нет. Если нет, то создаем нового пользователя и сохраняем информацию о нем в Mongo.
В противном случае возвращаем ошибку с помощью обратного вызова done и флэш-сообщения. Обратите внимание, что мы используем bcrypt-nodejs для создания хэша пароля перед его сохранением:
В общем, схема нашего приложения будет выглядеть так:
Теперь мы задаем маршруты для применения в следующем модуле, который принимает экземпляр паспорта созданного ранее в app.js. Сохраните этот модуль в файле routes/index.js:
module.exports = function(passport){
/* Получение страницы авторизации. */
router.get('/', function(req, res) {
// Вывод страницы авторизации со всеми флэш-сообщениями, если
// таковые существуют
res.render('index', { message: req.flash('message') });
});
/* Обработка POST-данных авторизации */
router.post('/login', passport.authenticate('login', {
successRedirect: '/home',
failureRedirect: '/',
failureFlash : true
}));
/* Получение страницы регистрации */
router.get('/signup', function(req, res){
res.render('register',{message: req.flash('message')});
});
/* Обработка регистрационных POST-данных */
router.post('/signup', passport.authenticate('signup', {
successRedirect: '/home',
failureRedirect: '/signup',
failureFlash : true
}));
return router;
}
Наиболее важной частью приведенного выше фрагмента кода является использование passport.authenticate() для делегирования аутентификации стратегиям login и signup, когда HTTP POST выполнен для маршрутов /login и /signup соответственно.
Обратите внимание, что не обязательно называть стратегии соответственно пути маршрута, им можно назначать произвольные имена.
Далее, мы создаем следующие два представления для нашего приложения:
extends layout
block content
div.container
div.row
div.col-sm-6.col-md-4.col-md-offset-4
h1.text-center.login-title Sign in to our Passport app
div.account-wall
img(class='profile-img', src='https://lh5.googleusercontent.com/-b0-k99FZlyE/AAAAAAAAAAI/AAAAAAAAAAA/eu7opA4byxI/photo.jpg?sz=120')
form(class='form-signin', action='/login', method='POST')
input(type='text', name='username' class='form-control', placeholder='Email',required, autofocus)
input(type='password', name='password' class='form-control', placeholder='Password', required)
button(class='btn btn-lg btn-primary btn-block', type='submit') Sign in
span.clearfix
a(href='/signup', class='text-center new-account') Create an account
#message
if message
h1.text-center.error-message #{message}
Благодаря Bootstrap наша страница авторизации теперь выглядит следующим образом:
Нам также потребуются еще два представления для ввода регистрационных данных и для домашней страницы приложения:
Если вы не работали с Jade, здесь можете найти документацию по нему.
Passport, будучи связующим софтом, позволяет добавлять определенные свойства и методы к объектам запросов и ответов, что в свою очередь дает возможность добавить очень удобный метод request.logout(), который отменяет сеанс пользователя:
Passport также предоставляет возможность защитить доступ к маршруту, который не предназначен для анонимных пользователей.
Это означает, что если некоторые пользователи пытается получить доступ к маршруту http://localhost:3000/home без авторизации в системе, они будут перенаправлены на главную страницу следующим образом:
Passport не является единственным возможным решением, когда речь заходит об аутентификации приложений Node.js, существуют и альтернативные варианты, такие как EveryAuth.
Но большое количество модулей, гибкость, поддержка сообщества и тот факт, что он является просто связующим программным обеспечением, действительно выделяет Passport из череды других приложений.
Вход пользователей в систему
Вот как выглядит схема действий, выполняемых в том случае, когда пользователь пытается войти в систему.
Вход пользователя в систему
Вот что происходит при входе пользователя в систему:
- Клиент отправляет серверу комбинацию, состоящую из публичного идентификатора и приватного ключа пользователя. Обычно это — адрес электронной почты и пароль.
- Сервер ищет пользователя в базе данных по адресу электронной почты.
- Если пользователь существует в базе данных — сервер хэширует отправленный ему пароль и сравнивает то, что получилось, с хэшем пароля, сохранённым в базе данных.
- Если проверка оказывается успешной — сервер генерирует так называемый токен или маркер аутентификации — JSON Web Token (JWT).
JWT — это временный ключ. Клиент должен отправлять этот ключ серверу с каждым запросом к аутентифицированной конечной точке.
Генерирование jwt в node.js
Давайте создадим функцию
generateToken
, которая нужна нам для завершения работы над сервисом аутентификации пользователей.
Создавать JWT можно с помощью библиотеки jsonwebtoken. Найти эту библиотеку можно в npm.
Защита конечных точек и проверка jwt
Теперь клиентскому коду нужно отправлять JWT в каждом запросе к защищённой конечной точке.
Рекомендуется включать JWT в заголовки запросов. Обычно их включают в заголовок Authorization.
Заголовок Authorization
Теперь, на сервере, нужно создать код, представляющий собой промежуточное ПО для маршрутов express. Поместим этот код в файл isAuth.ts:
Использование jwt с express
В этом уроке мы создадим простое веб-приложение на основе микросервиса для управления книгами в библиотеке с двумя сервисами. Один сервис будет отвечать за аутентификацию пользователей, а другой – за управление книгами.
Будет два типа пользователей – администраторы и члены библиотеки. Администраторы смогут просматривать и добавлять новые книги, в то время как пользователи смогут только просматривать их. В идеале они также смогут редактировать или удалять книги. Но чтобы максимально упростить эту статью, мы не будем вдаваться в такие подробности.
Чтобы начать работу, в терминале инициализируйте пустой проект Node.js с настройками по умолчанию:
Затем установим фреймворк Express:
Как имперсонировать пользователя?
Имперсонация пользователей — это техника, используемая для входа в систему под видом некоего конкретного пользователя без знания его пароля.
Эта возможность весьма полезна для супер-администраторов, разработчиков или сотрудников служб поддержки. Имперсонация позволяет им решать проблемы, которые проявляются только в ходе работы пользователей с системой.
Работать с приложением от имени пользователя можно и не зная его пароля. Для этого достаточно сгенерировать JWT с правильной подписью и с необходимыми метаданными, описывающими пользователя.
Создадим конечную точку, которая может генерировать токены для входа в систему под видом конкретных пользователей. Этой конечной точкой сможет пользоваться только супер-администратор системы.
Для чала нам нужно назначить этому пользователю роль, с которой связан более высокий, чем у других пользователей, уровень привилегий. Это можно сделать множеством различных способов. Например, достаточно просто добавить поле role в сведения о пользователе, хранящиеся в базе данных.
Выглядеть это может так, как показано ниже.
Новое поле в сведениях о пользователе
Клиентская часть:
Тут я просто объясню что делать на стороне клиента, но я не буду показывать сам код, это как нибудь в другой статье, тут только объяснения.
Для начала полученный токен записываем в localStorage, делаем это примерно так:
После чего при запросе к серверу передаём наш токен, примерно так это должно выглядеть:
Как видите здесь мы в header или в заголовки запроса добавили параметр Authorization, который хранит токен, но перед ним ещё ставим слово Bearer, и после пробела, уже сам токен.
Обновление токена
На данном этапе наше приложение обрабатывает аутентификацию и авторизацию для книжного сервиса, хотя в дизайне есть серьезный недостаток – срок действия JWT-токена никогда не истекает.
Если этот токен украдут, то доступ к учетной записи будет у них навсегда, а реальный пользователь не сможет отозвать доступ.
Чтобы устранить эту возможность, давайте обновим обработчик запроса на вход, чтобы срок действия токена истекал через определенный период. Мы можем сделать это, передав свойство expiresIn в качестве опции для подписи JWT.
Когда мы истекает срок действия токена, у нас также должна быть стратегия генерации нового токена на случай истечения срока действия. Для этого мы создадим отдельный JWT-токен, называемый refresh-токеном, который можно использовать для генерации нового.
Сначала создайте секрет маркера обновления и пустой массив для хранения маркеров обновления:
Когда пользователь входит в систему, вместо генерации одного токена генерируют как токены обновления, так и аутентификации:
app.post('/login', (req, res) => { // read username and password from request body const { username, password } = req.body; // filter user from the users array by username and password const user = users.find(u => { return u.username === username && u.password === password }); if (user) { // generate an access token const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' }); const refreshToken = jwt.sign({ username: user.username, role: user.role }, refreshTokenSecret); refreshTokens.push(refreshToken); res.json({ accessToken, refreshToken }); } else { res.send('Username or password incorrect'); } });
А теперь давайте создадим обработчик запроса, который генерирует новые токены на основе обновленных токенов:
Но и с этим есть проблема. Если токен refresh будет украден у пользователя, кто-то может использовать его для генерации любого количества новых токенов.
Чтобы избежать этого, давайте реализуем простую функцию выхода из системы:
Когда пользователь запрашивает выйти из системы, мы удалите токен обновления из нашего массива. Он гарантирует, чКогда пользователь попросит выйти из системы, мы удалим маркер обновления из нашего массива. Это гарантирует, что когда пользователь выйдет из системы, никто не сможет использовать маркер обновления для создания нового маркера аутентификации.
Объединенная сессия
Отдельной проблемой стала необходимость получения актуального состояния сессии и возможность его изменения в событиях веб-сокетов. Для этого идеально подошел пакет – express-socket.io-session. Правда, пришлось поколдовать над её подключением:
Изменили подключение сессии и настройки кук:
this.store = new pgSession({
pool: pgPool,
tableName: SESSION_TABLE
});
this.session = expressSession({
name: SESSION_KEY,
secret: SESSION.secret,
resave: false, // важно, для того, чтобы сессия не перезаписывалась на каждый чих
rolling: true,
saveUninitialized: true, // нужно для выдачи куки даже неавторизированному пользователю
proxy: true,
cookie: {
secure: true, // обязывает производить передачу по ssl
maxAge: SESSION_DURATION,
sameSite: 'none' // чтобы можно было отдавать на разные поддомены
}
store: this.store
});
Мы написали обработчики сессии таким образом, чтобы сессия подгружалась до начала обработки события, и сохранялась, если необходимо:
const asyncHandlerExtended = (fn, socket) => (data) => {
const cb = async () => {
await reloadSession(socket.handshake.session);
await fn({ socket, data });
await saveSession(socket.handshake.session);
};
return Promise.resolve(cb()).catch((err) => {
socket.emit('error', err);
});
};
Собрали все вместе при настройке сокетов:
import sharedSession from 'express-socket.io-session';
import io from 'socket.io';
const resultSocket = nameSpace ? this.io.of(nameSpace) : this.io;
resultSocket.use(sharedSession(session, { autoSave: true }));
Преимущество использования jwt перед традиционными методами
Как мы уже говорили ранее, JWT может содержать всю информацию о самом пользователе, в отличие от аутентификации на основе сеанса.
Это очень полезно для масштабирования веб-приложений, например, веб-приложений с микросервисами. Сегодня архитектура современного веб-приложения выглядит примерно так:
Все эти сервисы могут быть одним и тем же сервисом, который будет перенаправляться балансировщиком нагрузки в соответствии с использованием ресурсов (CPU или Memory Usage) каждого сервера, или некоторыми различными сервисами, такими как аутентификация и т.д.
Если мы используем традиционные методы авторизации, такие как cookies, нам придется использовать общую базу данных, например Redis, для обмена сложной информацией между серверами или внутренними службами. Но если мы поделимся секретом между микросервисами, мы можем просто использовать JWT, и тогда для авторизации пользователей не потребуется никаких других внешних ресурсов.
Проверка токена:
Последние что осталось рассмотреть, так это как сделать проверку токена на стороне сервера, для этого первым делом создадим папку «middleware», где будем хранить middleware функции, в неё создаём файл «authJwtMiddleware.js», и вот что в нём пишем:
В начале мы импортируем JwtStrategy и ExtractJwt, первый нам нужен будет для получения пользователя по токену, а второй для проверки что правильно передался токен.
Последние импортируем mongoose, он нужен будет для получения пользователя, следующие создаём настройки получения токена, первый параметр это jwtFromRequest, он обозначает откуда берём токен, и как раз тут и используем ExtractJwt, второй параметр это secretOrKey, это строка для дешифрование наше токена, как вы помните выше, мы использовали для шифрования слово «Hello», это значит что здесь используем его же.
Следующие возвращаем функцию которая будет обрабатывать JWT токен, внутри неё используем passport.use() или middleware функцию, которая принимает в себя создание нового объекта класса JwtStrategy, при создание он принимает наши настройки options и асинхронную callback функцию, которая в свою очередь принимает payload, в которой храниться всё что мы получили из токена, и функцию done, которая нужна для подтверждения, что всё прошло успешно или нет.
Внутри callback функции так же отлавливаем ошибки. В начале мы получаем модуль пользователя, и находим пользователя по id, который мы получили из payload, последние проверяем, получили мы пользователя или нет, если до, то возвращаем его, если нет, то возвращаем false.
дальше переходим в папку «routers», и там заходим в любой файл, где есть путь в котором нам надо проверять токен, для примера я возьму случайный путь, вот что я напишу:
В начале мы импортируем passport, потом при GET запросе мы используем метод passport.authenticate(), в качестве первого параметра он принимает тип аунтификации, у нас это JWT, второй объект с настройкам, но мы используем только один, это session: false, то есть не использование сессий, они нам нам нужны.
Последние что осталось сделать, так это добавить не много кода в «index.js», заходим в него и вот что добавляем:
В начале мы импортируем passport, потом инициализируем его, последние импортируем нашу middleware функцию для проверки токена и сразу запускаем passport, вот и всё, если вы сделали всё правильно, у вас должно всё работать.
Регистрация пользователей в системе
Когда в системе создаётся новый пользователь, его пароль необходимо хэшировать и сохранить в базе данных. Пароль в базе сохраняют вместе с адресом электронной почты и другими сведениями о пользователе (например, среди них может быть профиль пользователя, время регистрации и так далее).
Сервис книг
После этого давайте создадим файл books.js для нашего сервиса книг.
Начнем с импорта необходимых библиотек и настройки приложения Express:
После настройки, чтобы имитировать базу данных, давайте просто создадим массив книг:
const books = [ { "author": "Chinua Achebe", "country": "Nigeria", "language": "English", "pages": 209, "title": "Things Fall Apart", "year": 1958 }, { "author": "Hans Christian Andersen", "country": "Denmark", "language": "Danish", "pages": 784, "title": "Fairy tales", "year": 1836 }, { "author": "Dante Alighieri", "country": "Italy", "language": "Italian", "pages": 928, "title": "The Divine Comedy", "year": 1315 }, ];
Теперь мы можем создать очень простой обработчик запроса для получения всех книг из базы данных:
Поскольку наши книги должны быть видны только аутентифицированным пользователям. Мы должны создать промежуточное ПО для аутентификации.
Перед этим создайте секрет маркера доступа для подписания JWT, как и раньше:
Этот токен должен быть тем же самым, который используется в службе аутентификации. Благодаря тому, что секрет между ними общий, мы можем аутентифицироваться с помощью службы аутентификации, а затем авторизовать пользователей в службе book.
На этом этапе давайте создадим промежуточное ПО Express, которое будет обрабатывать процесс аутентификации:
В этом промежуточном ПО мы считываем значение заголовка authorization. Поскольку заголовок авторизации имеет значение в формате Bearer [JWT_TOKEN], мы разделили значение пробелом и выделили токен.
Затем мы проверили токен с помощью JWT. После проверки мы присоединяем объект пользователя к запросу и продолжаем. В противном случае мы отправим клиенту ошибку.
Мы можем настроить это промежуточное ПО в обработчике запроса GET следующим образом:
Давайте загрузим сервер и проверим, все ли работает правильно:
Служба аутентификации
Затем создадим файл под названием auth.js, который будет нашей службой аутентификации:
В идеале мы должны использовать базу данных для хранения информации о пользователях. Но для простоты давайте создадим массив пользователей, который мы будем использовать для их аутентификации.
Структура jwt
Давайте поговорим о структуре JWT через токен образец:
Как видно на изображении, в этом JWT есть три раздела, каждый из которых разделен точкой.
Примечание: Кодирование Base64 – это один из способов обеспечения сохранности данных, поскольку оно не сжимает и не шифрует данные, а просто кодирует их в понятном большинству систем виде. Вы можете прочитать любой текст в кодировке Base64, просто декодировав его.
Первый раздел JWT – это заголовок, который представляет собой строку в кодировке Base64. Если вы расшифруете заголовок, он будет выглядеть примерно так:
Раздел заголовка содержит алгоритм хэширования, который был использован для генерации знака и тип токена.
Второй раздел – полезная нагрузка, содержащая объект JSON, который был отправлен пользователю. Поскольку он закодирован только в Base64, его может легко декодировать любой.
Рекомендуется не включать в JWT какие-либо конфиденциальные данные, такие как пароли или личная информация.
Обычно тело JWT выглядит примерно так, хотя это не обязательно соблюдается:
В большинстве случаев свойство sub будет содержать идентификатор пользователя, свойство iat, которое сокращенно называется issued at, – это временная метка, когда токен был выпущен.
Вы также можете увидеть некоторые общие свойства, такие как eat или exp – время истечения срока действия токена.
Последний раздел – это подпись токена. Она генерируется путем хэширования строки base64UrlEncode(header) “.”. base64UrlEncode(payload) secret с использованием алгоритма, указанного в разделе заголовка.
Секрет – это случайная строка, которую должен знать только сервер. Ни один хэш не может быть преобразован обратно в исходный текст, и даже небольшое изменение исходной строки приведет к другому хэшу. Таким образом, секрет не может быть подвергнут обратному инжинирингу.
Когда эта подпись отправляется обратно на сервер, он может проверить, что клиент не изменил никаких деталей в объекте.
Требования к проекту
Вот требования к проекту, которым мы будем здесь заниматься:
- Наличие базы данных, в которой будет храниться адрес электронной почты пользователя и его пароль, либо — clientId и clientSecret, либо — нечто вроде комбинации из приватного и публичного ключей.
- Использование сильного и эффективного криптографического алгоритма для шифрования пароля.
В тот момент, когда я пишу этот материал, я считаю, что лучшим из существующих криптографических алгоритмов является Argon2. Я прошу вас не использовать простые криптографические алгоритмы вроде SHA256, SHA512 или MD5.
Кроме того, предлагаю вам взглянуть на этот замечательный материал, в котором вы можете найти подробности о выборе алгоритма для хэширования паролей.
Заключение
В этой статье мы познакомили вас с JWT и тем, как реализовать JWT с помощью Express. Я надеюсь, что теперь у вас есть часть хороших знаний о том, как работает JWT и как реализовать его в вашем проекте.
Как всегда, исходный код доступен на GitHub.
Create express authentication endpoints
🛠️️ In this section, you will create three endpoints that handle the application’s authentication flow:
GET /login
GET /logout
GET /callback
To manage these endpoints better, you will create them within an authentication module and export them through an Express router so that your Express application can use them.
🛠️️ To start, create an auth.js file under the project directory.
touch auth.js
Populate it with this template to define its structure:
🛠️️ Next, add the following under the Required External Modules section to import packages that are needed and load your environmental variables:
const express =require("express");const router = express.Router();const passport =require("passport");const querystring =require("querystring");require("dotenv").config();
Here’s an overview of the new modules you are using:
You’ll soon see how these modules streamline your route controller logic.
🛠️️ The first endpoint you’ll create is the GET/login one. Update the Routes Definitions section of your auth.js file as follows:
router.get("/login",
passport.authenticate("auth0",{
scope:"openid email profile"}),(req, res)=>{
res.redirect("/");});
В заключение
Надеюсь, описанная выше логика работы поможет читателям реализовать достаточно защищенные веб-сервера. На текущий момент в статье приведено мало снипетов кода, так как логика разбросана по разным файлам и частям кода, и собрать ее воедино будет проблематично.
Итоги
Нет ничего плохого в том, чтобы полагаться на сторонние сервисы и библиотеки аутентификации. Это помогает разработчикам экономить время. Но им необходимо ещё и знать о том, на каких принципах основана работа систем аутентификации, о том, что обеспечивают функционирование таких систем.
В этом материале мы исследовали возможности JWT-аутентификации, поговорили о важности выбора хорошего криптографического алгоритма для хэширования паролей. Мы рассмотрели создание механизма имперсонации пользователей.
Сделать то же самое с помощью чего-то вроде passport.js далеко не так просто. Аутентификация — это огромная тема. Возможно, мы к ней ещё вернёмся.
Уважаемые читатели! Как вы создаёте системы аутентификации для своих Node.js-проектов?