ЛУЧШЕЕ ПРОГРАММИРОВАНИЕ

Практическое руководство по аутентификации JWT с помощью NodeJS

Создайте модуль аутентификации для вашего следующего приложения NodeJS

Вы пытались интегрировать аутентификацию JWT в свое приложение Node.js, но так и не нашли правильного решения? Тогда вы попали в нужное место. В этом посте мы рассмотрим более тонкие детали аутентификации JWT в Node.js с использованием пакета npm jsonwebtoken.



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

  • Отправьте токен JWT в файле cookie вместо HTTP-заголовка
  • Установите короткий срок действия токена
  • Используйте токены обновления, чтобы повторно выпустить токены доступа, срок действия которых истекает через короткий промежуток времени.

Прежде чем вдаваться в подробности, я хочу выделить два момента:

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

Теперь, когда у нас есть цели, приступим к реализации.

Начальные приготовления…

Если вы еще этого не сделали, настройте среду Node для этого проекта. Затем установите следующие пакеты, которые мы будем использовать в этом руководстве.

  • Express: фреймворк Node.js, который мы будем использовать
  • Cookie-Parser: поскольку мы будем отправлять токен JWT в файле cookie, используйте этот пакет для анализа файла cookie, отправленного вместе с запросами.
  • Body-Parser: этот пакет для анализа тела входящих запросов для извлечения параметров POST.
  • Dotenv: этот пакет загружает переменные среды из файла .env в среду приложения.
  • Json-Web-Token: это пакет, который помогает нам с реализацией JWT.

Вы можете установить эти пакеты с помощью следующей команды.

npm install express cookie-parser bory-parser dotenv json-web-token --save

Теперь мы настроим серверную часть приложения в главном файле проекта app.js.

require('dotenv').config()
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const app = express()
const {login, refresh} = require('./authentication')
app.use(bodyParser.json())
app.use(cookieParser())
app.post('/login', login)
app.post('/refrsh', refresh)

На следующих шагах мы реализуем функции внутри controller.js, которые мы использовали в приведенном выше коде. Мы используем функцию входа в систему для обработки почтовых запросов, отправленных по маршруту / login, и входа в систему пользователей. Мы используем функцию обновления для обработки почтовых запросов, отправленных по маршруту / refresh, и выпуска новых токенов доступа с использованием токена обновления.

Настроить переменные среды

Перед реализацией логики входа пользователя в систему нам необходимо настроить переменные среды, необходимые для настройки JWT. Создайте файл .env и добавьте эти две переменные, которые мы будем использовать внутри приложения.

ACCESS_TOKEN_SECRET=swsh23hjddnns
ACCESS_TOKEN_LIFE=120
REFRESH_TOKEN_SECRET=dhw782wujnd99ahmmakhanjkajikhiwn2n
REFRESH_TOKEN_LIFE=86400

Вы можете добавить любую строку в качестве секрета. В качестве меры безопасности рекомендуется использовать более длинный секрет со случайными символами. Срок действия созданного нами токена доступа составит 120 секунд. Мы также устанавливаем секрет подписи токена обновления и срок его действия. Обратите внимание на то, что токен обновления имеет более длительный срок службы по сравнению с токеном доступа.

Обработка входа пользователя в систему и создания токена JWT

Теперь мы можем перейти к этапу реализации функции входа в систему, которую мы импортировали в файл app.js для обработки маршрута /login.

Мы собираемся хранить в приложении массив пользовательских объектов для этой реализации. В реальном сценарии вы будете получать эту информацию о пользователе из базы данных или любого другого места. Кроме того, это только для демонстрационных целей, НИКОГДА не храните фактические пароли.

let users = {
    john: {password: "passwordjohn"},
    mary: {password:"passwordmary"}
}

При реализации функции входа в систему сначала нам нужно получить имя пользователя и пароль, отправленные с запросом входа в систему POST.

const jwt = require('json-web-token')
// Never do this!
let users = {
    john: {password: "passwordjohn"},
    mary: {password:"passwordmary"}
}
exports.login = function(req, res){
    let username = req.body.username
    let password = req.body.password
    
    // Neither do this!
    if (!username || !password || users[username] !== password){
        return res.status(401).send()
    }    
}

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

Если клиент отправил правильные учетные данные на сервер, мы переходим к входу пользователя в систему, выпуская новые токены JWT. В этом случае новый входящий в систему пользователь получает два токена: токен доступа и токен обновления. Затем токен доступа вместе с ответом в файле cookie отправляется обратно клиенту. Токен обновления хранится в базе данных для выдачи токенов доступа в будущем. В нашем случае мы будем хранить токен обновления в ранее созданном пользовательском массиве.

const jwt = require('json-web-token')
// Never do this!
let users = {
    john: {password: "passwordjohn"},
    mary: {password:"passwordmary"}
}
exports.login = function(req, res){
    let username = req.body.username
    let password = req.body.password
    
    // Neither do this!
    if (!username || !password || users[username].password !== password){
        return res.status(401).send()
    }
    //use the payload to store information about the user such as username, user role, etc.
    let payload = {username: username}
    //create the access token with the shorter lifespan
    let accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
        algorithm: "HS256",
        expiresIn: process.env.ACCESS_TOKEN_LIFE
    })
    //create the refresh token with the longer lifespan
    let refreshToken = jwt.sign(payload, process.env.REFRESH_TOKEN_LIFE, {
        algorithm: "HS256",
        expiresIn: process.env.REFRESH_TOKEN_LIFE
    })
    //store the refresh token in the user array
    users[username].refreshToken = refreshToken
    //send the access token to the client inside a cookie
    res.cookie("jwt", accessToken, {secure: true, httpOnly: true})
    res.send()
}

При отправке токена доступа внутри файла cookie не забудьте установить флаг httpOnly, чтобы злоумышленники не могли получить доступ к cookie со стороны клиента. Мы также установили безопасный флаг в приведенном выше примере. Однако, если вы пытаетесь использовать этот код только через HTTP-соединение, а не через HTTPS-соединение, удалите флаг безопасности, чтобы отправить его вместе с ответом.

Добавить промежуточное ПО для аутентификации запросов пользователей

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

Давайте создадим новый файл с именем middleware.js и реализуем метод verify, чтобы проверить, аутентифицирован ли пользователь.

Во-первых, мы должны получить токен доступа из файла cookie, отправленного с запросом. Если запрос не содержит токена доступа, он не перейдет к намеченному маршруту и ​​вместо этого вернет ошибку 403 запрещено.

const jwt = require('json-web-token')
exports.verify = function(req, res, next){
    let accessToken = req.cookies.jwt
    //if there is no token stored in cookies, the request is unauthorized
    if (!accessToken){
        return res.status(403).send()
    }
}

Если запрос содержит токен доступа, то сервер проверит, был ли он выдан самим сервером с использованием сохраненного секрета. Если срок действия токена истек или он распознается как не подписанный сервером, метод verify jsonwebtoken выдаст ошибку. Мы можем обработать ошибку, чтобы вернуть клиенту ошибку 401.

const jwt = require('json-web-token')
exports.verify = function(req, res, next){
    let accessToken = req.cookies.jwt
    //if there is no token stored in cookies, the request is unauthorized
    if (!accessToken){
        return res.status(403).send()
    }
    let payload
    try{
        //use the jwt.verify method to verify the access token
        //throws an error if the token has expired or has a invalid signature
        payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET)
        next()
    }
    catch(e){
        //if an error occured return request unauthorized error
        return res.status(401).send()
    }
}

Теперь мы можем использовать это промежуточное ПО для защиты любого маршрута, который требует, чтобы пользователи вошли в систему перед доступом. Импортируйте промежуточное ПО в место, где вы обрабатываете маршруты, в нашем случае это app.js. Если мы пытаемся защитить маршрут с именем / comments, этого легко добиться, добавив промежуточное ПО перед обработчиком маршрута.

const {verify} = require('./middleware')
app.get('/comments', verify, routeHandler)

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

Выпуск нового токена доступа с использованием токена обновления

Помните маршрут / refresh и функцию обновления, которые мы использовали в исходном коде в файле app.js? Теперь мы можем реализовать эту функцию обновления для выпуска новых токенов доступа с использованием сохраненного токена обновления.

Используемая здесь функция, refresh, также находится внутри controller.js файла, который мы использовали ранее для реализации функции входа в систему.

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

exports.refresh = function (req, res){
    let accessToken = req.cookies.jwt
    if (!accessToken){
        return res.status(403).send()
    }
    let payload
    try{
        payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET)
    }
    catch(e){
        return res.status(401).send()
    }
}

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

Если срок действия токена обновления истек или его не удается проверить, сервер вернет несанкционированную ошибку. В противном случае новый токен доступа будет отправлен в cookie.

exports.refresh = function (req, res){
    let accessToken = req.cookies.jwt
    if (!accessToken){
        return res.status(403).send()
    }
    let payload
    try{
        payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET)
     }
    catch(e){
        return res.status(401).send()
    }
    //retrieve the refresh token from the users array
    let refreshToken = users[payload.username].refreshToken
    //verify the refresh token
    try{
        jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET)
    }
    catch(e){
        return res.status(401).send()
    }
    let newToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, 
    {
        algorithm: "HS256",
        expiresIn: process.env.ACCESS_TOKEN_LIFE
    })
    res.cookie("jwt", newToken, {secure: true, httpOnly: true})
    res.send()
}

На этом этапе наша реализация аутентификации JWT с помощью Node.js завершается.

Резюме

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

Спасибо за прочтение!