気が狂うことなくOAuth2をDjango/DRFバックエンドに統合する方法
公開: 2022-03-11私たちは皆そこにいました。 あなたはAPIバックエンドに取り組んでおり、それがどのように進んでいるかに満足しています。 最近、Minimum Viable Product(MVP)を完了し、テストはすべて合格しており、いくつかの新機能の実装を楽しみにしています。
次に、上司からメールが送信されます。「ちなみに、FacebookとGoogleを介してログインできるようにする必要があります。 私たちのような小さなサイトのためだけにアカウントを作成する必要はありません。」
素晴らしい。 スコープクリープが再び攻撃します。
幸いなことに、OAuth 2はソーシャル認証およびサードパーティ認証(Facebook、Googleなどのサービスで使用される)の業界標準として登場したため、幅広いソーシャルをサポートするためにその標準の理解と実装に集中できます。認証プロバイダー。
OAuth2に慣れていない可能性があります。 これが私に起こったとき、私はそうではありませんでした。
Python開発者として、あなたの本能は、PythonパッケージをインストールするためのPython Package Index(PyPA)推奨ツールであるpipにつながる可能性があります。 悪いニュースは、pipがOAuthを処理する278個のパッケージについて知っていることです。そのうち53個は特にDjangoに言及しています。 オプションを調査するだけで1週間の作業になります。コードを書き始めてもかまいません。
このチュートリアルでは、PythonSocialAuthを使用してOAuth2をDjangoまたはDjangoRestFrameworkに統合する方法を学習します。 この記事はDjangoRESTフレームワークに焦点を当てていますが、ここで提供される情報を適用して、他のさまざまな一般的なバックエンドフレームワークに同じものを実装できます。
OAuth2フローの概要
OAuth 2は、最初からWeb認証プロトコルとして設計されました。 これは、ネット認証プロトコルとして設計された場合とはまったく同じではありません。 HTMLレンダリングやブラウザリダイレクトなどのツールが利用可能であることを前提としています。
これは明らかにJSONベースのAPIの障害のようなものですが、これを回避することができます。
従来のサーバー側のWebサイトを作成しているかのようにプロセスを実行します。
サーバー側のOAuth2フロー
最初のステップは、アプリケーションフローの外部で完全に行われます。 プロジェクトの所有者は、ログインが必要な各OAuth2プロバイダーにアプリケーションを登録する必要があります。
この登録中に、OAuth 2プロバイダーにコールバックURIを提供します。このコールバックで、アプリケーションはリクエストを受信できるようになります。 代わりに、クライアントキーとクライアントシークレットを受け取ります。 これらのトークンは、ログイン要求を検証するために認証プロセス中に交換されます。
トークンは、サーバーコードをクライアントとして参照します。 ホストはOAuth2プロバイダーです。 これらは、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を提供しましたが、シークレットは提供していません。 代わりに、「プロファイル」スコープと「電子メール」スコープの両方に応答してアクセスする認証コードが必要であることをサーバーに通知しました。 これらのスコープは、ユーザーに要求するアクセス許可を定義し、受け取るアクセストークンの承認を制限します。
受信すると、ユーザーのブラウザーはOAuth2プロバイダーが制御する動的ページに移動します。 OAuth 2プロバイダーは、続行する前に、コールバックURIとクライアントキーが互いに一致することを確認します。 その場合、フローはユーザーのセッショントークンに応じて一時的に分岐します。
ユーザーが現在そのサービスにログインしていない場合は、ログインするように求められます。 ログインすると、アプリケーションのログインを許可する権限を要求するダイアログがユーザーに表示されます。
ユーザーが承認すると、OAuth 2サーバーは、クエリパラメーターに認証コードを含めて指定したコールバックURIにリダイレクトします: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE
。
認証コードは、有効期限が早く、使い捨てのトークンです。 受信するとすぐに、サーバーは向きを変え、認証コードとクライアントシークレットの両方を含む別のリクエストをOAuth2プロバイダーに送信する必要があります。
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リクエストを認証することですが、フローの性質上、ユーザーのシステムを介してルーティングする必要があります。 そのため、本質的に安全ではありません。
認証コードの制限(つまり、すぐに期限切れになり、1回しか使用できない)は、信頼できないシステムを介して認証クレデンシャルを渡す固有のリスクを軽減するためにあります。
サーバーからOAuth2プロバイダーのサーバーに直接行われるこの呼び出しは、OAuth2サーバー側のログインプロセスの重要なコンポーネントです。 通話を制御するということは、通話がTLSで保護されていることを知っていることを意味します。これにより、盗聴攻撃から通話を保護できます。
認証コードを含めると、ユーザーが明示的に同意を与えることが保証されます。 ユーザーには表示されないクライアントシークレットを含めることで、このリクエストが、認証コードを傍受したユーザーのシステム上のウイルスやマルウェアから発信されないようにします。
すべてが一致すると、サーバーはアクセストークンを返します。このトークンを使用して、ユーザーとして認証されている間にそのプロバイダーに電話をかけることができます。
サーバーからアクセストークンを受信すると、サーバーはユーザーのブラウザをもう一度ログインしたばかりのユーザーのランディングページにリダイレクトします。アクセストークンはユーザーのサーバー側のセッションキャッシュに保持されるのが一般的です。サーバーは、必要なときにいつでも特定のソーシャルプロバイダーに電話をかけることができます。
アクセストークンをユーザーが利用できるようにしないでください。
私たちが掘り下げることができるより多くの詳細があります。
たとえば、Googleにはアクセストークンの寿命を延ばす更新トークンが含まれていますが、Facebookには、短命のアクセストークンを長命のアクセストークンと交換できるエンドポイントが用意されています。 ただし、このフローは使用しないため、これらの詳細は重要ではありません。
このフローは、RESTAPIにとって面倒です。 フロントエンドクライアントに初期ログインページを生成させ、バックエンドにコールバックURLを提供させることもできますが、最終的には問題が発生します。 アクセストークンを受け取ったら、ユーザーをフロントエンドのランディングページにリダイレクトする必要がありますが、これを行うための明確でRESTfulな方法はありません。
幸いなことに、別のOAuth 2フローが利用可能であり、この場合ははるかにうまく機能します。
クライアント側のOAuth2フロー
このフローでは、フロントエンドがOAuth2プロセス全体の処理を担当します。 これは一般的にサーバー側のフローに似ていますが、重要な例外があります。フロントエンドはユーザーが制御するマシン上に存在するため、クライアントシークレットを委託することはできません。 解決策は、プロセスのそのステップ全体を単純に排除することです。
サーバー側のフローと同様に、最初のステップはアプリケーションの登録です。
この場合、プロジェクトの所有者は引き続きアプリケーションを登録しますが、Webアプリケーションとして登録します。 OAuth 2プロバイダーは引き続きクライアントキーを提供しますが、クライアントシークレットを提供しない場合があります。
フロントエンドは、OAuth 2プロバイダーが制御するWebページに誘導するソーシャルログインボタンをユーザーに提供し、ユーザーのプロファイルの特定の側面にアクセスするためのアプリケーションの許可を要求します。
ただし、今回のURLは少し異なります。
https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
今回のURLのresponse_type
パラメーターはtoken
であることに注意してください。
では、リダイレクトURIはどうですか?
これは、アクセストークンを適切に処理するために準備されたフロントエンド上の任意のアドレスです。
使用中のOAuth2ライブラリによっては、フロントエンドが実際に一時的にユーザーのデバイスでHTTPリクエストを受け入れることができるサーバーを実行する場合があります。 その場合、リダイレクトURLの形式はhttp://localhost:7862/callback/?token=TOKEN
です。
OAuth 2サーバーはユーザーが承認した後にHTTPリダイレクトを返し、このリダイレクトはユーザーのデバイスのブラウザーによって処理されるため、このアドレスは正しく解釈され、フロントエンドにトークンへのアクセスを許可します。
または、フロントエンドで適切なページを直接実装することもできます。 いずれにせよ、この時点で、フロントエンドはクエリパラメータの解析とアクセストークンの処理を担当します。
この時点から、フロントエンドはトークンを使用してOAuth2プロバイダーのAPIを直接呼び出すことができます。 しかし、ユーザーはそれを本当に望んでいません。 彼らはあなたのAPIへの認証されたアクセスを望んでいます。 バックエンドが提供する必要があるのは、フロントエンドがソーシャルプロバイダーのアクセストークンをAPIへのアクセスを許可するトークンと交換できるエンドポイントだけです。
フロントエンドへのアクセストークンの提供はサーバー側のフローよりも本質的に安全性が低いため、これを許可するのはなぜですか?
クライアント側のフローにより、バックエンドのRESTAPIとユーザー向けのフロントエンドをより厳密に分離できます。 バックエンドサーバーをリダイレクトURIとして指定することを厳密に妨げるものは何もありません。 最終的な効果は、ある種のハイブリッドフローになります。
問題は、サーバーが適切なユーザー向けページを生成してから、何らかの方法で制御をフロントエンドに戻す必要があることです。
最近のプロジェクトでは、フロントエンドUIとすべてのビジネスロジックを処理するバックエンドの間で関心の分離を厳密に行うのが一般的です。 これらは通常、明確に定義されたJSONAPIを介して通信します。 上記のハイブリッドフローは、関心の分離を混乱させますが、バックエンドにユーザー向けのページを提供し、何らかのフローを設計してフロントエンドに戻るように制御します。

フロントエンドがアクセストークンを処理できるようにすることは、関心の分離を維持するための便利な手法です。 侵害されたクライアントからのリスクがいくらか増加しますが、一般的にはうまく機能します。
このフローはフロントエンドにとって複雑に見えるかもしれません。フロントエンドチームがすべてを自分で開発する必要がある場合はそうです。 ただし、FacebookとGoogleはどちらも、最小限の構成でプロセス全体を処理するログインボタンをフロントエンドに含めることができるライブラリを提供しています。
これがバックエンドでのトークン交換のレシピです。
クライアントフローでは、バックエンドはOAuth2プロセスからかなり分離されています。 誤解しないでください:これは簡単な仕事ではありません。 少なくとも次の機能をサポートする必要があります。
- フロントエンドが提供したトークンが有効であることを確認するために、少なくとも1つのリクエストをOAuth 2プロバイダーに送信します。任意のランダムな文字列ではありませんが、
- トークンが有効な場合は、APIの有効なトークンを返します。 それ以外の場合は、情報エラーを返します。
- これが新規ユーザーの場合は、そのユーザーの
User
モデルを作成し、適切に入力します。 - これが
User
モデルがすでに存在するユーザーである場合は、電子メールアドレスで一致させて、ソーシャルログイン用に新しいアカウントを作成する代わりに、正しい既存のアカウントにアクセスできるようにします。 - ユーザーがソーシャルメディアで提供した内容に基づいて、ユーザーのプロファイルの詳細を更新します。
幸いなことに、このすべての機能をバックエンドに実装するのは、予想よりもはるかに簡単です。
これが、わずか20行のコードでこれらすべてをバックエンドで機能させる方法の魔法です。 これはPythonSocialAuthライブラリ(以降「PSA」)に依存するため、requirements.txtにsocial-auth-core
とsocial-auth-app-django
両方を含める必要がありrequirements.txt
。
また、ここに記載されているようにライブラリを構成する必要があります。 わかりやすくするために、これは一部の例外処理を除外していることに注意してください。
この例の完全なコードはここにあります。
@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は、非常にクールで非常に複雑な機械です。 認証と数十のソーシャル認証プロバイダーへのアクセスを処理することは完全に満足しており、Django、Flask、Pyramid、CherryPy、WebPyなどの最も人気のあるPythonWebフレームワークで動作します。
ほとんどの場合、上記のコードは非常に標準的なDjango RESTフレームワーク(DRF)関数ベースのビューです。urls.pyでマップしたパスでPOSTリクエストをリッスンし、リクエストをurls.py
で送信すると仮定します。期待するフォーマット、それからあなたにUser
オブジェクト、またはNone
を取得します。
User
オブジェクトを取得した場合、それはプロジェクトの他の場所で構成したモデルタイプであり、すでに存在している場合と存在していない場合があります。 PSAは、トークンの検証、ユーザーの一致が存在するかどうかの識別、必要に応じたユーザーの作成、およびソーシャルプロバイダーからのユーザーの詳細の更新をすでに処理しています。
ユーザーがソーシャルプロバイダーのユーザーからあなたのユーザーにマッピングされ、既存のユーザーに関連付けられる方法の正確な詳細は、上記で定義されたSOCIAL_AUTH_PIPELINE
によって指定されます。 これがどのように機能するかについて学ぶことはもっとたくさんありますが、それはこの投稿の範囲外です。 あなたはここでそれについてもっと読むことができます。
魔法の重要な部分は、ビューの@psa()
デコレータです。これは、ビューに渡されるrequest
オブジェクトにいくつかのメンバーを追加します。 私たちにとって最も興味深いのはrequest.backend
です(PSAにとって、バックエンドは任意のソーシャル認証プロバイダーです)。
適切なバックエンドが選択され、ビューへのbackend
引数に基づいてrequest
オブジェクトに追加され、URL自体が入力されます。
backend
オブジェクトを入手したら、アクセスコードを指定して、そのプロバイダーに対して認証することができます。 それがdo_auth
メソッドです。 これにより、構成ファイルからSOCIAL_AUTH_PIPELINE
全体が使用されます。
パイプラインを拡張すると、パイプラインは非常に強力なことを実行できますが、デフォルトの組み込み機能だけで、必要なすべてのことをすでに実行しています。
その後、通常のDRFコードに戻ります。有効なUser
オブジェクトを取得した場合は、適切なAPIトークンを非常に簡単に返すことができます。 有効なUser
オブジェクトを取得しなかった場合、エラーが発生しやすくなります。
この手法の欠点の1つは、エラーが発生した場合にエラーを返すのは比較的簡単ですが、具体的に何が悪かったのかについて多くの洞察を得るのが難しいことです。 PSAは、サーバーが問題の内容について返した可能性のある詳細をすべて飲み込みます。
繰り返しになりますが、適切に設計された認証システムの性質上、エラーの原因についてはかなり不透明です。 ログイン試行後にアプリケーションがユーザーに「無効なパスワード」と通知した場合、それは「おめでとうございます! 有効なユーザー名を推測しました。」
自分で転がしてみませんか?
一言で言えば:拡張性。 まったく同じ方法でAPI呼び出しでまったく同じ情報を必要とする、または返すソーシャルOAuth2プロバイダーはほとんどありません。 ただし、あらゆる種類の特殊なケースと例外があります。
すでにPSAを設定した後で新しいソーシャルプロバイダーを追加することは、設定ファイルの数行の構成の問題です。 コードを調整する必要はまったくありません。 PSAはそれらすべてを抽象化するため、独自のアプリケーションに集中できます。
いったいどうやってこれをテストするのですか?
良い質問! unittest.mock
は、ライブラリの奥深くにある抽象化レイヤーの下に埋め込まれたAPI呼び出しをモックアウトするのには適していません。 モックへの正確な道を見つけるだけでもかなりの努力が必要です。
代わりに、PSAはRequestsライブラリの上に構築されているため、優れたResponsesライブラリを使用して、HTTPレベルでプロバイダーをモックアウトします。
テストの完全な説明はこの記事の範囲を超えていますが、テストのサンプルがここに含まれています。 SocialAuthTests
mocked
があることに注意する特定の関数。
PSAに手間のかかる作業を任せます。
OAuth2プロセスは詳細で複雑であり、多くの固有の複雑さがあります。 幸いなことに、可能な限り簡単な方法で処理するための専用のライブラリを導入することで、その複雑さの多くを回避することができます。
PythonSocialAuthはその点で素晴らしい仕事をしています。 クライアント側の暗黙的なOAuth2フローを利用して、わずか25行のコードでシームレスなユーザー作成とマッチングを実現するDjango/DRFビューを示しました。 それはそれほど粗末ではありません。