Как создать встроенный логин с помощью 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 😁