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

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

В этом руководстве мы рассмотрим другой подход к аутентификации (а также управлению доступом, SSO и т.д.) в React-приложениях.

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

Github-репозиторий примера: github.com/userfront/react-example

Аутентификация в React


Обычно аутентификации в React-приложении выглядит следующим образом: приложение делает запрос к серверу аутентификации, который возвращает токен доступа (access token), затем этот токен сохраняется в браузере и может быть использован при последующих запросах к вашему серверу (или другим серверам, если это необходимо).

Будь то стандартная аутентификация по логину (электронной почте) и паролю или через магические ссылки (magic links) и технологии единого входа (SSO) такие как Google, Azure и Facebook, мы хотим, чтобы наше React-приложение отправляло первоначальный запрос на сервер аутентификации и чтобы этот сервер, в свою очередь, генерировал токен.

Задачи React

  • Отправка первоначального запроса на сервер аутентификации
  • Получение и хранение токена доступа
  • Отправка токена доступа на ваш сервер при каждом последующем запросе.


JWT (JSON Web Tokens)


JSON Web Tokens (JWT) — это компактные и безопасные токены, которые можно использовать для аутентификации и управления доступом в React-приложениях.

Каждый JWT содержит в себе простой JSON-объект, называемый «полезной нагрузкой» (payload), и подписывается специальным образом, чтобы ваш сервер мог удостовериться в подлинности его полезной нагрузки.

Важно отметить, что эта полезная нагрузка JWT может быть прочитана кем угодно: и вашим React-приложением, и любыми третьими лицами, которые видят токен.

Токен доступа JWT выглядит следующим образом:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjRjMDUxZTg3LTU2ZWEtNGUzNC05ZmE3LThkYWNkOThkOWUzMyJ9.eyJtb2RlIjoidGVzdCIsInRlbmFudElkIjoiZGVtbzEyMzQiLCJ1c2VySWQiOjEsInVzZXJVdWlkIjoiNGU2ODgwZTMtZDU0YS00NTNlLTgwMDQtMmJmMGNjNWM1MmY2IiwiaXNDb25maXJtZWQiOnRydWUsImF1dGhvcml6YXRpb24iOnsiZGVtbzEyMzQiOnsicm9sZXMiOlsiY29udHJpYnV0b3IiLCJ2aWV3ZXIiLCJhZG1pbiJdfX0sImlzcyI6InVzZXJmcm9udCIsInNlc3Npb25JZCI6IjNlYTUxOTNjLWE3ZWEtNGM1Zi05ZmRlLTZhZjk0ZTgyNGQ3NCIsImlhdCI6MTY1ODUxNzc2OSwiZXhwIjoxNjU5MTIyNTY5fQ.LB7IyP09G6f64Ho0CdjA8xDe-s7KAjJGPe5lC9GRwSrWHZSnvmoMb1HTRkrO1oabDcpMOvDmyjjkK0Nb-0RoK-bKX3i5PPtvKGu18VKgShxgg0bXYsA-HRrh8bsOExXhKO_I7gS7GVGCMDlSlbGFDRVdtqSqv1rfKj0pRP9PScFjuB9bbdGlmJXbQSw8V_zRxPYEUOnoTqW7-vfyn3DONbo0cElm1mRVMZzK1W8-Kpg6MRTt7nlMj60ysoBktM4w6KdOlDTmlyLzy6kjkqSylT_pDk0ALWW2tCRV8qZcrzJhWu-g8x6MIEUCo8TArELdl6aUGLdItVi-WmQCZNwajQ

Полезной нагрузкой в этом JWT является средняя часть (отделенная точками):

eyJtb2RlIjoidGVzdCIsInRlbmFudElkIjoiZGVtbzEyMzQiLCJ1c2VySWQiOjEsInVzZXJVdWlkIjoiNGU2ODgwZTMtZDU0YS00NTNlLTgwMDQtMmJmMGNjNWM1MmY2IiwiaXNDb25maXJtZWQiOnRydWUsImF1dGhvcml6YXRpb24iOnsiZGVtbzEyMzQiOnsicm9sZXMiOlsiY29udHJpYnV0b3IiLCJ2aWV3ZXIiLCJhZG1pbiJdfX0sImlzcyI6InVzZXJmcm9udCIsInNlc3Npb25JZCI6IjNlYTUxOTNjLWE3ZWEtNGM1Zi05ZmRlLTZhZjk0ZTgyNGQ3NCIsImlhdCI6MTY1ODUxNzc2OSwiZXhwIjoxNjU5MTIyNTY5fQ

JSON-объект в полезной нагрузке JWT зашифрован в base64:

JSON.parse(atob("eyJtb2RlIjoidGVzdCIsInRlbmFudElkIjoiZGVtbzEyMzQiLCJ1c2VySWQiOjEsInVzZXJVdWlkIjoiNGU2ODgwZTMtZDU0YS00NTNlLTgwMDQtMmJmMGNjNWM1MmY2IiwiaXNDb25maXJtZWQiOnRydWUsImF1dGhvcml6YXRpb24iOnsiZGVtbzEyMzQiOnsicm9sZXMiOlsiY29udHJpYnV0b3IiLCJ2aWV3ZXIiLCJhZG1pbiJdfX0sImlzcyI6InVzZXJmcm9udCIsInNlc3Npb25JZCI6IjNlYTUxOTNjLWE3ZWEtNGM1Zi05ZmRlLTZhZjk0ZTgyNGQ3NCIsImlhdCI6MTY1ODUxNzc2OSwiZXhwIjoxNjU5MTIyNTY5fQ"));


/**
* {
*   "userId": 1,
*   "userUuid": "4e6880e3-d54a-453e-8004-2bf0cc5c52f6",
*   "isEmailConfirmed": true,
*   ...
* }
*/


Токены доступа JWT


Благодаря современной криптосистеме RSA сервер аутентификации способен генерировать защищенные JWT, которые любая другая машина может прочитать и проверить с помощью публичного (открытого) ключа.

Ваш бэкенд-сервер может подтвердить подлинность токена доступа JWT, сверив его с публичным ключом.

Это позволяет бэкенд-серверу отклонять любые токены доступа JWT, которые не были сгенерированы сервером аутентификации (или срок действия которых истек).

JWT в React-приложении:
  • Ваше React-приложение запрашивает токен доступа JWT всякий раз, когда пользователь хочет войти в систему.
  • Сервер аутентификации генерирует токен доступа JWT на основе приватного (закрытого ключа) и затем отправляет его обратно в React-приложение.
  • React-приложение хранит этот токен доступа JWT и отправляет его на бэкенд-сервер всякий раз, когда пользователю необходимо сделать запрос.
  • Ваш бэкенд-сервер проверяет токен доступа JWT с помощью публичного ключа, а затем считывает полезную нагрузку, чтобы определить, какой пользователь выполняет запрос.

Примечание:
Все эти шаги на бумаге выглядят достаточно просто, но каждый из них имеет свой набор «подводных камней», на которые вы обязательно наткнетесь, когда вам нужно будет реализовать их на практике и обеспечить безопасность. Особенно когда с течением времени появляются новые источники угроз и требуется исправление или поддержка новых платформ — вам всегда будет нужно совершать какие-нибудь действия, направленные на обеспечение безопасности.

Userfront устраняет всю эту мороку с авторизацией в React-приложениях


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

Это значительно упрощает работу с аутентификацией в React-приложениях и, что, пожалуй, наиболее важно, берет на себя заботу об актуальности всех протоколов аутентификации.

Настройка аутентификации в React


Теперь мы рассмотрим процесс создания всех основных аспектов аутентификации в React-приложении.

Для создания приложения мы будем использовать Create React App, а для маршрутизации на стороне клиента — React Router.

GitHub репозиторий примера: github.com/userfront/react-example

Оборот токенов доступа JWT


На высоком уровне ответственность React по аутентификации заключается в следующем:

  • Отправка первоначального запроса в Userfront для получения токена доступа JWT. Это делают формы регистрации и логина в систему.
  • Отправка токена доступа JWT на ваш сервер при каждом последующем запросе.



Установка Create React App


Чтобы начать работу с React, нам нужно установить Create React App и React Router.

npx create-react-app my-app
cd my-app
npm install react-router-dom --save
npm start

Теперь наше React-приложение доступно по адресу localhost:3000.

Как сказано на превью, мы можем начать с редактирования файла src/App.js.

Превью


Маршрутизация


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

Замените содержимое файла src/App.js на следующий код (который был взят из руководства React Router):

// src/App.js

import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/login">Login</Link>
            </li>
            <li>
              <Link to="/reset">Reset</Link>
            </li>
            <li>
              <Link to="/dashboard">Dashboard</Link>
            </li>
          </ul>
        </nav>

        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/reset" element={<PasswordReset />} />
          <Route path="/dashboard" element={<DefaultLayout />} />
        </Routes>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function Login() {
  return <h2>Login</h2>;
}

function PasswordReset() {
  return <h2>Password Reset</h2>;
}

function Dashboard() {
  return <h2>Dashboard</h2>;
}

Мы создали маршруты и готовы заняться непосредственно аутентификацией.

Превью


Регистрация, логин и сброс пароля


Начнем мы с того, что добавим на главную страницу форму для регистрации.

Вы можете найти инструкцию созданию формы регистрации в разделе Toolkit на вашей панели управления.

Она будет выглядеть следующим образом:

Userfront Toolkit



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

Следуя инструкциям, установите Userfront Toolkit для React с помощью следующей команды:

npm install @userfront/toolkit --save
npm start

Затем добавьте форму регистрации на главную страницу, импортировав и инициализировав Userfront, и обновив функцию Home(), чтобы она отображала форму:

// src/App.js

import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import Userfront, { SignupForm } from "@userfront/toolkit/react";

Userfront.init("demo1234");

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/login">Login</Link>
            </li>
            <li>
              <Link to="/reset">Reset</Link>
            </li>
            <li>
              <Link to="/dashboard">Dashboard</Link>
            </li>
          </ul>
        </nav>

        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/reset" element={<PasswordReset />} />
          <Route path="/dashboard" element={<DefaultLayout />} />
        </Routes>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
      <SignupForm />
    </div>
  );
}

function Login() {
  return <h2>Login</h2>;
}

function PasswordReset() {
  return <h2>Password Reset</h2>;
}

function Dashboard() {
  return <h2>Dashboard</h2>;
}

Теперь форма регистрации размещена на главной странице. Давайте попробуем зарегистрировать нового пользователя.

Превью


Тестовый режим


По умолчанию форма находится в «Тестовом режиме» (Test mode), позволяющем создавать учетные данные пользователей в тестовой среде, которую можно отдельно просмотреть на панели управления Userfront.



Формы логина и сброса пароля мы можем добавить тем же способом, что и форму регистрации. Чтобы позволить пользователю выйти из системы, мы можем вызвать встроенный метод Userfront.logout.

Нам нужно сделать следующие изменения в файле src/App.js:
  • Добавить в метод Login() возврат формы входа в систему.
  • Добавить в метод PasswordReset() возврат формы сброса пароля.
  • Добавить в метод Dashboard() отображение данных пользователя и кнопки выхода из системы.

Обратите внимание на toolId для каждой формы в вашем Toolkit. Приведенные в данном примере кода toolId не подойдут для вашего приложения.

// src/App.js

import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import Userfront, {
  SignupForm,
  LoginForm,
  PasswordResetForm
} from "@userfront/toolkit/react";

Userfront.init("demo1234");

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/login">Login</Link>
            </li>
            <li>
              <Link to="/reset">Reset</Link>
            </li>
            <li>
              <Link to="/dashboard">Dashboard</Link>
            </li>
          </ul>
        </nav>

        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/reset" element={<PasswordReset />} />
          <Route path="/dashboard" element={<DefaultLayout />} />
        </Routes>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
      <SignupForm />
    </div>
  );
}

function Login() {
  return (
    <div>
      <h2>Login</h2>
      <LoginForm />
    </div>
  );
}

function PasswordReset() {
  return (
    <div>
      <h2>Password Reset</h2>
      <PasswordResetForm />
    </div>
  );
}

function Dashboard() {
  const userData = JSON.stringify(Userfront.user, null, 2);
  return (
    <div>
      <h2>Dashboard</h2>
      <pre>{userData}</pre>
      <button onClick={Userfront.logout}>Logout</button>
    </div>
  );
}

На этом этапе регистрация, вход в систему и сброс пароля должны уже быть вполне работоспособны. Следует отметить, что форма входа на странице /login будет автоматически перенаправлять на страницу /dashboard, если вы уже вошли в систему.

Теперь пользователи могут регистрироваться, логиниться, разлогиниваться и сбрасывать пароль.

Превью


Защищенный маршрут в React


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

Если пользователь не вошел в систему, но пытается посетить /dashboard, мы можем перенаправить его на экран входа в систему.

Чтобы это реализовать мы можем обернуть компонент <DefaultLayout /> компонентом , который проверяет, вошел ли пользователь в систему. Если пользователь залогинился, его токен доступа будет доступен в виде Userfront.tokens.accessToken, поэтому нам достаточно просто проверить его наличие.

Для перенаправления браузера в случае отсутствия токена доступа компонент RequireAuth использует Navigate и useLocation из React Router.

// src/App.js

import React from "react";
import {
  BrowserRouter as Router,
  Routes,
  Route,
  Link,
  Navigate,
  useLocation,
} from "react-router-dom";
import Userfront, {
  SignupForm,
  LoginForm,
  PasswordResetForm
} from "@userfront/toolkit/react";

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/login">Login</Link>
            </li>
            <li>
              <Link to="/reset">Reset</Link>
            </li>
            <li>
              <Link to="/dashboard">Dashboard</Link>
            </li>
          </ul>
        </nav>

        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/reset" element={<PasswordReset />} />
          <Route
            path="/dashboard"
            element={
              <RequireAuth>
                <DefaultLayout />
              </RequireAuth>
            }
          />
        </Routes>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
      <SignupForm />
    </div>
  );
}

function Login() {
  return (
    <div>
      <h2>Login</h2>
      <LoginForm />
    </div>
  );
}

function PasswordReset() {
  return (
    <div>
      <h2>Password Reset</h2>
      <PasswordResetForm />
    </div>
  );
}

function Dashboard() {
  const userData = JSON.stringify(Userfront.user, null, 2);
  return (
    <div>
      <h2>Dashboard</h2>
      <pre>{userData}</pre>
      <button onClick={Userfront.logout}>Logout</button>
    </div>
  );
}

function RequireAuth({ children }) {
  let location = useLocation();
  if (!Userfront.tokens.accessToken) {
    // Redirect to the /login page
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

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

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

Превью


Аутентификация через API в React


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

Библиотек для чтения и проверки JWT достаточно большое количество практически на каждом языке. В списке ниже приведены некоторые популярные библиотеки для работы с JWT.

JWT-библиотеки для:


Ваше React-приложение может передавать токен доступа JWT в виде Bearer-токена внутри заголовка Authorization. Например:

async function getInfo() {
  const res = await window.fetch("/your-endpoint", {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${Userfront.tokens.accessToken}`,
    },
  });

  console.log(res);
}

getInfo();

Перед тем как обработать такой запрос, бэкенд должен сначала считать токен доступа JWT из заголовка Authorization и проверить его корректность с помощью публичного ключа JWT, который можно найти в панели управления Userfront.

Ниже приведен пример Node.js middleware для чтения и проверки токена доступа JWT:

// Пример Node.js (Express.js)

const jwt = require("jsonwebtoken");

function authenticateToken(req, res, next) {
  // Считываем токен доступа JWT из заголовка запроса
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];
  if (token == null) return res.sendStatus(401); // Возвращаем 401 при отсутствии токена

  // Проверяем токен с помощью публичного ключа Userfront
  jwt.verify(token, process.env.USERFRONT_PUBLIC_KEY, (err, auth) => {
    if (err) return res.sendStatus(403); // Возвращаем 403 при непройденной верификации
    req.auth = auth;
    next();
  });
}

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

console.log(req.auth);

// =>
{
  mode: 'test',
  tenantId: 'demo1234',
  userId: 1,
  userUuid: 'ab53dbdc-bb1a-4d4d-9edf-683a6ca3f609',
  isEmailConfirmed: false,
  authorization: {
    demo1234: {
      roles: ["admin"]
    },
  },
  sessionId: '35d0bf4a-912c-4429-9886-cd65a4844a4f',
  iat: 1614114057,
  exp: 1616706057
}

На основе этой информации можно выполнить дополнительные проверки или использовать userId или userUuid для поиска данных, связанных с пользователем.

Например, если необходимо сделать маршрут доступным только пользователям-администраторам, можно проверять объект authorization из валидного токена доступа:

// Пример Node.js (Express.js)

app.get("/users", (req, res) => {
  const authorization = req.auth.authorization["demo1234"] || {};

  if (authorization.roles.includes("admin")) {
    // Разрешаем доступ
  } else {
    // Запрещаем доступ
  }
});

React SSO (технологии единого входа)


Также мы можем добавить в React-приложение социальных поставщиков идентификационных данных, таких как Google, Facebook и LinkedIn, или бизнес поставщиков идентификации, таких как Azure AD, Office365 и т.д.

Для этого необходимо создать приложение с поставщиком идентификации (например, Google), а затем добавить учетные данные SSO в панель управления Userfront. В результате мы получаем модифицированную систему логина.

Для реализации Single Sign On с помощью этого подхода не требуется никакого дополнительного кода: можно добавлять и удалять поставщиеов, не изменяя формы или способ обработки токенов доступа JWT.

Заключение


Добавление аутентификации и контроля доступа в React-приложение не обязательно должно быть сложной задачей. И этап настройки, и, что еще важнее, сопровождение в течение долгого времени — все это можно переложить на современные платформы, такие как Userfront.

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

Более подробную информацию о добавлении аутентификации в React-приложение можно найти в руководстве Userfront, где описано все: от настройки форм аутентификации до документации по API, примеры репозиториев, работа с различными языками и фреймворками и т.д.

image

Комментарии (1)


  1. GreaterGlider
    07.09.2023 11:33

    Спасибо за столь подробную статью, но сейчас, в 2023, я бы взял next-auth для решения задачи аутентификации и вообще next.js в котором дается решение многих рутинных задач из коробки.