Привет, друзья!
В этой статье я покажу вам, как создать полноценный сервис для аутентификации и авторизации (далее — просто сервис) с помощью Auth0.
Auth0 — это платформа, предоставляющая готовые решения для разработки сервисов любого уровня сложности. Auth0 поддерживается командой, стоящей за разработкой JWT (JSON Web Token/веб-токен в формате JSON). Это вселяет определенную уверенность в безопасности Auth0-сервисов.
Бесплатная версия Auth0 позволяет регистрировать до 7000 пользователей.
В этой статье я писал о том, что такое JWT, и как разработать собственный сервис с нуля.
Знакомство с Auth0 можно начать отсюда.
Исходный код Auth0 SDK, который мы будем использовать для разработки приложения, можно найти здесь.
Исходный код проекта, который мы будем разрабатывать, находится здесь.
В статье я расскажу только о самых основных возможностях, предоставляемых Auth0.
В примерах и на скриншотах ниже вы увидите реальные чувствительные данные/sensitive data. Это не означает, что вы сможете их использовать. После публикации статьи сервис будет удален.
Подготовка и настройка проекта
Создаем директорию, переходим в нее и создаем клиента — шаблон React/TypeScript-приложения с помощью Create React App:
mkdir react-auth0 cd react-auth0 # cd !$ yarn create react-app client --template typescript # or npx create-react-app ...
Создаем директорию для сервера, переходим в нее и инициализируем Node.js-приложение:
mkdir server cd server yarn init -yp # or npm init -y
Создаем аккаунт Auth0:
Создаем tenant/арендатора:
Создаем одностраничное приложение/single page application на вкладке Applications/Applications:
Переходим в раздел Settings:
Создаем файл .env в директории client и записываем в него значения полей Domain и Client ID:
REACT_APP_AUTH0_DOMAIN = auth0-test-app.eu.auth0.com REACT_APP_AUTH0_CLIENT_ID = Ykv47YaNC3naGvfljFt8LyhzVPRPZCJY
Прописываем URL клиента в полях Allowed Callback URLs, Allowed Logout URLs и Allowed Web Origins:
Сохраняем изменения.
Создаем API на вкладке Applications/API:
Переходим в раздел Settings:
Записываем значение поля Identifier и URL сервера в файл .env:
REACT_APP_AUTH0_AUDIENCE='https://auth0-test-app' REACT_APP_SERVER_URI='http://localhost:4000/api'
Создаем файл .env в директории server следующего содержания:
AUTH0_DOMAIN='auth0-test-app.eu.auth0.com' AUTH0_AUDIENCE='https://auth0-test-app' CLIENT_URI='http://localhost:3000'
Клиент
Переходим в директорию client и устанавливаем дополнительные зависимости:
cd client # зависимости для продакшна yarn add @auth0/auth0-react react-router-dom react-loader-spinner # зависимость для разработки yarn add -D sass
- @auth0/auth0-react —
Auth0 SDKдляReact-приложений - react-router-dom — библиотека для маршрутизации
- react-loader-spinner — индикатор загрузки
- sass —
CSS-препроцессор
Структура директории src:
- api - messages.ts - components - AuthButton - LoginButton - LoginButton.tsx - LogoutButton - LogoutButton.tsx - AuthButton.tsx - Boundary - Error - error.scss - Error.tsx - Spinner - Spinner.tsx - Boundary.tsx - Navbar - Navbar.tsx - pages - AboutPage - AboutPage.tsx - HomePage - HomePage.tsx - MessagePage - message.scss - MessagePage.tsx - ProfilePage - profile.scss - ProfilePage.tsx - providers - AppProvider.tsx - Auth0ProviderWithNavigate.tsx - router - AppRoutes.tsx - AppLinks.tsx - styles - _mixins.scss - _variables.scss - types - index.d.ts - utils - createStore.tsx - App.scss - App.tsx - index.tsx ...
Логика работы приложения:
- в панели для навигации имеется кнопка для авторизации;
- кнопка рендерится условно в зависимости от статуса авторизации пользователя;
- если пользователь не авторизован, при нажатии кнопки он перенаправляется в
Auth0для выполнения входа в систему; - если пользователь авторизован, при нажатии кнопки выполняется выход из системы;
- в приложении имеется 4 страницы:
HomePage,AboutPage,ProfilePageиMessagePage; - первые две страницы находятся в открытом доступе;
- последние две — требуют авторизации;
- при переходе неавторизованного пользователя на страницу
ProfilePage, он перенаправляется вAuth0; - после входа в систему пользователь возвращается на страницу
ProfilePage, где видит информацию о своем профиле; - на странице
MessagePageпользователь может отправить два запроса к серверу: на получение открытого сообщения и на получение защищенного сообщения; - если пользователь не авторизован, при отправке запроса на получение защищенного сообщения возвращается ошибка.
Дальше я буду рассказывать только о том, что касается непосредственно Auth0.
Интеграция приложения с Auth0
Для интеграции приложения с Auth0 используется провайдер Auth0Provider.
Для того, чтобы иметь возможность переправлять пользователя на кастомную страницу после входа в систему, дефолтный провайдер необходимо апгрейдить следующим образом (providers/Auth0ProviderWithNavigate):
// импортируем дефолтный провайдер import { Auth0Provider } from '@auth0/auth0-react' // хук для выполнения программной навигации import { useNavigate } from 'react-router-dom' import { Children } from 'types' const domain = process.env.REACT_APP_AUTH0_DOMAIN as string const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID as string const audience = process.env.REACT_APP_AUTH0_AUDIENCE as string const Auth0ProviderWithNavigate = ({ children }: Children) => { const navigate = useNavigate() // функция, вызываемая после авторизации const onRedirectCallback = (appState: { returnTo?: string }) => { // путь для перенаправления указывается в свойстве `returnTo` // по умолчанию пользователь возвращается на текущую страницу navigate(appState?.returnTo || window.location.pathname) } return ( <Auth0Provider domain={domain} clientId={clientId} // данная настройка нужна для взаимодействия с сервером audience={audience} redirectUri={window.location.origin} onRedirectCallback={onRedirectCallback} > {children} </Auth0Provider> ) } export default Auth0ProviderWithNavigate
С сигнатурой провайдера можно ознакомиться здесь.
Оборачиваем компоненты приложения в провайдер (index.tsx):
import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' import Auth0ProviderWithNavigate from 'providers/Auth0ProviderWithNavigate' import { AppProvider } from 'providers/AppProvider' import App from './App' ReactDOM.render( <React.StrictMode> {/* провайдер маршрутизации */} <BrowserRouter> {/* провайдер авторизации */} <Auth0ProviderWithNavigate> {/* провайдер состояния */} <AppProvider> <App /> </AppProvider> </Auth0ProviderWithNavigate> </BrowserRouter> </React.StrictMode>, document.getElementById('root') )
Вход и выход из системы
Для входа в систему используется метод loginWithRedirect, а для выхода — метод logout. Оба метода возвращаются хуком useAuth0. useAuth0 также возвращает логическое значение isAuthenticated (и много чего еще) — статус авторизации, который можно использовать для условного рендеринга кнопок.
Вот как реализована кнопка для аутентификации (components/AuthButton/AuthButton.tsx):
// импортируем хук import { useAuth0 } from '@auth0/auth0-react' import { LoginButton } from './LoginButton/LoginButton' import { LogoutButton } from './LogoutButton/LogoutButton' export const AuthButton = () => { // получаем статус авторизации const { isAuthenticated } = useAuth0() return isAuthenticated ? <LogoutButton /> : <LoginButton /> }
Кнопка для входа в систему (components/AuthButton/LoginButton/LoginButton.tsx):
// импортируем хук import { useAuth0 } from '@auth0/auth0-react' export const LoginButton = () => { // получаем метод для входа в систему const { loginWithRedirect } = useAuth0() return ( <button className='auth login' onClick={loginWithRedirect}> Log In </button> ) }
Кнопка для выхода из системы (components/AuthButton/LogoutButton/LogoutButton.tsx):
// импортируем хук import { useAuth0 } from '@auth0/auth0-react' export const LogoutButton = () => { // получаем метод для выхода из системы const { logout } = useAuth0() return ( <button className='auth logout' // после выхода из системы, пользователь перенаправляется на главную страницу onClick={() => logout({ returnTo: window.location.origin })} > Log Out </button> ) }
С сигнатурой хука можно ознакомиться здесь.
Состояние авторизации
Состояние авторизации пользователя сохраняется на протяжении времени жизни id_token/токена идентификации. Время жизни токена устанавливается на вкладке Settings приложения в поле ID Token Expiration раздела ID Token и по умолчанию составляет 36 000 секунд или 10 часов:
Токен записывается в куки, которые можно найти в разделе Storage/Cookies вкладки Application инструментов разработчика в браузере:
Это означает, что статус авторизации пользователя сохраняется при перезагрузке страницы, закрытии/открытии вкладки браузера и т.д.
При выходе из системы куки вместе с id_token удаляется.
Создание защищенной страницы
Для защиты страницы от доступа неавторизованных пользователей предназначена утилита withAuthenticationRequired. Хук useAuth0, кроме прочего, возвращает объект user с нормализованными данными пользователя.
Страница ProfilePage реализована следующим образом (pages/ProfilePage/ProfilePage.tsx):
import './profile.scss' import { useAuth0, withAuthenticationRequired } from '@auth0/auth0-react' import { Spinner } from 'components/index.components' // оборачиваем код компонента в утилиту export const ProfilePage = withAuthenticationRequired( () => { // получаем данные пользователя const { user } = useAuth0() return ( <> <h1>Profile Page</h1> <div className='profile'> <img src={user?.picture} alt={user?.name} /> <div> <h2>{user?.name}</h2> <p>{user?.email}</p> </div> </div> </> ) }, { // обе настройки являются опциональными returnTo: '/profile', onRedirecting: () => <Spinner /> } )
С сигнатурой утилиты можно ознакомиться здесь.
Проверка работоспособности клиента
Находясь в директории client, выполняем команду yarn start или npm start для запуска сервера для разработки:
Нажимаем на кнопку Log In. Попадаем на страницу регистрации/авторизации Auth0:
По умолчанию предоставляется возможность входа в систему с помощью аккаунта Google (Google OAuth 2.0). Позже мы добавим возможность авторизации с помощью аккаунта GitHub.
Входим в систему. Возвращаемся на главную страницу. Видим, что кнопка Log In сменилась кнопкой Log Out.
Выходим из системы. Пробуем перейти на страницу Profile. Снова попадаем на страницу Auth0. Входим в систему. Возвращаемся на страницу профиля:
Подключение GitHub
Переходим на вкладку Authentication/Social и нажимаем кнопку Create Connection:
Выбираем GitHub из предложенного списка:
Заходим в профиль GitHub. Переходим в раздел Settings/Developer settings/OAuth Apps и нажимаем на кнопку Register a new application:
Заполняем поля Application name, Homepage URL (https://ВАШ-ДОМЕН.auth0.com) и Authorization callback URL (https://ВАШ-ДОМЕН/login/callback):
Нажимаем на кнопку Generate a new client secret. Копируем значения полей Client ID и Client secret и вставляем их в соответствующие поля Auth0:
В разделе Attributes в дополнение к Basic Profile выбираем Email address, а в разделе Permissions — read:user.
Нажимаем на кнопку Create. Подключаем клиентское приложение и API.
Нажимаем на кнопку Try Connection для проверки соединения.
Нажимаем на кнопку Authorize ВАШЕ_ИМЯ.
Получаем сообщение о том, что соединение работает:
Теперь если мы нажмем Log In в приложении, то увидим, что у нас появилась возможность авторизоваться через GitHub:
Что касается Google, то Auth0 предоставляет тестовые ключи, которые должны быть заменены настоящими перед выпуском приложения в продакшн.
Займемся страницей MessagePage и сервером.
Интеграция клиента с сервером
API
Начнем с API (api/messages.ts):
// адрес сервера const SERVER_URI = process.env.REACT_APP_SERVER_URI // сервис для получения открытого сообщения export async function getPublicMessage() { let data = { message: '' } try { const response = await fetch(`${SERVER_URI}/messages/public`) if (!response.ok) throw response data = await response.json() } catch (e) { throw e } finally { return data.message } } // сервис для получения защищенного сообщения // функция принимает `access_token/токен доступа` export async function getProtectedMessage(token: string) { let data = { message: '' } try { const response = await fetch(`${SERVER_URI}/messages/protected`, { headers: { // добавляем заголовок авторизации с токеном Authorization: `Bearer ${token}` } }) if (!response.ok) throw response data = await response.json() } catch (e) { throw e } finally { return data.message } }
Страница MessagePage (pages/MessagePage/MessagePage.tsx).
Импортируем хуки, компонент, провайдер, сервисы и стили:
import { useAuth0 } from '@auth0/auth0-react' import { getProtectedMessage, getPublicMessage } from 'api/messages' import { Boundary } from 'components/Boundary/Boundary' import { useAppSetter } from 'providers/AppProvider' import { useState } from 'react' import './message.scss'
Получаем сеттеры, определяем состояние для сообщения и его типа:
export const MessagePage = () => { const { setLoading, setError } = useAppSetter() const [message, setMessage] = useState('') const [type, setType] = useState('') // TODO }
Для генерации токена доступа (access_token) предназначен метод getAccessTokenSilently, возвращаемый хуком useAuth0:
const { getAccessTokenSilently } = useAuth0()
Определяем функцию для запроса открытого сообщения:
function onGetPublicMessage() { setLoading(true) getPublicMessage() .then(setMessage) .catch(setError) .finally(() => { setType('public') setLoading(false) }) }
Определяем функцию для получения защищенного сообщения:
function onGetProtectedMessage() { setLoading(true) // генерируем токен и передаем его сервису `getProtectedMessage` getAccessTokenSilently() .then(getProtectedMessage) .then(setMessage) .catch(setError) .finally(() => { setType('protected') setLoading(false) }) }
Наконец, возвращаем разметку:
return ( <Boundary> <h1>Message Page</h1> <div className='message'> <button onClick={onGetPublicMessage}>Get Public Message</button> <button onClick={onGetProtectedMessage}>Get Protected Message</button> {message && <h2 className={type}>{message}</h2>} </div> </Boundary> )
Сервер
Переходим в директорию server и устанавливаем зависимости:
# зависимости для продакшна yarn add express helmet cors dotenv express-jwt jwks-rsa # зависимости для разработки yarn add -D nodemon
express:Node.js-фреймворкдля разработки веб-серверов;helmet: утилита для установкиHTTP-заголовков, связанных с безопасностью. Об этих заголовках можно почитать здесь;cors: утилита для установкиHTTP-заголовков, связанных сCORS;dotenv: утилита для работы с переменными среды окружения;express-jwt:посредник/middlewareдля валидацииJWTчерез модульjsonwebtoken;jwks-rsa: утилита для извлеченияключей подписания/signing keysизJWKS(JSON Web Key Set/набор веб-ключей в форматеJSON);nodemon: утилита для запуска сервера для разработки.
О том, что такое JWKS и для чего он используется, можно почитать здесь.
Пример интеграции jwks-rsa с express-jwt можно найти здесь.
Структура сервера:
- routes - api.routes.js - messages.routes.js - utils - checkJwt.js - .env - index.js - ...
Здесь нас интересуют 2 файла: messages.routes.js и checkJwt.js.
messages.routes.js:
import { Router } from 'express' import { checkJwt } from '../utils/checkJwt.js' const router = Router() router.get('/public', (req, res) => { res.status(200).json({ message: 'Public message' }) }) router.get('/protected', checkJwt, (req, res) => { res.status(200).json({ message: 'Protected message' }) }) export default router
При запросе к api/messages/public возвращается сообщение Public message. При запросе к api/messages/protected выполняется проверка JWT. Данный маршрут (роут) является защищенным. Когда проверка прошла успешно, возвращается сообщение Protected message. В противном случае, утилита возвращает ошибку.
Рассмотрим этого посредника (utils/checkJwt.js).
Импортируем утилиты:
import jwt from 'express-jwt' import jwksRsa from 'jwks-rsa' import dotenv from 'dotenv'
Получаем доступ к переменным среды окружения, хранящимся в файле .env, и извлекаем их значения:
dotenv.config() const domain = process.env.AUTH0_DOMAIN const audience = process.env.AUTH0_AUDIENCE
audience — простыми словами, это аудитория токена, т.е. те, для кого предназначен токен.
Определяем утилиту:
export const checkJwt = jwt({ secret: jwksRsa.expressJwtSecret({ cache: true, // ограничение максимального количества запросов rateLimit: true, // 10 запросов в минуту jwksRequestsPerMinute: 10, // обратите внимание на сигнатуру пути jwksUri: `https://${domain}/.well-known/jwks.json` }), // аудитория audience, // тот, кто подписал токен issuer: `https://${domain}/`, // алгоритм, использованный для подписания токена algorithms: ['RS256'] })
Определяем тип кода сервера (модуль) и команды для запуска сервера в режиме для разработки и в производственном режиме в package.json:
"type": "module", "scripts": { "start": "node index.js", "dev": "nodemon index.js" }
Запускаем сервер для разработки с помощью команды yarn dev или npm run dev и возвращаемся в браузер.
Выходим из системы. Переходим на страницу MessagePage и пытаемся получить открытое сообщение:
Работает.
Теперь пробуем получить защищенное сообщение:
Получаем сообщение об ошибке, которое говорит о необходимости авторизации.
Авторизуемся и пробуем снова:
Получилось!
Кажется, что наш сервис аутентификации/авторизации работает, как ожидается.
Согласитесь, что Auth0 существенно облегчает выполнение нетривиальной задачи по разработке сервиса аутентификации/авторизации веб-приложения.
Пожалуй, это все, чем я хотел поделиться с вами в данной статье.
Надеюсь, вы нашли для себя что-то интересное и не зря потратили время.
Благодарю за внимание. Happy coding и счастливого Нового года!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/598727/

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