Аутентификация в Django-REST с помощью Auth.js

Перевод статьи «Django REST Framework Authentication with Auth.js».

В этом уроке рассматривается реализация системы аутентификации на основе Django REST (с помощью Django REST Framework) и ее интеграция с Auth.js (ранее известным как NextAuth.js) во фронтенде. Мы разберем настройку аутентификации на основе учетных данных, а также аутентификацию с помощью Google.

Содержание

Введение

Стандартная система аутентификации Django является отличным готовым решением для полнофункциональных Django-проектов. Тем не менее, при использовании Django REST Framework с клиент-серверной архитектурой использовать встроенную аутентификацию Django может быть довольно сложно. Кроме того, Django не поддерживает social auth.

К счастью, вы можете расширить систему аутентификации Django и добавить поддержку social auth с помощью пакетов django-allauth и dj-rest-auth, разработанных сообществом. После того как система аутентификации будет настроена, подключиться к ней из фронтенда будет довольно просто.

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

В связи с этим появилась библиотека Auth.js. Это стандартизированное решение аутентификации с открытым исходным кодом для Next.js, SvelteKit и SolidStart. В библиотеке реализованы аутентификация на основе учетных данных, аутентификация без пароля, социальная аутентификация и многое другое. Она также поддерживает более 60 социальных провайдеров.

В этом руководстве мы рассмотрим, как реализовать систему аутентификации на основе REST-технологии Django и подключиться к ней с помощью фронтенда Next.js. К концу этого урока у вас будет полностью рабочая система аутентификации, поддерживающая социальную аутентификацию. Сначала мы поработаем над бэкендом, а затем перейдем к фронтенду.

Реализованное решение будет иметь следующий поток аутентификации:

Схема потока аутентификации

Цели

К концу этого урока вы сможете:

  • Реализовать систему аутентификации на основе Django REST с помощью Django REST Framework, django-allauth и dj-rest-auth.
  • Включить и настроить аутентификацию по JSON Web Token с помощью djangorestframework-simplejwt.
  • Настроить django-cors-headers, чтобы избежать проблем с CORS при подключении из фронтенда.
  • Настроить Auth.js для поддержки аутентификации на основе учетных данных, а также социальной аутентификации.
  • Подключить систему аутентификации Django на основе REST к Auth.js.
  • Добавить социальную аутентификацию с помощью Google.

Бэкенд

В этом разделе мы создадим новый проект Django, запустим специальное приложение для аутентификации, установим и настроим все зависимости, а также протестируем систему аутентификации через cURL.

Вы можете пропустить настройку Django и продолжить работу с вашим проектом.

Настройка Django

Начните с создания нового каталога для вашего проекта и виртуальной среды:

$ mkdir django-rest-authjs && cd django-rest-authjs
$ python3.11 -m venv env
$ source env/bin/activate
(env)$

Затем установите Django и загрузите новый проект:

(env)$ pip install Django==4.2.3
(env)$ django-admin startproject core .

Примените миграции и запустите сервер:

(env)$ python manage.py migrate
(env)$ python manage.py runserver

Наконец, откройте ваш любимый браузер и перейдите по адресу http://localhost:8000. Вы должны увидеть стандартную главную страницу Django.

Приложение для аутентификации

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

Создайте новое приложение:

(env)$ python manage.py startapp authentication

Добавьте его в INSTALLED_APPS в core/settings.py:

# core/settings.py

INSTALLED_APPS = [
    # ...
    "authentication.apps.AuthenticationConfig",
]

Создайте на уровне приложения файл urls.py  в папке authentication:

# authentication/urls.py

from django.urls import path
from . import views

urlpatterns = [
    # URLs will come here
]

Зарегистрируйте новый urls.py глобально:

# core/urls.py

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('api/auth/', include('authentication.urls')),
    path("admin/", admin.site.urls),
]

На этом первоначальная настройка закончена.

Аутентификация на основе учетных данных

В этом разделе мы настроим Django REST Frameworkdjango-allauth и dj-rest-auth для включения аутентификации на основе учетных данных. Кроме того, мы настроим django-cors-headers, чтобы избежать проблем с CORS при подключении из фронтенда.

Django REST Framework

Начните с установки Django REST Framework:

(env)$ pip install djangorestframework==3.14.0

Далее перейдите к core/settings.py и добавьте его в INSTALLED_APPS  с authtoken:

# core/settings.py

INSTALLED_APPS = [
    # ...
    "rest_framework",
    "rest_framework.authtoken",
]

Приложение authtoken необходимо, поскольку мы будем использовать аутентификацию на основе токенов вместо стандартной аутентификации Django SessionAuthentication. Токен-аутентификация больше подходит для клиент-серверных систем.

Существует две системы аутентификации на основе токенов, из которых мы можем выбрать:

Я рекомендую использовать JWT-аутентификацию с помощью плагина djangorestframework-simplejwt. JWT-аутентификация лучше тем, что она более безопасна, проще для базы данных и имеет стандартизированные HTTP-ответы.

Чтобы установить плагин Simple JWT, выполните команду:

$(env) pip install djangorestframework-simplejwt==5.2.2

Далее добавьте его в INSTALLED_APPS под rest_framework.authtoken:

# core/settings.py

INSTALLED_APPS = [
    # ...
    "rest_framework.authtoken",
    "rest_framework_simplejwt",
]

Затем добавьте следующие настройки в нижнюю часть файла core/settings.py:

# core/settings.py

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": False,
    "BLACKLIST_AFTER_ROTATION": False,
    "UPDATE_LAST_LOGIN": True,
    "SIGNING_KEY": "complexsigningkey",  # generate a key and replace me
    "ALGORITHM": "HS512",
}

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

Не забывайте об импорте:

from datetime import timedelta

Наконец, измените класс аутентификации DRF по умолчанию:

# core/settings.py

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ]
}

django-allauth

Сначала установите django-allauth:

(env)$ pip install django-allauth==0.52.0

Далее перейдите к файлу settings.py и добавьте приложение в INSTALLED_APPS вместе с приложениями account и socialaccount:

# core/settings.py

INSTALLED_APPS = [
    "django.contrib.sites",  # Убедитесь, что 'django.contrib.sites' установлен
    # ...
    "allauth",
    "allauth.account",
    "allauth.socialaccount",  # Добавьте, если хотите социальную аутентификацию
]

django-allauth зависит от фреймворка Django «sites», поэтому убедитесь, что он у вас установлен. Кроме того, убедитесь, что у вас установлен SITE_ID :

# core/settings.py

SITE_ID = 1  # make sure SITE_ID is set

Далее необходимо отключить функцию проверки электронной почты:

# core/settings.py

ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = "none"

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

Наконец, еще раз выполните миграцию базы данных:

(env)$ python manage.py migrate

dj-rest-auth

Давайте установим dj-rest-auth:

(env)$ pip install "dj-rest-auth[with_social]==4.0.0"

Нам необходимо использовать спецификатор with_social, так как мы хотим включить стандартный процесс регистрации. Кроме того, мы будем использовать этот пакет позже, когда включим социальную аутентификацию.

Далее перейдите к core/settings.py и добавьте приложение в INSTALLED_APPS:

# core/settings.py

INSTALLED_APPS = [
    # ...
    "dj_rest_auth",
    "dj_rest_auth.registration",
]

Затем добавьте в settings.py следующие параметры конфигурации:

# core/settings.py

REST_AUTH = {
    "USE_JWT": True,
    "JWT_AUTH_HTTPONLY": False,
}

Примечания:

  1. USE_JWT требуется, поскольку мы используем djangorestframework-simplejwt.
  2. JWT_AUTH_HTTPONLY должен быть выключен, иначе dj-rest-auth не будет отправлять токены обновления.

Все настройки REST_AUTH  смотрите в официальной документации.

На момент написания статьи в официальном руководстве по установке указано, что необходимо зарегистрировать dj_rest_auth.urls. Я не рекомендую этого делать, поскольку URL по умолчанию содержат неработающие URL при неправильной настройке (например, сброс пароля, проверка электронной почты).

Вместо этого загляните в исходный код dj-rest-auth  и вручную подберите необходимые URL-адреса:

# authentication/urls.py

from dj_rest_auth.jwt_auth import get_refresh_view
from dj_rest_auth.registration.views import RegisterView
from dj_rest_auth.views import LoginView, LogoutView, UserDetailsView
from django.urls import path
from rest_framework_simplejwt.views import TokenVerifyView

urlpatterns = [
    path("register/", RegisterView.as_view(), name="rest_register"),
    path("login/", LoginView.as_view(), name="rest_login"),
    path("logout/", LogoutView.as_view(), name="rest_logout"),
    path("user/", UserDetailsView.as_view(), name="rest_user_details"),
    path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
    path("token/refresh/", get_refresh_view().as_view(), name="token_refresh"),
]

django-cors-headers

Последний пакет, который нам необходимо установить и настроить, — это django-cors-headers. Этот пакет будет устанавливать правильные CORS-заголовки при взаимодействии нашего фронтенда с бэкендом.

Установка:

$(env) pip install django-cors-headers==4.1.0

Добавьте его в INSTALLED_APPS в core/settings.py:

# core/settings.py

INSTALLED_APPS = [
    # ...
    "corsheaders",
]

Добавьте CorsMiddleware в список MIDDLEWARE:

# core/settings.py

MIDDLEWARE = [
    # ...
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    # ...
]

CorsMiddleware следует размещать как можно выше. Особенно перед любым промежуточным ПО, которое может генерировать ответы, таким как CommonMiddleware или  WhiteNoiseMiddleware.

Наконец, добавьте следующие настройки в нижнюю часть core/settings.py:

# core/settings.py

if DEBUG:
    CORS_ALLOW_ALL_ORIGINS = True
else:
    CORS_ALLOWED_ORIGINS = [
        "http://localhost:3000/",
        "http://127.0.0.1:3000/",
    ]

Отлично! Вот и все.

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

Тестирование

Для тестирования API мы будем использовать cURL. Ответы мы также будем передавать в jq для их автоматического форматирования и выделения цветом.

Начните с запуска сервера разработки Django:

(env)$ python manage.py runserver
Регистрация

Во-первых, создайте пользователя:

$ curl -XPOST -H "Content-type: application/json" -d '{
      "username": "user1",
      "password1": "complexpassword123",
      "password2": "complexpassword123"
  }' 'http://localhost:8000/api/auth/register/' | jq

Вы должны получить аналогичный ответ:

{
  "access": "eyJhb...",
  "refresh": "eyJhb...",
  "user": {
    "pk": 1,
    "username": "user1",
    "email": ""
  }
}

Теперь для аутентификации можно использовать токен access. Он действителен в течение времени, указанного в core/settings.py, после чего его необходимо обновить с помощью токена refresh.

Вход в систему

Войдите в систему под именем вновь созданного пользователя:

$ curl -XPOST -H "Content-type: application/json" -d '{
      "username": "user1",
      "password": "complexpassword123"
  }' 'http://localhost:8000/api/auth/login/' | jq

Ответ будет аналогичен этому:

{
  "access": "eyJhb...",
  "refresh": "eyJhb...",
  "user": {
    "pk": 1,
    "username": "user1",
    "email": ""
  }
}
Подробности о пользователе

Получение данных о пользователе по полученному токену access:

$ curl -XGET -H 'Authorization: Bearer <your_access_token>' \
    -H "Content-type: application/json" 'http://localhost:8000/api/auth/user/' | jq

Ответ:

{
  "pk": 1,
  "username": "user1",
  "email": ""
}
Токен Verify

Чтобы убедиться в том, что токен все еще действителен, можно передать его в /api/auth/token/verify/ следующим образом:

$ curl -XPOST -H "Content-type: application/json" -d '{
    "token": "<your_access_token>"
  }' 'http://localhost:8000/api/auth/token/verify/' | jq

Ответ:

{}

Коды состояния:

  • 200 означает, что токен все еще действителен
  • 4xx означает, что токен больше не действителен
Обновление токена

Чтобы получить обновленный токен access, необходимо использовать свой токен refresh  следующим образом:

$ curl -XPOST -H "Content-type: application/json" -d '{
    "refresh": "your_refresh_token>"
  }' 'http://localhost:8000/api/auth/token/refresh/' | jq

Ответ:

{
  "access": "eyJhb...",
  "access_expiration": "2023-07-11T17:42:22.480739Z"
}

Ответ будет содержать новый токен access  и время его действия.

Выход из системы

Для выхода пользователя из системы попробуйте отправить следующий запрос:

$ curl -XPOST -H 'Authorization: Bearer <your_access-token>' \
    -H "Content-type: application/json" 'http://localhost:8000/api/auth/logout/' | jq

Ответ:

{
  "detail": "Neither cookies or blacklist are enabled. Delete the token on client."
}

Поскольку мы отключили cookies и не настроили черный список, этот эндпоинт ничего не делает. Нет необходимости вызывать его при выходе пользователя из системы. Единственное, что нужно сделать, это удалить токен из клиента.

Если вам нужен более надежный подход, настройте черный список с помощью приложения JWT Blacklist.

Социальная аутентификация с помощью Google

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

Если вы не заинтересованы в настройке социальной аутентификации, можете пропустить этот раздел.

Настройка

Чтобы включить функцию Google sign up, сначала необходимо создать ключ OAuth Client. Перейдите в Google Cloud Console, выберите проект, который вы хотите использовать, и выполните поиск «API Credentials»:

Список с результатами поиска. Красная стрелка указывает на нужный пункт

Далее нажмите на кнопку «Create credentials» и выберите в выпадающем списке «OAuth Client ID»:

Красная стрелка указывает на OAuth Client ID в выпадающем списке

Выберите «Web-приложение», задайте пользовательское имя и добавьте URL-адрес вашего фронтенда в качестве URI авторизованного перенаправления. Для удобства тестирования я рекомендую также добавить:

  • http://127.0.0.1:3000
  • http://127.0.0.1:3000/api/auth/callback/google
  • https://developers.google.com/oauthplayground

Наконец, нажмите кнопку «Создать», чтобы сгенерировать учетные данные:

Красная стрелка указывает на кнопку Create внизу формы

Вам будут представлены «Идентификатор клиента» и «Секрет клиента». Запишите их, так как они понадобятся нам на следующем этапе.

Двигаясь дальше, вернитесь в свой проект Django и добавьте следующее приложение в INSTALLED_APPS:

# core/settings.py

INSTALLED_APPS = [
    # ...
    "allauth.socialaccount.providers.google",
]

Далее добавьте настройку SOCIALACCOUNT_PROVIDERS  в core/settings.py следующим образом:

# core/settings.py

SOCIALACCOUNT_PROVIDERS = {
    "google": {
        "APP": {
            "client_id": "<your google client id>",  # replace me
            "secret": "<your google secret>",        # replace me
            "key": "",                               # leave empty
        },
        "SCOPE": [
            "profile",
            "email",
        ],
        "AUTH_PARAMS": {
            "access_type": "online",
        },
        "VERIFIED_EMAIL": True,
    },
}

Обязательно замените <your google client id> и <your google secret> на ваши реальные ключи. Оставьте свойство key пустым, поскольку оно не требуется для социальной аутентификации Google.

Добавьте в authentication/views.py новое представление, которое наследует от SocialLoginView:

# authentication/views.py

from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client


class GoogleLogin(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter
    callback_url = "http://127.0.0.1:3000/"
    client_class = OAuth2Client

Наконец, добавьте в authentication/urls.py следующие два URL:

# authentication/urls.py

from allauth.socialaccount.views import signup
from authentication.views import GoogleLogin

urlpatterns = [
    # ...
    path("google/", GoogleLogin.as_view(), name="google_login"),
]

Не забывайте об импорте:

from authentication.views import GoogleLogin

Тестирование

Для проверки работоспособности эндпоинта API необходимо сначала получить id_token и затем отправить его на эндпоинт бэкенда. Проще всего получить id_token через Google’s OAuth 2.0 Playground.

Далее передайте id_token в качестве access_token в теле запроса cURL:

$ curl -XPOST -H "Content-type: application/json" -d '{
    "access_token": "<your_id_token>"
  }' 'http://localhost:8000/api/auth/google/'

Вы должны получить аналогичный ответ:

{
  "access": "eyJhb...",
  "refresh": "eyJhb...",
  "user": {
    "pk": 2,
    "username": "user2",
    "email": ""
  }
}

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

Фронтенд

В этом разделе мы напишем проект на Next.js, настроим Chakra UI, установим и настроим Auth.js, включим аутентификацию на основе учетных данных и социальную аутентификацию. Наконец, мы протестируем систему аутентификации.

Веб-приложение будет иметь следующие эндпоинты:

  1. / предлагает пользователю войти в систему
  2. /profile отображает информацию об учетной записи пользователя
  3. /api/auth/... обрабатывается динамическим API-маршрутизатором Auth.js

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

Настройка Next.js

Начните с вызова create-next-app для генерации проекта Next.js:

$ npx create-next-app@13.4.9 django-rest-authjs-frontend

√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... No
√ Would you like to use `src/` directory? ... No
√ Would you like to use App Router? (recommended) ... No
√ Would you like to customize the default import alias? ... No

Creating a new Next.js app in ~\django-rest-authjs-frontend.

Для простоты я рекомендую ответить «Да» только на TypeScript и ESLint.

Настройка пользовательского интерфейса Chakra

Для упрощения процесса создания пользовательского интерфейса мы будем использовать Chakra UI — минималистичную, модульную и доступную библиотеку компонентов для быстрого создания пользовательских интерфейсов React. В библиотеке есть встроенные компоненты, специализированные хуки, система стилей и многое другое.

Не стесняйтесь заменять Chakra UI на MUIAntDesignTailwindCSS или любую другую библиотеку фронтенда.

Установите библиотеку, выполнив следующую команду:

$ npm i @chakra-ui/react@2.5.5 @chakra-ui/next-js@2.1.4 @emotion/react@11.10.6 \
        @emotion/styled@11.10.6 framer-motion@10.12.4

Далее создайте новую тему с помощью extendTheme() и оберните Component с ChakraProvider в pages/_app.tsx примерно так:

// pages/_app.tsx

import type {AppProps} from "next/app";
import {ChakraProvider, extendTheme} from "@chakra-ui/react";

export const theme = extendTheme({});

export default function App({Component, pageProps: {session, ...pageProps}}: AppProps) {
  return (
    <ChakraProvider theme={theme}>
      <Component {...pageProps} />
    </ChakraProvider>
  );
}

Не забывайте об импорте:

import {ChakraProvider, extendTheme} from "@chakra-ui/react";

Мы создали новую тему, поскольку в следующем шаге нам нужно будет передать ее скрипту цветового режима. Кроме того, определение темы таким образом позволяет в будущем легко настроить Chakra.

Далее займемся сценарием цветового режима.

Скрипт цветового режима обеспечивает корректную работу синхронизации локального хранилища. Он должен быть добавлен перед любым содержимым (в нашем случае в файл pages/_document.tsx).

Включите ColorModeScript в свой pages/_document.tsx примерно так:

// pages/_document.tsx

import {Head, Html, Main, NextScript} from "next/document";
import {ColorModeScript} from "@chakra-ui/react";
import {theme} from "./_app";

export default function Document() {
  return (
    <Html lang="en">
      <Head/>
      <body>
        <ColorModeScript initialColorMode={theme.config.initialColorMode}/>
        <Main/>
        <NextScript/>
      </body>
    </Html>
  );
}

Опять же, не стоит забывать об импорте:

import {ColorModeScript} from "@chakra-ui/react";
import {theme} from "./_app";

Views

Давайте заменим стандартную индексную страницу Next.js на приглашение к входу в систему.

Перейдите к pages/index.tsx и замените его содержимое на следующее:

// pages/index.tsx

import {Box, Button, Spinner, Text, VStack} from "@chakra-ui/react";

export default function Home() {
  return (
    <Box m={8}>
      <VStack>
        <Text>You are not authenticated.</Text>
        <Button colorScheme="blue" onClick={() => console.log("Beep boop")}>
          Sign in
        </Button>
      </VStack>
    </Box>
  );
}

Запустите сервер разработки:

$ npx next dev

Откройте ваш любимый браузер и перейдите по адресу http://127.0.0.1:3000/. На экране должна появиться страница с сообщением «Вы не авторизованы» и кнопкой «Войти». При нажатии на кнопку в консоль должно быть выведено отладочное сообщение.

Сообщение "You are not authenticated"

Отлично. На этом начальная настройка завершена.

Аутентификация на основе учетных данных

В этом разделе мы установим Auth.js и займемся аутентификацией на основе учетных данных.

Axios

Перед началом работы с Auth.js установим Axios — простой HTTP-клиент на основе обещаний для браузера и Node.js. Эта библиотека позволит нам отправлять HTTP-запросы к нашему бэкенду с помощью элегантного и лаконичного синтаксиса.

Установите его с помощью NPM:

$ npm install axios@1.3.6

Auth.js

Двигаясь дальше, установите Auth.js:

$ npm install next-auth@4.22.1

Для корректной работы Auth.js сначала необходимо задать некоторые переменные окружения. В Next.js переменные окружения автоматически загружаются из  .env.local корня проекта.

Создайте .env.local со следующим содержимым:

# .env.local

NEXTAUTH_URL=http://127.0.0.1:3000/
NEXTAUTH_SECRET=verycomplexsecretkey
NEXTAUTH_BACKEND_URL=http://127.0.0.1:8000/api/
NEXT_PUBLIC_BACKEND_URL=http://127.0.0.1:8000/api/

Далее необходимо создать обработчик динамических маршрутов и папку конфигураций Auth.js. Создайте папку auth в папке pages/api и файл […nextauth].js  во вновь созданной папке.

pages/
├── api/
│   └── auth/
│       └── [...nextauth].js
├── _app.tsx
├── _document.tsx
└── index.tsx

Прежде чем приступить к работе с кодом, давайте поясним, как работают сессии Auth.js. Auth.js имеет два встроенных способа работы с сессиями:

  1. Использование базы данных (сессия хранится в базе данных)
  2. Использование JWT-токена (сессия хранится в токене)

Поскольку в качестве бэкенда мы используем Django, то придется воспользоваться вторым подходом. Недостатком этого подхода является утомительная передача аргументов и хранение accessTokenrefreshToken и других чувствительных переменных в JWT-токене.

Вставьте следующее содержимое в […nextauth].js:

// pages/api/auth/[...nextauth].js

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import axios from "axios";

// These two values should be a bit less than actual token lifetimes
const BACKEND_ACCESS_TOKEN_LIFETIME = 45 * 60;            // 45 minutes
const BACKEND_REFRESH_TOKEN_LIFETIME = 6 * 24 * 60 * 60;  // 6 days

const getCurrentEpochTime = () => {
  return Math.floor(new Date().getTime() / 1000);
};

const SIGN_IN_HANDLERS = {
  "credentials": async (user, account, profile, email, credentials) => {
    return true;
  },
};
const SIGN_IN_PROVIDERS = Object.keys(SIGN_IN_HANDLERS);

export const authOptions = {
  secret: process.env.AUTH_SECRET,
  session: {
    strategy: "jwt",
    maxAge: BACKEND_REFRESH_TOKEN_LIFETIME,
  },
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        username: {label: "Username", type: "text"},
        password: {label: "Password", type: "password"}
      },
      // The data returned from this function is passed forward as the
      // `user` variable to the signIn() and jwt() callback
      async authorize(credentials, req) {
        try {
          const response = await axios({
            url: process.env.NEXTAUTH_BACKEND_URL + "auth/login/",
            method: "post",
            data: credentials,
          });
          const data = response.data;
          if (data) return data;
        } catch (error) {
          console.error(error);
        }
        return null;
      },
    }),
  ],
  callbacks: {
    async signIn({user, account, profile, email, credentials}) {
      if (!SIGN_IN_PROVIDERS.includes(account.provider)) return false;
      return SIGN_IN_HANDLERS[account.provider](
        user, account, profile, email, credentials
      );
    },
    async jwt({user, token, account}) {
      // If `user` and `account` are set that means it is a login event
      if (user && account) {
        let backendResponse = account.provider === "credentials" ? user : account.meta;
        token["user"] = backendResponse.user;
        token["access_token"] = backendResponse.access;
        token["refresh_token"] = backendResponse.refresh;
        token["ref"] = getCurrentEpochTime() + BACKEND_ACCESS_TOKEN_LIFETIME;
        return token;
      }
      // Refresh the backend token if necessary
      if (getCurrentEpochTime() > token["ref"]) {
        const response = await axios({
          method: "post",
          url: process.env.NEXTAUTH_BACKEND_URL + "auth/token/refresh/",
          data: {
            refresh: token["refresh_token"],
          },
        });
        token["access_token"] = response.data.access;
        token["refresh_token"] = response.data.refresh;
        token["ref"] = getCurrentEpochTime() + BACKEND_ACCESS_TOKEN_LIFETIME;
      }
      return token;
    },
    // Since we're using Django as the backend we have to pass the JWT
    // token to the client instead of the `session`.
    async session({token}) {
      return token;
    },
  }
};

export default NextAuth(authOptions);

Примечания:

  1. Сначала мы определили время жизни токенов бэкенда и метод получения времени UNIX.
  2. Обработчики входа в систему будут использоваться только при социальной аутентификации. При использовании аутентификации на основе учетных данных проверка уже выполняется в методе authorize().
  3. Затем мы определили authOptions и добавили CredentialsProvider в его провайдеры.
  4. В CredentialsProvider мы определили, какие поля должна содержать форма входа, и определили метод authorize(), который передает учетные данные на бэкенд для их проверки.
  5. Для присоединения данных к JWT-токену и передачи их клиенту мы использовали обратные вызовы signIn()jwt() и session(). Обновление токена на бэкенде реализовано в колбэке jwt().

Чтобы узнать больше об обратных вызовах Auth.js, ознакомьтесь с Auth.js Callbacks.

Наконец, оберните ваше приложение с помощью SessionProvider в pages/_app.tsx следующим образом:

// pages/_app.tsx

import type {AppProps} from "next/app";
import {SessionProvider} from "next-auth/react";
import {ChakraProvider, extendTheme} from "@chakra-ui/react";

export const theme = extendTheme({});

export default function App({Component, pageProps: {session, ...pageProps}}: AppProps) {
  return (
    <SessionProvider session={session}>
      <ChakraProvider theme={theme}>
        <Component {...pageProps} />
      </ChakraProvider>
    </SessionProvider>
  );
}

Не забывайте об импорте:

import {SessionProvider} from "next-auth/react";

Провайдер SessionProvider обеспечит доступ к сессии через хук useSession().

Для внедрения типа Session создайте в корне проекта новую папку с именем types. Затем добавьте в нее следующий файл с именем next-auth.d.ts.

Views

Теперь давайте поработаем с представлениями.

Сначала измените свой pages/index.tsx следующим образом:

// pages/index.tsx

import {useRouter} from "next/router";
import {signIn, useSession} from "next-auth/react";
import {Box, Button, Spinner, Text, VStack} from "@chakra-ui/react";

export default function Home() {

  const router = useRouter();
  const {data: session, status} = useSession();

  if (status == "loading") {
    return <Spinner size="lg"/>;
  }

  // If the user is authenticated redirect to `/profile`
  if (session) {
    router.push("profile");
    return;
  }

  return (
    <Box m={8}>
      <VStack>
        <Text>You are not authenticated.</Text>
        <Button
            colorScheme="blue"
            onClick={() => signIn(undefined, {callbackUrl: "/profile"})}
        >
          Sign in
        </Button>
      </VStack>
    </Box>
  );
}

Примечания:

  1. Мы использовали хук useSession()  для получения сессии от SessionProvider.
  2. Во время загрузки сессии пользователю представляется компонент Spinner.
  3. В случае если пользователь уже вошел в систему, мы перенаправляем его на /profile через useRouter().
  4. Кнопка входа в систему теперь вызывает метод signIn().

Далее создайте в папке pages новый файл с именем profile.tsx со следующим содержимым:

// pages/profile.tsx

import {useState} from "react";
import {signOut, useSession} from "next-auth/react";
import {Box, Button, Code, HStack, Spinner, Text, VStack} from "@chakra-ui/react";
import axios from "axios";

export default function Home() {

  const {data: session, status} = useSession({required: true});
  const [response, setResponse] = useState("{}");

  const getUserDetails = async (useToken: boolean) => {
    try {
      const response = await axios({
        method: "get",
        url: process.env.NEXT_PUBLIC_BACKEND_URL + "auth/user/",
        headers: useToken ? {Authorization: "Bearer " + session?.access_token} : {},
      });
      setResponse(JSON.stringify(response.data));
    } catch (error) {
      setResponse(error.message);
    }
  };

  if (status == "loading") {
    return <Spinner size="lg"/>;
  }

  if (session) {
    return (
      <Box m={8}>
        <VStack>
          <Text>PK: {session.user.pk}</Text>
          <Text>Username: {session.user.username}</Text>
          <Text>Email: {session.user.email || "Not provided"}</Text>
          <Code>
            {response}
          </Code>
        </VStack>
        <HStack justifyContent="center" mt={4}>
          <Button colorScheme="blue" onClick={() => getUserDetails(true)}>
            User details (with token)
          </Button>
          <Button colorScheme="orange" onClick={() => getUserDetails(false)}>
            User details (without token)
          </Button>
          <Button colorScheme="red" onClick={() => signOut({callbackUrl: "/"})}>
            Sign out
          </Button>
        </HStack>
      </Box>
    );
  }

  return <></>;
}

Примечания:

  1. Мы использовали useRouter() и useSession() так же, как и в предыдущем фрагменте кода.
  2. Мы добавили метод getUserDetails(), который получает информацию о пользователе и отображает ее.
  3. При нажатии на кнопку выхода из системы вызывается метод signOut().

Тестирование

Перезапустите сервер разработки Next:

$ npx next dev

Затем откройте свой любимый браузер и перейдите по адресу http://127.0.0.1:3000. Нажмите кнопку «Войти» и убедитесь, что вы перенаправлены на форму регистрации. Заполните форму и нажмите кнопку «Войти с учетными данными».

Окно входа в систему. Стрелка указывает на кнопку Sign with Credentials

При успешном входе в систему вы будете перенаправлены на страницу профиля. Попробуйте получить информацию о пользователе, используя свой accessToken.

Стрелка указывает на кнопку User details (with token)

Социальная аутентификация с помощью Google

В этом разделе мы настроим социальную аутентификацию с помощью Google. Интеграция других социальных провайдеров с Auth.js практически аналогична. Просто убедитесь, что ваш бэкенд Django работает, прежде чем переходить к фронтенду.

Не стесняйтесь пропустить этот раздел, если вас не интересует социальная аутентификация.

Настройка

Во-первых, добавьте в .env.local две переменные окружения:

# .env.local

# ...
GOOGLE_CLIENT_ID=<your_google_client_id>
GOOGLE_CLIENT_SECRET=<your_google_secret>

Обязательно замените <your_google_client_id> и <your_google_secret> на ваши реальные ключи. Помните, что ключи должны совпадать с ключами, указанными в core/settings.py.

Далее зарегистрируйте обработчик входа в систему Google в […nextauth].js:

// pages/api/auth/[...nextauth].js

const SIGN_IN_HANDLERS = {
  // ...
  "google": async (user, account, profile, email, credentials) => {
    try {
      const response = await axios({
        method: "post",
        url: process.env.NEXTAUTH_BACKEND_URL + "auth/google/",
        data: {
          access_token: account["id_token"]
        },
      });
      account["meta"] = response.data;
      return true;
    } catch (error) {
      console.error(error);
      return false;
    }
  }
};

Единственное назначение этого обработчика — переслать id_token на эндпоинт Django social. Все остальное — создание аккаунта, регистрацию пользователя и т.д. — Django берет на себя.

Затем добавьте GoogleProvider в нижнюю часть массива providers  в […nextauth].js:

// pages/api/auth/[...nextauth].js

export const authOptions = {
  // ...
  providers: [
    // ...
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code"
        }
      }
    }),
  ],
  // ...
};

Не забудьте импортировать GoogleProvider в верхней части файла:

import GoogleProvider from "next-auth/providers/google";

Это все, что мы должны сделать.

Тестирование

Перед тестированием убедитесь, что ваш бэкенд Django запущен и доступен по URL, заданному в .env.local. После этого завершите процесс сервера разработки Next и запустите его снова:

$ npx next dev

Перейдите на сайт http://127.0.0.1:3000/ в вашем любимом браузере и нажмите на кнопку «Войти». Выберите «Войти с помощью Google» и проверьте, работает ли социальная аутентификация.

Окно входа в систему. Стрелка указывает на кнопку Sign in with Google

Заключение

В этом руководстве мы успешно реализовали систему аутентификации Django REST и связали ее с Auth.js. Реализованная система поддерживает аутентификацию на основе учетных данных, а также социальную аутентификацию с помощью Google. Она построена таким образом, что в нее легко можно добавить других социальных провайдеров, таких как Twitter, GitHub и т.д.

Исходный код доступен в GitHub-репозитории django-rest-authjs.

Дальнейшие шаги

  1. Обязательно реализуйте ротацию токенов refresh для социальных провайдеров.
  2. По умолчанию Auth.js не заботится о создании учетных записей. Если вы хотите включить регистрацию пользователей, то либо сделайте это в методе authorize(), либо создайте специальную страницу регистрации.
  3. Для включения функций проверки электронной почты и сброса пароля ознакомьтесь с разделом «Проверка электронной почты и сброс пароля» в статье Django REST Framework Authentication.
  4. Чтобы включить функцию привязки социальных аккаунтов, обратитесь к «Social Connect Views».