Как создать встроенный логин с помощью React
Auth0 — это поставщик «Аутентификация как услуга», что означает, что он обеспечивает реализацию аутентификации в вашем приложении без необходимости самостоятельной реализации полного потока. Следовательно, обработка токенов Id, Access и Refresh осуществляется самим Auth0, что позволяет вам сосредоточиться на приложении, которое вы создаете, и меньше беспокоиться о хранении и доступе к токенам и безопасности.
В этом блоге я расскажу, как я реализовал поток аутентификации с использованием Auth0 и React.
Возможно, вы сталкивались с пакетом «auth0-react» — пакетом, который является абстракцией ванильного пакета «auth0-js», который предоставляет API более высокого порядка, который значительно упрощает реализацию за счет использования Auth0. -предоставленная страница аутентификации — которая обрабатывает регистрацию и вход в систему (вы будете перенаправлены на эту страницу). Однако его можно настроить, если у вас есть учетная запись с активированным выставлением счетов.
Я буду использовать ванильный пакет «auth0-js», так как я буду использовать бесплатную учетную запись и хочу, чтобы процесс аутентификации происходил в моем приложении — встроенный вход в систему.
Установка
Для настройки панели управления Auth0 требуется несколько шагов.
- Перейдите на веб-сайт Auth0 и создайте нового «арендатора».
- Создайте новое приложение на боковой панели «Приложения» созданного арендатора.
- Перейдите на вкладку настроек созданного приложения.
- Добавьте URL-адреса, которые вы будете использовать при разработке, в следующих разделах. (Не забудьте обновить это всякий раз, когда вы используете другой локальный хост или после развертывания приложения).
- Включить ротацию токена обновления (если она не включена) — это понадобится нам для реализации постоянства пользователя при обновлении.
- Прокрутите вниз до «Дополнительных настроек» и нажмите на вкладку «Типы грантов». Убедитесь, что опция «Пароль» отмечена флажком.
- Нажмите на созданного арендатора в верхнем левом углу и перейдите в «Настройки».
- Нажмите на вкладку «Общие» и прокрутите, пока не найдете «Каталог по умолчанию» в разделе «Настройки авторизации API».
- Добавьте «Имя пользователя-Пароль-Аутентификация» в каталог по умолчанию. Убедитесь, что нет никаких опечаток.
- Перейдите к «Правилам» на боковой панели и «Создайте» новое «Пустое» правило. Это правило прикрепит атрибут «роль», который мы укажем, к объекту, который мы получим при аутентификации. Мы будем использовать этот атрибут для реализации авторизации.
– Добавьте название своего веб-сайта в<your-website>.
. Убедитесь, что вы нередактируетеnamespace
, кроме этого. (Имя правила может быть любым, которое вы предпочитаете).
— Это правило будет выполняться по запросу на вход, непосредственно перед выдачей токена id, тем самым внедряя роль в токен id.
- Перейдите к «Аутентификация» и создайте новое подключение к базе данных, дайте ему имя «Имя пользователя-пароль-аутентификация».
- Последний шаг. Вернитесь к созданному приложению, скопируйте Домен, Идентификатор клиента и Секрет клиента и вставьте эти значения в файл своего проекта. в моем случае я вставил их в файл env вместе с несколькими другими значениями, представленными на скриншоте ниже.
- URL-адрес перенаправления относится к URL-адресу, на котором запущено приложение; Соединение с БД — это база данных, которую мы создали; Тип ответа указывает, в какой форме мы хотим получить ответ при входе в систему; Режим ответа указывает, где будет отображаться ответ — в нашем случае он будет добавлен к нашему URL-адресу в виде фрагмента, однако он не будет использоваться, поскольку мы будем использовать метод встроенной аутентификации.
- Наконец, создайте новый файл, реализующий «WebAuth», который поступает из пакета «auth0-js» следующим образом. (Нам нужен offline_access для получения токенов обновления)
import auth0 from 'auth0-js'; export const webAuth = new auth0.WebAuth({ domain: `${process.env.REACT_APP_AUTH0_DOMAIN}`, clientID: `${process.env.REACT_APP_AUTH0_CLIENT_ID}`, responseType: `${process.env.REACT_APP_AUTH0_RESPONSE_TYPE}`, redirectUri: `${process.env.REACT_APP_REDIRECT_URL}`, responseMode: `${process.env.REACT_APP_AUTH0_RESPONSE_MODE}`, scope: 'openid profile email offline_access' });
регистр
Теперь, когда базовая установка готова, мы можем перейти к мясу и картошке. Приведенный ниже фрагмент кода является примером процесса регистрации.
const loginUser = async () => { webAuth.client.login({ realm: `${process.env.REACT_APP_AUTH0_DB_CONNECTION}`, username: email, password: password, }, async (err, result) => { if (err) { return err; } await authenticate(result); }); } const webAuthLogin = async () => { webAuth.signup({ connection: `${process.env.REACT_APP_AUTH0_DB_CONNECTION}`, email, password, user_metadata: { role: UserType.CUSTOMER, }, }, async (err, result) => { if (err) { return err; } await loginUser(); }); }
Для регистрации требуется электронная почта/имя пользователя и пароль. Наряду с этим вы можете отправлять дополнительные метаданные для обогащения профиля пользователя в пределах user_metadata.
Если вы помните, этот атрибут — это то, что мы называли получением атрибута роли.
Если с базовой настройкой все в порядке, этот запрос должен быть выполнен успешно, и вы сможете просмотреть этого пользователя на вкладке «Пользователи» в разделе «Управление пользователями».
Полученный результат будет обогащенным объектом, содержащим токены идентификатора и доступа. Вызываемая функция входа регистрирует зарегистрированного пользователя в приложении. Я займусь этим дальше.
Авторизоваться
Процесс входа в систему на первый взгляд относительно прост, как видно из фрагмента выше. Однако реализация функции аутентификации, которая вызывается при успешном ответе, требует немного больше усилий.
Следующий фрагмент — это функция authenticate
.
const authenticate = async (result) => { auth0Service.handleAuthentication(result); await auth0Service.setUserProfile(result.accessToken, result.idToken, dispatch); }
В приведенном выше фрагменте вызывается внешняя служба, которая выполняет закулисные функции, необходимые для удержания пользователя на обновлении страницы. Если настойчивость не нужна, этот шаг не требуется — будет достаточно полученного результата.
handleAuthentication
предназначен для хранения токенов в session storage
(local storage
тоже подойдет).
public handleAuthentication(result: any): void { if (result.idToken || result.id_token) { this.setSession(result); } else { History.push('/'); window.location.reload(); } } private setSession(result: any) { const expiresAt = result.expiresIn ? JSON.stringify(result.expiresIn * 1000 + new Date().getTime()) : JSON.stringify(result.expires_in * 1000 + new Date().getTime()); this.setSessionStorage(result, expiresAt); } private setSessionStorage(result: any, expiresAt: any): void { sessionStorage.setItem('refresh_token', result.refreshToken ? result.refreshToken : result.refresh_token); sessionStorage.setItem('expires_at', expiresAt); }
В приведенном выше фрагменте результат передается в setSession
, который получает время истечения срока действия токена, чтобы гарантировать, что можно использовать только токен, срок действия которого не истек. setSessionStorage
сохраняет полученный токен обновления и время истечения срока действия в хранилище сеансов. (проверки для result.idToken
&result.id_token
и result.refreshToken
& result.refresh_token
являются единственными, потому что есть вероятность, что Auth0 вернет их либо как camelCase, либо как snake_case)
Причина, по которой токен обновления хранится в хранилище сеансов, а не идентификатор или токены доступа, заключается в том, чтобы избежать атак CSRF (поскольку они содержат конфиденциальную информацию). Однако токен обновления их не содержит — он используется исключительно для получения других токенов доступа, поэтому сам по себе не имеет смысла.
setUserProfile
— это сохранение аутентифицированного пользователя в памяти — в данном случае это redux.
public async setUserProfile( accessToken: string, idToken: string, dispatch: any, ): Promise<any> { webAuth.client.userInfo(accessToken, async (err: any, result: any) => { if (err) { console.error('Something went wrong: ', err.message); return; } return this.authenticateUser( accessToken, idToken, result, dispatch, ); }); } private async authenticateUser( accessToken: string, idToken: string, result: any, dispatch: any, ) { dispatch( login({ email: result?.email, userType: result?.['https://<your-website>/claims/role'], idToken, accessToken, }) ); }
В приведенном выше фрагменте полученный токен доступа используется для получения информации о пользователе, которая использовалась для регистрации. Затем эта информация отправляется в redux. (В правиле мы указали возвращать атрибут role в нашем объекте результата. Если требуется дополнительная информация, это так же просто, как добавить ее в то же правило 😁).
Сохранение при обновлении
Теперь, когда мы интегрировали часть постоянства в логин, в этом разделе основное внимание будет уделено восстановлению вошедшего в систему пользователя при обновлении.
App.jsx useEffect(() => { const dispatchUserData = (authResult) => { const { user } = authResult.data; dispatch( login({ email: user?.email, accessToken: authResult.access_token, idToken: authResult.id_token, userType: user?.user_metadata?.role, }) ); } const setAuthenticatedUser = async () => { let authResult; if (isUserAuthenticated) { authResult = await auth0Service.getInitialAuthenticatedUser(); } if (authResult) dispatchUserData(authResult); } setAuthenticatedUser(); }, [auth0Service, dispatch, isUserAuthenticated]); External File public async getInitialAuthenticatedUser(): Promise<any> { if (sessionStorage.getItem('refresh_token')) { const isUserAuthenticated = this.isAuthenticated(); const refreshTokenResponse = await this.getUserWithRefreshToken(); if (isUserAuthenticated && refreshTokenResponse) { this.handleAuthentication(refreshTokenResponse); const user = await getUser(refreshTokenResponse.access_token); return { ...user, ...refreshTokenResponse }; } } } public isAuthenticated(): boolean { const date = sessionStorage.getItem('expires_at'); const refreshToken = sessionStorage.getItem('refresh_token'); if (date && refreshToken) { const expiresAt = JSON.parse(date); if (!refreshToken || (new Date().getTime() > expiresAt)) { this.removeSessionStorage(); return false; }; return true; } return false; } private async getUserWithRefreshToken(): Promise<any> { const response = await axios.post(`https://${process.env.REACT_APP_AUTH0_DOMAIN}/oauth/token`, { grant_type: 'refresh_token', client_id: `${process.env.REACT_APP_AUTH0_CLIENT_ID}`, refresh_token: sessionStorage.getItem('refresh_token'), client_secret: `${process.env.REACT_APP_AUTH0_CLIENT_SECRET}` }, { headers: { 'Content-Type': 'application/json', }, }, ); return response.data; } private async getUser(accessToken: string): Promise<any> { webAuth.client.userInfo(accessToken, async (err: any, result: any) => { if (err) { console.error('Something went wrong: ', err.message); return; } return result; }); } public removeSessionStorage(): void { sessionStorage.removeItem('refresh_token'); sessionStorage.removeItem('expires_at'); }
Приведенный выше фрагмент помещается в файл App
, поскольку он запускается при загрузке страницы. Определенный useEffect
вызывает вспомогательную функцию для получения текущего пользователя, вошедшего в систему, и сохраняет их в избыточности.
getInitialAuthenticatedUser
вызывает функцию, которая проверяет, аутентифицирован ли пользователь. Эта функция isUserAuthenticated
проверяет, что токен, хранящийся в хранилище сеансов, не просрочен (если это так, она удаляет его и возвращает false — пользователя нет).
ФункцияgetUserWithRefreshToken
говорит сама за себя. Он вызывает API вашего созданного приложения Auth0, передавая токен обновления, доступный в хранилище сеансов, для получения ответа. Та же процедура выполняется, когда вновь полученный токен обновления сохраняется в хранилище сеансов, переопределяя существующий в настоящее время.
getUser
вызывается с полученным токеном доступа, который, наконец, возвращает пользовательский объект.
Поздравляем! Теперь у вас есть работающий поток аутентификации, реализованный с использованием Auth0 😁