Репозиторий проекта

На этой неделе мы рассмотрим использование социального входа Auth0 для аутентификации в GitHub. Мы также настраиваем веб-перехватчики для создания локального пользователя в нашей базе данных Prisma каждый раз, когда новый пользователь входит в систему Auth0.

Социальный вход с GitHub

Включение различных социальных провайдеров очень просто с Auth0. Следуйте этому руководству, чтобы настроить ряд социальных провайдеров — Google, Facebook, Twitter и т. д. Я только собираюсь настроить GitHub.

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

Крючки Auth0

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

async function (user, context, callback) {
  // do some stuff
  callback(null, user, context);
}

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

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

Первый параметр, ожидаемый обратным вызовом, — это ошибка. Если это значение равно null или не определено, процесс входа будет продолжен. Если этот параметр является истинным значением, он вызовет исключение и остановит процесс входа.

Если мы не вызовем функцию обратного вызова, процесс входа в систему в конечном итоге прервется.

Давайте настроим новый маршрут API в нашем приложении Next.js для обработки запроса от хука Auth0.

// pages/api/auth/hooks.js

module.exports = async (req, res) => {
  const { email } = JSON.parse(req.body)
  // create user in prisma
  console.log('created user')
  res.send({ received: true })
}

Нам нужно вызвать res.send, чтобы хук получил код состояния 200 и мог продолжить процесс входа.

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

async function (user, context, callback) {
  await request.post('http://localhost:3000/api/auth/hooks', {
    body: JSON.stringify({
      email: user.email,
    })
  });

  callback(null, user, context);
}

Крючки Auth0 похожи на точки с запятой — вы можете выбрать то, что предпочитаете в другом месте, но, вероятно, лучше поместить их здесь!

Теперь давайте активируем хук, войдя в наше приложение Next.js.

ОШИБКА!

Проблема в том, что этот хук Auth0 работает на каком-то удаленном сервере Auth0, а не на нашей локальной машине. Поэтому он понятия не имеет, что такое локальный хост. Нгрок на помощь!

Нгрок

Это инструмент, который перенаправляет общедоступный URL-адрес в Интернете на определенный порт, работающий на локальном хосте (наш сервер разработки Next.js). Это часто называют туннелированием.

Мы можем установить его с помощью npm.

npm i -g ngrok

А затем перенаправьте его на порт: 3000.

ngrok http 3000

Это должно дать вам URL-адрес, который вы можете использовать для замены «http://localhost:3000» в нашем запросе хука Auth0.

async function (user, context, callback) {
  await request.post('https://0d4d01c96799.au.ngrok.io/api/auth/hooks', {
    body: JSON.stringify({
      email: user.email,
    })
  });
  callback(null, user, context);
}

Теперь вы сможете инициировать запрос к нашему новому маршруту API, выполнив вход в приложение Next.js.

Не забудьте установить это на свой рабочий URL-адрес при развертывании приложения!

Вы должны увидеть этот выход «созданного пользователя» в консоль терминала, но мы пока этого не делаем. Давайте создадим нового пользователя в Prisma.

// pages/api/auth/hooks.js

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

module.exports = async (req, res) => {
  const { email } = JSON.parse(req.body)
  
  const user = await prisma.user.create({
    data: { email },
  })

  await prisma.$disconnect()
  
  console.log('created user')
  res.send({ received: true })
}

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

// pages/api/auth/hooks.js

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

module.exports = async (req, res) => {
  try {
    const { email } = JSON.parse(req.body)
    const user = await prisma.user.create({
      data: { email },
    })
    console.log('created user')
  } catch (err) {
    console.log(err)
  } finally {
    await prisma.$disconnect()
    res.send({ received: true })
  }
}

Блок finally будет работать независимо от того, успешно ли мы создали пользователя или возникло исключение. Написание здесь логики очистки помогает СУШИТЬ наш код.

Теперь это должно создавать нового пользователя в Prisma каждый раз, когда пользователь входит в систему. Подождите, КАЖДЫЙ РАЗ?!?! это не хорошо!

Проблема 1: Новый пользователь при каждом входе в систему!

К счастью, мы ничего не подтолкнули к производству. Это могло стоить нам немного денег в приложении с высоким трафиком!

Мы хотим создать пользователя только при первом входе в систему, поэтому нам нужен какой-то способ узнать, успешно ли мы создали пользователя в прошлом. Мы могли бы открыть другой маршрут API для проверки связи с базой данных Prisma и убедиться, что пользователь с таким адресом электронной почты еще не существует, но для этого потребуется еще один переход от серверов Auth0 к Vercel. Мы не хотим заставлять нашего пользователя ждать без необходимости.

К счастью, Auth0 дает нам возможность устанавливать метаданные для нашего пользователя.

Мы можем установить метаданные после создания пользователя таким образом.

user.app_metadata = user.app_metadata || {};
user.app_metadata.localUserCreated = true;

Нам нужно вручную указать Auth0, чтобы он сохранял эти метаданные следующим образом.

await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);

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

if (!user.app_metadata.localUserCreated) {
  // create prisma user
}

Полное правило должно выглядеть примерно так.

async function (user, context, callback) {
  user.app_metadata = user.app_metadata || {};

  if (!user.app_metadata.localUserCreated) {
    await request.post('https://0d4d01c96799.au.ngrok.io/api/auth/hooks', {
      body: JSON.stringify({
        email: user.email,
      })
    });
    user.app_metadata.localUserCreated = true;
    await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);
  }
  callback(null, user, context);
}

Давайте также поместим это в блок try-catch, чтобы убедиться, что мы отвечаем, если возникает исключение.

async function (user, context, callback) {
  try {
    user.app_metadata = user.app_metadata || {};
  
    if (!user.app_metadata.localUserCreated) {
      await request.post('https://0d4d01c96799.au.ngrok.io/api/auth/hooks', {
        body: JSON.stringify({
          email: user.email,
        })
      });
      user.app_metadata.localUserCreated = true;
      await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);
    }
    callback(null, user, context);
  } catch (err) {
    callback(err);
  }
}

Здорово! Итак, теперь каждый раз, когда пользователь входит в систему, а у нас нет учетной записи в prisma, он будет вызывать наш маршрут API для создания пользователя.

ЖДАТЬ! У нас просто есть открытый маршрут API, который будет создавать пользователя каждый раз, когда мы отправляем ему запрос?!? Это не хорошо! Откуда мы знаем, что это исходит от Auth0?!?

Проблема 2: Наш маршрут API для аутентификации не аутентифицирован!

Хорошо, есть несколько способов решить эту проблему. Вы можете подумать: «Разве не для этого у нас есть библиотека Auth0? Просто оберните его той функцией withApiAuthRequired, о которой вы бредили!»

Поскольку это исходит от Auth0, а не от нашего приложения Next.js, сеанс на самом деле не существует!

Нам нужно будет вручную отправить секретное значение из хука Auth0 и проверить его наличие и правильность в маршруте API. Это решение похоже на ключи API, которые сопоставляются с конкретным пользователем.

В меню «Правила» мы можем создать новый секрет.

Я рекомендую установить значение в длинную случайно сгенерированную строку.

Теперь мы можем получить доступ к этому значению в нашем хуке Auth0 следующим образом.

configuration.AUTH0_HOOK_SECRET

Давайте опубликуем это вместе с нашим запросом на маршрут API.

async function (user, context, callback) {
  try {
    user.app_metadata = user.app_metadata || {};
  
    if (!user.app_metadata.localUserCreated) {
      await request.post('https://0d4d01c96799.au.ngrok.io/api/auth/hooks', {
        body: JSON.stringify({
          email: user.email,
          secret: configuration.AUTH0_HOOK_SECRET,
        })
      });
      user.app_metadata.localUserCreated = true;
      await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);
    }
    callback(null, user, context);
  } catch (err) {
    callback(err);
  }
}

Теперь нам нужно обновить файл .env нашего приложения Next.js, чтобы он содержал это значение.

// .env

// other secrets
AUTH0_HOOK_SECRET=that-super-secret-value-that-no-one-else-knows

И оберните нашу логику создания пользователя проверкой, чтобы убедиться, что значение правильное.

const { email, secret } = JSON.parse(req.body)

if (secret === process.env.AUTH0_HOOK_SECRET) {
  // create user
} else {
  console.log('You forgot to send me your secret!')
}

Весь маршрут API должен выглядеть примерно так.

// pages/api/auth/hooks.js

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

module.exports = async (req, res) => {
  try {
    const { email, secret } = JSON.parse(req.body)
    if (secret === process.env.AUTH0_HOOK_SECRET) {
      const user = await prisma.user.create({
        data: { email },
      })
      console.log('created user')
    } else {
      console.log('You forgot to send me your secret!')
    }
  } catch (err) {
    console.log(err)
  } finally {
    await prisma.$disconnect()
    res.send({ received: true })
  }
}

Следуйте той же логике из Хостинг на Vercel, автоматическое развертывание с помощью GitHub и настройка пользовательских доменов, чтобы добавить наши новые секреты Auth0 в Vercel — без этого наше размещенное приложение не будет работать.

Отлично! Вот и все! Мы сделали это!

Теперь каждый раз, когда новый пользователь входит в наше приложение Next.js, Auth0 сообщит нам об этом, чтобы мы могли создать пользователя в нашей базе данных Prisma, чтобы отслеживать те дополнительные биты данных, о которых заботится наше приложение!

Подписывайтесь на меня

"Веб-сайт"

Твиттер

"YouTube"

Следующая неделя

Обработка платежей с помощью Stripe и вебхуков