私たちは皆そこにいました。あなたはAPIバックエンドに取り組んでおり、その進行状況に満足しています。最近、Minimum Viable Product(MVP)を完了し、テストはすべて合格しており、いくつかの新機能の実装を楽しみにしています。
次に、上司からメールが送信されます。「ちなみに、FacebookとGoogle経由でログインできるようにする必要があります。私たちのような小さなサイトのためだけにアカウントを作成する必要はありません。」
すごい。スコープクリープが再び発生します。
幸いなことに、OAuth 2はソーシャル認証およびサードパーティ認証(Facebook、Googleなどのサービスで使用される)の業界標準として登場したため、その標準の理解と実装に集中して、幅広い ソーシャル認証プロバイダー 。
OAuth2に慣れていない可能性があります。これが私に起こったとき、私はそうではありませんでした。
高レベルの設計ドキュメントテンプレート
として Python開発者 、あなたの本能はあなたにつながるかもしれません ピップ 、PythonパッケージをインストールするためのPython Package Index(PyPA)推奨ツール。悪いニュースは、pipがOAuthを処理する278個のパッケージについて知っていることです。そのうち53個は特に言及しています。 Django 。オプションを調査するだけで1週間の作業になります。コードを書き始めてもかまいません。
このチュートリアルでは、OAuth2をDjangoまたはに統合する方法を学習します。 Django Rest Framework を使用して Python Social Auth 。この記事はDjangoRESTフレームワークに焦点を当てていますが、ここで提供される情報を適用して、他のさまざまな一般的なバックエンドフレームワークに同じものを実装できます。
OAuth 2は、最初からWeb認証プロトコルとして設計されました。これは、ネット認証プロトコルとして設計された場合とまったく同じではありません。 HTMLレンダリングやブラウザリダイレクトなどのツールが利用可能であることを前提としています。
これは明らかにJSONベースのAPIの障害ですが、これを回避することができます。
従来のサーバー側のウェブサイトを作成しているかのようにプロセスを実行します。
最初のステップは、アプリケーションフローの外部で完全に行われます。プロジェクトの所有者は、ログインが必要な各OAuth2プロバイダーにアプリケーションを登録する必要があります。
この登録中に、OAuth2プロバイダーに コールバック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とクライアントキーが互いに一致することを確認します。その場合、フローはユーザーのセッショントークンに応じて一時的に分岐します。
ユーザーが現在そのサービスにログインしていない場合は、ログインするように求められます。ログインすると、アプリケーションのログインを許可する権限を要求するダイアログがユーザーに表示されます。
ユーザーが承認すると、OAuth2サーバーはユーザーを指定したコールバックURIにリダイレクトします。 認証コード クエリパラメータ内:GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE
。
認証コードは、有効期限が早く、1回限りのトークンです。サーバーは受信後すぐに向きを変え、認証コードとクライアントシークレットの両方を含めて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リクエストを認証することですが、フローの性質上、ユーザーのシステムを介してルーティングする必要があります。そのため、本質的に安全ではありません。
認証コードの制限(つまり、すぐに期限切れになり、一度しか使用できない)は、信頼されていないシステムを介して認証資格情報を渡す固有のリスクを軽減するためにあります。
サーバーからOAuth2プロバイダーのサーバーに直接行われるこの呼び出しは、OAuth2サーバー側のログインプロセスの重要なコンポーネントです。通話を制御するということは、通話がTLSで保護されていることがわかっているため、盗聴攻撃から通話を保護するのに役立ちます。
認証コードを含めると、ユーザーが明示的に同意を与えることが保証されます。ユーザーには表示されないクライアントシークレットを含めることで、このリクエストが、認証コードを傍受したユーザーのシステム上のウイルスやマルウェアから発信されないようにします。
すべてが一致する場合、サーバーは アクセストークン 、ユーザーとして認証されている間、そのプロバイダーに電話をかけることができます。
サーバーからアクセストークンを受信すると、サーバーはユーザーのブラウザをもう一度ログインしたばかりのユーザーのランディングページにリダイレクトします。アクセストークンはユーザーのサーバー側のセッションキャッシュに保持されるのが一般的です。サーバーは、必要なときにいつでも特定のソーシャルプロバイダーに電話をかけることができます。
アクセストークンをユーザーが利用できるようにしないでください。
私たちが掘り下げることができるより多くの詳細があります。
たとえば、Googleには 更新トークン これにより、アクセストークンの寿命が延びますが、Facebookは、短命のアクセストークンをより長命のアクセストークンと交換できるエンドポイントを提供します。ただし、このフローは使用しないため、これらの詳細は重要ではありません。
このフローは、RESTAPIにとって面倒です。フロントエンドクライアントに初期ログインページを生成させ、バックエンドにコールバックURLを提供させることもできますが、最終的には問題が発生します。アクセストークンを受け取ったら、ユーザーをフロントエンドのランディングページにリダイレクトする必要がありますが、そのための明確でRESTfulな方法はありません。
幸い、別のOAuth 2フローが利用可能であり、この場合ははるかにうまく機能します。
このフローでは、フロントエンドがOAuth2プロセス全体の処理を担当します。これは一般にサーバー側のフローに似ていますが、重要な例外があります。フロントエンドはユーザーが制御するマシン上に存在するため、クライアントシークレットを委託することはできません。解決策は、プロセスのそのステップ全体を単純に排除することです。
サーバー側のフローと同様に、最初のステップはアプリケーションの登録です。
この場合、プロジェクト所有者は引き続きアプリケーションを登録しますが、Webアプリケーションとして登録します。 OAuth2プロバイダーは引き続き クライアントキー 、ただし、クライアントシークレットを提供しない場合があります。
フロントエンドは、OAuth 2プロバイダーが制御するWebページに誘導するソーシャルログインボタンをユーザーに提供し、ユーザーのプロファイルの特定の側面にアクセスするためのアプリケーションの許可を要求します。
今回は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はどうですか?
これは、アクセストークンを適切に処理するために準備されたフロントエンド上の任意のアドレスです。
使用中のOAuth2ライブラリによっては、フロントエンドが実際に一時的にユーザーのデバイスでHTTPリクエストを受け入れることができるサーバーを実行する場合があります。その場合、リダイレクトURLの形式はhttp://localhost:7862/callback/?token=TOKEN
です。
OAuth 2サーバーはユーザーが承認した後にHTTPリダイレクトを返し、このリダイレクトはユーザーのデバイスのブラウザによって処理されるため、このアドレスは正しく解釈され、フロントエンドにトークンへのアクセスを許可します。
プログラミングにおける並行性とは
あるいは、フロントエンドは適切なページを直接実装することもできます。いずれにせよ、この時点で、フロントエンドはクエリパラメータの解析とアクセストークンの処理を担当します。
この時点から、フロントエンドはトークンを使用してOAuth2プロバイダーのAPIを直接呼び出すことができます。しかし、ユーザーはそれを本当に望んでいません。彼らはあなたのAPIへの認証されたアクセスを望んでいます。バックエンドが提供する必要があるのは、フロントエンドがソーシャルプロバイダーのアクセストークンをAPIへのアクセスを許可するトークンと交換できるエンドポイントだけです。
フロントエンドへのアクセストークンの提供はサーバー側のフローよりも本質的に安全性が低いのに、なぜこれを許可するのでしょうか。
クライアント側のフローにより、バックエンドのRESTAPIとユーザー向けのフロントエンドをより厳密に分離できます。バックエンドサーバーをリダイレクトURIとして指定することを厳密に妨げるものは何もありません。最終的な効果は、ある種のハイブリッドフローになります。
問題は、サーバーが適切なユーザー向けページを生成してから、何らかの方法でフロントエンドに制御を戻す必要があることです。
最近のプロジェクトでは、すべてのビジネスロジックを処理するフロントエンドUIとバックエンドの間で関心の分離を厳密に行うのが一般的です。これらは通常、明確に定義されたJSONAPIを介して通信します。上記のハイブリッドフローは、関心の分離を混乱させますが、バックエンドにユーザー向けのページを提供し、何らかのフローを設計してフロントエンドに戻るように制御します。
フロントエンドがアクセストークンを処理できるようにすることは、関心の分離を維持するための便利な手法です。侵害されたクライアントからのリスクがいくらか増加しますが、一般的にはうまく機能します。
このフローはフロントエンドにとって複雑に見えるかもしれません。フロントエンドチームがすべてを自分で開発する必要がある場合はそうです。ただし、両方 フェイスブック そして グーグル 最小限の構成でプロセス全体を処理するログインボタンをフロントエンドに含めることができるライブラリを提供します。
クライアントフローでは、バックエンドはOAuth2プロセスからかなり分離されています。誤解しないでください。これは簡単な仕事ではありません。少なくとも次の機能をサポートする必要があります。
User
を作成しますそれらのモデルを作成し、適切に入力します。User
のユーザーである場合モデルはすでに存在し、メールアドレスで照合して、ソーシャルログイン用に新しいアカウントを作成する代わりに、正しい既存のアカウントにアクセスできるようにします。幸いなことに、このすべての機能をバックエンドに実装するのは、予想よりもはるかに簡単です。
これが、わずか20行のコードでこれらすべてをバックエンドで機能させる方法の魔法です。 これは Python Social Auth ライブラリ(以降「PSA」)なので、両方を含める必要があります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は、非常にクールで非常に複雑な機械です。認証と任意のアクセスへのアクセスを処理することは完全に幸せです 数十のソーシャル認証プロバイダー 、そしてそれは以下を含む最も人気のあるPythonウェブフレームワークで動作します Django 、 フラスコ 、 ピラミッド 、 CherryPy 、および WebPy 。
ほとんどの場合、上記のコードは非常に標準的なDjango RESTフレームワーク(DRF)関数ベースのビューです。urls.py
でマップしたパスでPOSTリクエストをリッスンします。そして、期待する形式でリクエストを送信すると仮定すると、User
を取得します。オブジェクト、またはNone
。
User
を取得した場合オブジェクト。プロジェクトの他の場所で構成したモデルタイプであり、すでに存在している場合と存在していない場合があります。 PSAはすでにトークンの検証、ユーザーの一致が存在するかどうかの識別、必要に応じてユーザーの作成、ソーシャルプロバイダーからのユーザーの詳細の更新を処理しました。
ユーザーがソーシャルプロバイダーのユーザーからあなたのユーザーにマッピングされ、既存のユーザーに関連付けられる方法の正確な詳細は、SOCIAL_AUTH_PIPELINE
によって指定されます。上で定義されています。これがどのように機能するかについて学ぶことはまだたくさんありますが、それはこの投稿の範囲外です。あなたはそれについてもっと読むことができます ここに 。
魔法の重要な部分は@psa()
です。ビューのデコレータ。request
に一部のメンバーを追加します。ビューに渡されるオブジェクト。私たちにとって最も興味深いのはrequest.backend
です(PSAにとって、バックエンドは任意のソーシャル認証プロバイダーです)。
適切なバックエンドが選択され、request
に追加されましたbackend
に基づくオブジェクトビューへの引数。URL自体が入力されます。
node jsWebアプリケーションフレームワーク
backend
を取得したらオブジェクトが手元にある場合は、アクセスコードを指定して、そのプロバイダーに対して認証することができます。それがdo_auth
です方法。これにより、SOCIAL_AUTH_PIPELINE
全体が使用されます。設定ファイルから。
パイプラインは、拡張するとかなり強力なことを実行できますが、デフォルトの組み込み機能だけで、必要なすべてのことをすでに実行しています。
その後、通常のDRFコードに戻ります。有効なUser
を取得した場合オブジェクトの場合、適切なAPIトークンを非常に簡単に返すことができます。有効なUser
を取得できなかった場合オブジェクトを戻すと、エラーが発生しやすくなります。
この手法の欠点の1つは、エラーが発生した場合にエラーを返すのは比較的簡単ですが、具体的に何が悪かったのかについて多くの洞察を得るのが難しいことです。 PSAは、サーバーが問題の内容について返した可能性のある詳細をすべて飲み込みます。
繰り返しになりますが、適切に設計された認証システムの性質上、エラーの原因についてかなり不透明です。ログイン試行後にアプリケーションがユーザーに「無効なパスワード」と通知した場合、それは「おめでとうございます!有効なユーザー名を推測しました。」
一言で言えば:拡張性。まったく同じ方法でAPI呼び出しでまったく同じ情報を必要とする、または返すソーシャルOAuth2プロバイダーはほとんどありません。ただし、あらゆる種類の特殊なケースと例外があります。
すでにPSAを設定した後で新しいソーシャルプロバイダーを追加することは、設定ファイルの数行の構成の問題です。コードを調整する必要はまったくありません。 PSAはそれらすべてを抽象化するため、独自のアプリケーションに集中できます。
良い質問! unittest.mock
ライブラリの奥深くにある抽象化レイヤーの下に埋め込まれたAPI呼び出しをモックアウトするのには適していません。モックへの正確なパスを見つけるだけでもかなりの努力が必要です。
代わりに、PSAはRequestsライブラリの上に構築されているため、優れたものを使用します 反応 HTTPレベルでプロバイダーをモックアウトするライブラリ。
テストの完全な説明はこの記事の範囲を超えていますが、テストのサンプルが含まれています ここに 。 mocked
があることに注意する特定の関数コンテキストマネージャーとSocialAuthTests
クラス。
OAuth2プロセスは詳細で複雑であり、固有の複雑さがたくさんあります。幸いなことに、可能な限り簡単な方法で処理するための専用ライブラリを導入することで、その複雑さの多くを回避することができます。
Python SocialAuthはその点で素晴らしい仕事をしています。クライアント側の暗黙的なOAuth2フローを利用して、わずか25行のコードでシームレスなユーザー作成とマッチングを実現するDjango / DRFビューを示しました。それはそれほど粗末ではありません。