nikkie-ftnextの日記

イベントレポートや読書メモを発信

djoserの「Social Endpoints」を試す(ハマって解決するも、Google OAuth2のトークンを得た先がわかっていません)

はじめに

ひろプリがいいぞ! nikkieです。

最近djoserを触っていたのは、ソーシャルアカウントを使った認証がサポートされているらしいと聞いたためです。
いよいよ本題に切り込みます

目次

djoserのドキュメント「Social Endpoints」

※Social Endpointsはベータとのことです

REST APIの認証系エンドポイントを追加できるライブラリdjoser1
ソーシャルアカウントで認証できるエンドポイントを触ってみます。

ワークフロー

「Social Endpoints」ドキュメントの中の「Provider Auth」にある箇条書きより

  1. djoserが追加するエンドポイントにredirect_uriを渡してアクセス(GET)
    • redirect_uriは私たちが開発しているアプリケーションのエンドポイント
  2. authorization_urlというキーを持つJSONが返る。アプリケーションのユーザをそのURLへリダイレクト
    • authorization_urlは外部のソーシャルサービスのエンドポイント
  3. アプリケーションのユーザは外部のソーシャルサービスで認証される。外部のサービスは1のredirect_uriにGETリクエスト(クエリ文字列のキーにcodestateを持つ)
  4. 3のcodestateを使って、1のエンドポイントにPOSTリクエスト(application/x-www-form-urlencoded)。私たちのアプリケーションでも認証される

djoserは/o/{{ provider }}/というURLを追加しています。

  • 1では/o/{{ provider }}/redirect_uriパラメタ(クエリ文字列)を渡してGETリクエス
  • 4では/o/{{ provider }}/codestateパラメタ(application/x-www-form-urlencoded)を渡してPOSTリクエス

Google OAuth2の例

Python 3.11.8

  • djoser==2.2.2
    • django==5.0.6
    • djangorestframework==3.15.1
    • social-auth-app-django==5.4.1
  • django-environ==0.11.2

OAuth 2.0 Client IDを作る

Google CloudのConsoleにて
APIとサービス」 > 「認証情報」から作ります。

Djangoの設定

プロジェクトのurls.py

urlpatterns = [
    path("api/auth/social/", include("djoser.social.urls")),
    path("accounts/profile/", RedirectSocialView.as_view()),
]

social.py(プロジェクトの中に作成)

from django.http import JsonResponse
from django.views import View


class RedirectSocialView(View):
    def get(self, request):
        return JsonResponse(
            {
                "code": str(request.GET["code"]),
                "state": str(request.GET["state"]),
            }
        )

settings.py

INSTALLED_APPS = [
    # ...

    "rest_framework",
    "djoser",
    "social_django",

    # ...
]

MIDDLEWARE = [
    "social_django.middleware.SocialAuthExceptionMiddleware",
    # ...
]

AUTHENTICATION_BACKENDS = (
    "social_core.backends.google.GoogleOAuth2",
)

SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = env("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY")
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = env("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET")

DJOSER = {
    "SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": ["http://127.0.0.1:8000/accounts/profile/"]
}

動作確認

Browsable APIで動いた!

http://127.0.0.1:8000/api/auth/social/o/google-oauth2/?redirect_uri=http://127.0.0.1:8000/accounts/profile/ にブラウザでアクセス

authorization_urlが返っています!
これをブラウザの別のタブで開きます。
codeとstateからなるJSONが確認できます(RedirectSocialViewの実装による)。

再びBrowsable APIに戻り、「Raw data」をPOSTします。

  • Media typeは「application/x-www-form-urlencoded」
  • Contentは「code=...&state=...」

すると、JSONが返ってきます。成功です!(201)

{
    "access": "...",
    "refresh": "...",
    "user": "Googleアカウントのユーザ名"
}

curlではうまくいかない

ずっとcurlでやっていてハマり続けました。

% curl 'http://127.0.0.1:8000/api/auth/social/o/google-oauth2/?redirect_uri=http://127.0.0.1:8000/accounts/profile/'
{"authorization_url":"..."}

authorization_urlにブラウザでアクセス。
codeとstateからなるJSONを確認します。

再びcurlでリクエストを組み立てると

% curl -L -d 'state=...' -d 'code=...' http://127.0.0.1:8000/api/auth/social/o/google-oauth2/
{"non_field_errors":["Session value state missing."]}

400 Bad Requestです。

breakpointを仕込んでいったところ、どうやら以下で出ているようで、
https://github.com/python-social-auth/social-core/blob/4.5.4/social_core/backends/oauth.py#L100-L101
Browsable APIからだとセッション(cookie)の関係でうまくいっているのではないかと考えています(真偽不明)(curlでもcookieの扱い、できるのかな?)

Browsable APIで動いたものの

ここからどうすればいいかが分かりません。
access, refresh, userからなるJSONで何ができるんでしょう?

アクセストークが取得できているので、ユーザが認可したGoogleAPIにリクエストを送れるようですが、accessの値をそのまま使っていいのかな?(宿題事項)
私が気になっていたのは、「Social Endpoints」を使ってDjangoアプリにログインしたことにできるのかということなのですが、この点もよく分かっていません(DjangoアプリのBearerトークンが発行できたらいいが、果たして)

終わりに

curlで実施していたために、djoserの「Social Endpoints」のGoogle OAuth2が通らずハマりました。
Browsable APIでは実行でき、「Social Endpoints」のドキュメントの意味はかなり分かりました。
返ってきたアクセストークンを扱ってソーシャルサービスを利用したり、ソーシャルアカウントを使ってDjangoアプリで認証(DjangoアプリのREST APIを叩くためのトークン発行)したりできるのかが、まだ分かっていません

参照したもの

どちらもドンピシャではなかったです。
あと、どちらの記事もやりたいことに対して扱っているものが余分に思われ(すべてのコードに説明がされておらず、おまじないがある感)、新しい概念が多すぎましたし、最小限の見定めが難しかったです

P.S. 5/18(土) Djangoもくもく会

この記事はもくもくの成果物です。

nibuさんの成果物

次回は6月です。よろしければ!