Как интегрировать OAuth 2 в ваш сервер Django/DRF, не сойдя с ума

Опубликовано: 2022-03-11

Мы все были там. Вы работаете над серверной частью API и довольны тем, как все идет. Вы недавно завершили минимальный жизнеспособный продукт (MVP), все тесты пройдены, и вы с нетерпением ждете реализации некоторых новых функций.

Затем начальник отправляет вам электронное письмо: «Кстати, нам нужно разрешить людям авторизоваться через Facebook и Google; им не нужно создавать учетную запись только для такого маленького сайта, как наш».

Здорово. Ползучесть масштаба снова наносит удар.

Хорошей новостью является то, что OAuth 2 стал отраслевым стандартом для социальной и сторонней аутентификации (используемой такими службами, как Facebook, Google и т. д.), поэтому вы можете сосредоточиться на понимании и реализации этого стандарта для поддержки широкого спектра социальных сетей. провайдеры аутентификации.

Вероятно, вы не знакомы с OAuth 2; Я не был, когда это случилось со мной.

Интегрируйте OAuth 2 в серверную часть Django/DRF

Как разработчик Python, ваше чутье может привести вас к pip, рекомендуемому инструменту Python Package Index (PyPA) для установки пакетов Python. Плохая новость заключается в том, что pip знает о 278 пакетах, связанных с OAuth, в 53 из которых конкретно упоминается Django. Это стоит недели работы только для того, чтобы изучить варианты, не говоря уже о том, чтобы начать писать код.

В этом руководстве вы узнаете, как интегрировать OAuth 2 в Django или Django Rest Framework с помощью Python Social Auth. Хотя эта статья посвящена Django REST Framework, вы можете применить представленную здесь информацию, чтобы реализовать то же самое во множестве других распространенных серверных фреймворков.

Краткий обзор потока OAuth 2

OAuth 2 с самого начала разрабатывался как протокол веб-аутентификации. Это не совсем то же самое, как если бы он был разработан как сетевой протокол аутентификации; предполагается, что вам доступны такие инструменты, как рендеринг HTML и перенаправления браузера.

Это, очевидно, является препятствием для API на основе JSON, но вы можете обойти это.

Вы пройдете через этот процесс, как если бы вы писали традиционный веб-сайт на стороне сервера.

Поток OAuth 2 на стороне сервера

Первый шаг происходит полностью вне потока приложения. Владелец проекта должен зарегистрировать ваше приложение у каждого провайдера OAuth 2, для которого вам нужны логины.

Во время этой регистрации они предоставляют провайдеру OAuth 2 callback URI , по которому ваше приложение будет доступно для приема запросов. Взамен они получают клиентский ключ и клиентский секрет . Эти токены обмениваются в процессе аутентификации для проверки запросов на вход.

Токены ссылаются на код вашего сервера как на клиент. Хост является поставщиком OAuth 2. Они не предназначены для клиентов вашего API.

Процесс начинается, когда ваше приложение создает страницу с кнопкой, например «Войти через Facebook» или «Войти через Google+». По сути, это не что иное, как простые ссылки, каждая из которых указывает на URL-адрес, подобный следующему:

 https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email

(Примечание: разрывы строк вставлены в указанный выше URI для удобочитаемости.)

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

После получения браузер пользователя перенаправляется на динамическую страницу, контролируемую поставщиком OAuth 2. Прежде чем продолжить, поставщик OAuth 2 проверяет, совпадают ли URI обратного вызова и ключ клиента. Если это так, поток ненадолго расходится в зависимости от токенов сеанса пользователя.

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

Предполагая, что пользователь подтверждает, сервер OAuth 2 затем перенаправляет его обратно на указанный вами URI обратного вызова, включая код авторизации в параметрах запроса: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE .

Код авторизации — это одноразовый токен с быстрым сроком действия; сразу же после его получения ваш сервер должен развернуться и сделать еще один запрос к провайдеру OAuth 2, включая как код авторизации, так и секрет вашего клиента:

 POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET

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

Ограничения на код авторизации (т. е. срок его действия быстро истекает и его можно использовать только один раз) призваны снизить неотъемлемый риск передачи учетных данных аутентификации через ненадежную систему.

Этот вызов, сделанный непосредственно с вашего сервера на сервер поставщика OAuth 2, является ключевым компонентом процесса входа в систему OAuth 2 на стороне сервера. Управление вызовом означает, что вы знаете, что вызов защищен с помощью TLS, что помогает защитить его от атак с прослушиванием телефонных разговоров.

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

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

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

Токен доступа никогда не должен быть доступен пользователю!

Есть больше деталей, в которые мы могли бы углубиться.

Например, Google включает токен обновления , который продлевает срок службы вашего токена доступа, а Facebook предоставляет конечную точку, в которой вы можете обменять недолговечные токены доступа на что-то более долговечное. Однако эти детали не имеют для нас значения, потому что мы не собираемся использовать этот поток.

Этот процесс громоздок для REST API. Несмотря на то, что внешний клиент может сгенерировать начальную страницу входа, а внутренний предоставить URL-адрес обратного вызова, в конечном итоге вы столкнетесь с проблемой. Вы хотите перенаправить пользователя на целевую страницу внешнего интерфейса после получения токена доступа, и нет четкого RESTful способа сделать это.

К счастью, есть еще один доступный поток OAuth 2, который в этом случае работает намного лучше.

Поток OAuth 2 на стороне клиента

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

Первый шаг, как и в потоке на стороне сервера, — это регистрация приложения.

В этом случае владелец проекта все равно регистрирует приложение, но уже как веб-приложение. Поставщик OAuth 2 по-прежнему будет предоставлять ключ клиента , но может не предоставлять никакого секрета клиента.

Внешний интерфейс предоставляет пользователю кнопку социального входа, которая направляет на веб-страницу, контролируемую поставщиком OAuth 2, и запрашивает разрешение для нашего приложения на доступ к определенным аспектам профиля пользователя.

Однако на этот раз URL-адрес выглядит немного иначе:

 https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email

Обратите внимание, что параметр response_type на этот раз в URL-адресе — это token .

Так что насчет URI перенаправления?

Это просто любой адрес на внешнем интерфейсе, который подготовлен для надлежащей обработки токена доступа.

В зависимости от используемой библиотеки OAuth 2 внешний интерфейс может фактически временно запускать сервер, способный принимать HTTP-запросы на устройстве пользователя; в этом случае URL-адрес перенаправления имеет вид http://localhost:7862/callback/?token=TOKEN .

Поскольку сервер OAuth 2 возвращает перенаправление HTTP после того, как пользователь принял его, и это перенаправление обрабатывается браузером на устройстве пользователя, этот адрес интерпретируется правильно, предоставляя внешнему интерфейсу доступ к токену.

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

С этого момента внешний интерфейс может напрямую вызывать API поставщика OAuth 2, используя токен. Но пользователи на самом деле этого не хотят; им нужен аутентифицированный доступ к вашему API. Все, что серверной части необходимо предоставить, — это конечная точка, в которой внешний интерфейс может обменять токен доступа социального провайдера на токен, предоставляющий доступ к вашему API.

Зачем вообще разрешать это, учитывая, что предоставление токена доступа к внешнему интерфейсу по своей сути менее безопасно, чем поток на стороне сервера?

Поток на стороне клиента обеспечивает более строгое разделение между внутренним REST API и пользовательским интерфейсом. Ничто не мешает вам указать внутренний сервер в качестве URI перенаправления; конечным эффектом будет своего рода гибридный поток.

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

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

Разрешение внешнему интерфейсу обрабатывать токен доступа — это целесообразный метод, сохраняющий разделение задач. Это несколько увеличивает риск от скомпрометированного клиента, но в целом работает хорошо.

Этот процесс может показаться сложным для фронтенда, и это так, если вам нужно, чтобы фронтенд-команда разрабатывала все самостоятельно. Однако и Facebook, и Google предоставляют библиотеки, которые позволяют интерфейсу включать кнопки входа в систему, которые обрабатывают весь процесс с минимальной конфигурацией.

Вот рецепт обмена токенами на серверной части.

В клиентском потоке серверная часть довольно изолирована от процесса OAuth 2. Не вводите себя в заблуждение: это не простая работа. Вам нужно, чтобы он поддерживал как минимум следующие функции.

  • Отправьте хотя бы один запрос поставщику OAuth 2, просто чтобы убедиться, что токен, предоставленный внешним интерфейсом, действителен, а не какая-то произвольная случайная строка.
  • Когда токен действителен, верните действительный токен для вашего API. В противном случае вернуть информативную ошибку.
  • Если это новый пользователь, создайте для него модель User и заполните ее соответствующим образом.
  • Если это пользователь, для которого уже существует модель User , сопоставьте их по адресу электронной почты, чтобы они получили доступ к правильной существующей учетной записи, а не создавали новую для входа в социальную сеть.
  • Обновите данные профиля пользователя на основе того, что они предоставили в социальных сетях.

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

Вот волшебство того, как заставить все это работать на серверной части всего за две дюжины строк кода. Это зависит от библиотеки Python Social Auth (далее «PSA»), поэтому вам необходимо включить в файл requirements.txt как social-auth-core , так и social-auth-app-django .

Вам также необходимо настроить библиотеку, как описано здесь. Обратите внимание, что это исключает некоторую обработку исключений для ясности.

Полный код для этого примера можно найти здесь.

 @api_view(http_method_names=['POST']) @permission_classes([AllowAny]) @psa() def exchange_token(request, backend): serializer = SocialSerializer(data=request.data) if serializer.is_valid(raise_exception=True): # This is the key line of code: with the @psa() decorator above, # it engages the PSA machinery to perform whatever social authentication # steps are configured in your SOCIAL_AUTH_PIPELINE. At the end, it either # hands you a populated User model of whatever type you've configured in # your project, or None. user = request.backend.do_auth(serializer.validated_data['access_token']) if user: # if using some other token back-end than DRF's built-in TokenAuthentication, # you'll need to customize this to get an appropriate token object token, _ = Token.objects.get_or_create(user=user) return Response({'token': token.key}) else: return Response( {'errors': {'token': 'Invalid token'}}, status=status.HTTP_400_BAD_REQUEST, )

В настройках осталось немного больше (полный код), и все готово:

 AUTHENTICATION_BACKENDS = ( 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.facebook.FacebookOAuth2', 'django.contrib.auth.backends.ModelBackend', ) for key in ['GOOGLE_OAUTH2_KEY', 'GOOGLE_OAUTH2_SECRET', 'FACEBOOK_KEY', 'FACEBOOK_SECRET']: # Use exec instead of eval here because we're not just trying to evaluate a dynamic value here; # we're setting a module attribute whose name varies. exec("SOCIAL_AUTH_{key} = os.environ.get('{key}')".format(key=key)) SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_uid', 'social_core.pipeline.social_auth.auth_allowed', 'social_core.pipeline.social_auth.social_user', 'social_core.pipeline.user.get_username', 'social_core.pipeline.social_auth.associate_by_email', 'social_core.pipeline.user.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', )

Добавьте сопоставление с этой функцией в свой urls.py , и все готово!

Как работает эта магия?

Python Social Auth — очень крутой и очень сложный механизм. Он прекрасно справляется с аутентификацией и доступом к любому из нескольких десятков социальных провайдеров аутентификации и работает с большинством популярных веб-фреймворков Python, включая Django, Flask, Pyramid, CherryPy и WebPy.

По большей части приведенный выше код представляет собой очень стандартное функциональное представление Django REST framework (DRF): он прослушивает POST-запросы по тому пути, на который вы сопоставляете его в своем urls.py , и, если вы отправляете ему запрос в format, который он ожидает, он затем получает объект User или None .

Если вы получаете объект User , это тип модели, который вы настроили в другом месте вашего проекта, который может уже существовать, а может и не существовать. PSA уже позаботилась о проверке токена, определении наличия совпадения пользователя, создании пользователя при необходимости и обновлении сведений о пользователе от поставщика социальных услуг.

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

Ключевым моментом магии является декоратор @psa() в представлении, который добавляет некоторые члены к объекту request , который передается в ваше представление. Наиболее интересным для нас является request.backend (для PSA бэкэнд — это любой поставщик социальной аутентификации).

Для нас был выбран соответствующий бэкэнд, который был добавлен к объекту request на основе аргумента backend для представления, которое заполняется самим URL-адресом.

Когда у вас есть backend объект, он вполне может аутентифицировать вас с помощью этого провайдера, учитывая ваш код доступа; это метод do_auth . Это, в свою очередь, задействует весь SOCIAL_AUTH_PIPELINE из вашего файла конфигурации.

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

После этого он просто возвращается к обычному коду DRF: если у вас есть действительный объект User , вы можете очень легко вернуть соответствующий токен API. Если вы не получили действительный объект User , легко сгенерировать ошибку.

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

Опять же, в природе хорошо спроектированных систем аутентификации быть довольно непрозрачными в отношении источников ошибок. Если приложение когда-либо сообщает пользователю «Неверный пароль» после попытки входа в систему, это равносильно фразе «Поздравляем! Вы угадали допустимое имя пользователя».

Почему бы просто не свернуть свой собственный?

Одним словом: расширяемость. Очень немногие поставщики социальных сетей OAuth 2 требуют или возвращают точно такую ​​же информацию в своих вызовах API точно таким же образом. Хотя бывают разные частные случаи и исключения.

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

Как мне проверить это?

Хороший вопрос! unittest.mock не очень хорошо подходит для имитации вызовов API, скрытых под слоем абстракции глубоко внутри библиотеки; просто обнаружение точного пути к mock потребует значительных усилий.

Вместо этого, поскольку PSA построен поверх библиотеки Requests, вы используете превосходную библиотеку Responses, чтобы имитировать провайдеров на уровне HTTP.

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

Пусть PSA сделает тяжелую работу.

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

Python Social Auth отлично справляется с этой задачей. Мы продемонстрировали представление Django/DRF, которое использует неявный поток OAuth2 на стороне клиента для беспрепятственного создания и сопоставления пользователей всего за 25 строк кода. Это не так уж и плохо.