はじめに
表題の通り、今回は SPA(Next.js) と DRF(Django REST framework) と djangoライブラリの dj-rest-auth と django-allauth を利用してOAuth2.0 のソーシャル認証を試みます。
dj-rest-auth
django-allauth
ログイン認証を実装する上で、以下の要件を意識しています。
-
可能な限りライブラリで完結すること
- セキュリティ的に堅牢であること、ここでオリジナリティを出しても競合との差は出ないことから、必要最小限のコード量で実装したいです
-
ソーシャルログインにおいて、プロパイダーの追加が柔軟であること
- 例えば、Google ログインが実装されている状態から、新たに Github ログインを追加する場合、最小の差分で実装できることを言います
今回は以下の3つの方法で実装しました。
- django-allauthメイン + セッションベース
- django-allauthメイン + JWT
- dj-rest-authメイン + JWT
それぞれについて、以下の項目で評価しました。各々高いほど良いと設定しています。
- ライブラリ依存度(ライブラリだけで実装できるか・高いほど実装コストが低い)
- 拡張容易性(プロパイダーの追加が容易か)
- 自由度(カスタマイズのしやすさ)
以下の評価は、私の実装経験に基づく相対的な比較です。
環境や要件によって最適な選択は異なるため、あくまで一つの参考値としてご覧ください。
| 方法 | ライブラリ依存度 | 拡張容易性 | 自由度 |
|---|---|---|---|
| django-allauth + セッション | ★★★★★ | ★★★★★ | ★★★☆☆ |
| django-allauth + JWT | ★★★★☆ | ★★★★★ | ★★☆☆☆ |
| dj-rest-auth + JWT | ★★☆☆☆ | ★★★☆☆ | ★★★★★ |
事前準備
-
npx create-next-app@latestやdjango-admin startproject <YOUR_PROJECT_NAME> .でフロントエンド、バックエンドのプロジェクトを作ります-
<YOUR_PROJECT_NAME>を私はconfigとしました
-
- フロントエンドからバックエンドへ通信できるように
settings.pyを修正します - API通信では、私は
axiosを利用しましたnpm install axios
環境
フロントエンド
- axios:
1.12.2 - next:
15.5.4 - react:
19.1.0 - openid-client:
6.8.1 - URL:
http://localhost:3000 - App router + Javascript
バックエンド
- Django:
5.2.7 - djangorestframework:
3.16.1 - djangorestframework_simplejwt:
5.5.1 - django-allauth:
65.13.0 - dj-rest-auth:
7.0.1 - URL:
http://localhost:8000
データベース
- PostgreSQL:
16
プロジェクト立ち上げ
Next.js と Django プロジェクトを作成後、API通信を可能にし、認証用アプリとカスタムユーザーモデルを作成します。
python manage.py startapp <YOUR_APP_NAME> で認証用アプリを Django に作成します。今回は authentication という名前にします。
authentication ← startapp コマンドで作成しました
┝ models.py
┝ views.py
┝ urls.py ← 手動でファイルを作成しました
┝ ...
config ← startproject で作成しました
┝ settings.py
┝ urls.py
┝ ...
manage.py
requirements.txt
Django
djangorestframework
psycopg2-binary
django-cors-headers
import os
from pathlib import Path
...
ALLOWED_HOSTS = [
os.environ["FRONTEND_HOST"], # ローカルなので "localhost" を入れています
]
# CORSとCSRFを設定してAPI通信を可能にします
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
os.environ["FRONTEND_ORIGIN"], # ローカルなので "http://localhost:3000" を入れています
]
CSRF_TRUSTED_ORIGINS = [
os.environ["FRONTEND_ORIGIN"], # ローカルなので "http://localhost:3000" を入れています
]
INSTALLED_APPS = [
...
'django.contrib.staticfiles',
"corsheaders",
"rest_framework",
"authentication", # startapp で作成したアプリ
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
...
]
...
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.environ["POSTGRES_HOST"],
"PORT": os.environ["POSTGRES_PORT"],
}
}
...
AUTH_USER_MODEL = 'authentication.Account' # カスタムユーザーモデルを認証用のモデルとします
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('authentication/', include("authentication.urls")),
]
from django.db import models
from django.contrib.auth.models import AbstractUser
class Account(AbstractUser):
email = models.EmailField(unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
メールアドレスとパスワードでログインできるようになればいいので、上記のように最小限の記述で実装をします。
src
┝ app
│ ┝ page.js
│ ┝ ...
│
┝ lib
┝ api.js // ここにバックエンドへのAPIを記述していきます
package.json
...
import axios from "axios";
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE, // ローカルなので 'http://localhost:8000' を入れています
withCredentials: true,
});
// クッキー内の値を取得する
function getCookie(name) {
return document.cookie.split("; ")
.find(r => r.startsWith(name + "="))?.split("=")[1];
}
// リクエストを送る直前に走る。メソッドによってCSRFトークンをヘッダーに追加する
api.interceptors.request.use((config) => {
const method = config.method.toLowerCase();
if (["post","put","patch","delete"].includes(method)) {
config.headers["X-CSRFToken"] = getCookie("csrftoken");
config.headers["Content-Type"] = "application/json";
}
return config;
});
ソーシャルログインを試みる
ようやく本題ですが、dj-rest-auth と django-allauth ライブラリを用いてここからソーシャルログインを試みます。
pip install dj-rest-auth[with_social]
pip install django-allauth[socialaccount]
django-allauthメイン + セッションベース
セッションIDを用いたログイン/ログアウトと認証は dj-rest-auth、ソーシャルログインは django-allauth を棲み分けて実装します。まず、Google ログインの実装を試みます。
図解
ログインするまでの簡単な流れを以下に図示します。
Google OAuth2 認証作成
Google でログインするにはGoogleコンソールで OAuth2 の認証を作成する必要があります。
以下の記事がわかりやすかったのでご参照ください。
承認済みのJavaScript生成元 と 承認済みのリダイレクトURI を入力する欄がありますが、このプロジェクトの場合以下の通りになります。
- 承認済みのJavaScript生成元:
http://localhost:8000 - 承認済みのリダイレクトURI:
http://localhost:8000/authentication/allauth/google/login/callback/
クライアントIDとクライアントシークレットをコピーして、.env ファイルに置くと良いでしょう。このプロジェクトの場合、以下のように設定しています。
GOOGLE_OAUTH_CLIENT_ID=<クライアントID>
GOOGLE_OAUTH_CLIENT_SECRET=<クライアントシークレット>
ソースコード
Django
djangorestframework
psycopg2-binary
django-cors-headers
+ dj-rest-auth[with_social]
+ django-allauth[socialaccount]
INSTALLED_APPS = [
...
+ 'django.contrib.sites',
'corsheaders',
'rest_framework',
+ 'rest_framework.authtoken',
+ 'dj_rest_auth',
+ 'allauth',
+ 'allauth.account',
+ 'allauth.socialaccount',
+ 'allauth.socialaccount.providers.google',
'authentication',
]
MIDDLEWARE = [
...
+ "allauth.account.middleware.AccountMiddleware",
]
+ AUTHENTICATION_BACKENDS = [
+ 'django.contrib.auth.backends.ModelBackend',
+ 'allauth.account.auth_backends.AuthenticationBackend',
+ ]
# 認証方法とデフォルトの権限の設定
+ REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
+ "rest_framework.authentication.SessionAuthentication",
+ ],
+ "DEFAULT_PERMISSION_CLASSES": (
+ "rest_framework.permissions.IsAuthenticated",
+ ),
+ }
+ ACCOUNT_LOGIN_METHODS = {'email'}
+ ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
### django-allauth の設定
+ SITE_ID = 1
+ SOCIALACCOUNT_LOGIN_ON_GET = True
+ LOGIN_REDIRECT_URL = "/authentication/callback/"
+ SOCIALACCOUNT_PROVIDERS = {
+ 'google': {
+ "OAUTH_PKCE_ENABLED": True,
+ "SCOPE": [
+ "openid",
+ "email",
+ "profile"
+ ],
+ 'APP': {
+ 'client_id': os.environ['GOOGLE_OAUTH_CLIENT_ID'],
+ 'secret': os.environ['GOOGLE_OAUTH_CLIENT_SECRET'],
+ 'key': ''
+ },
+ },
+ }
###
from django.urls import path, include
from authentication.views import frontend_callback_redirect
urlpatterns = [
path('dj-rest-auth/', include("dj_rest_auth.urls")),
path('allauth/', include('allauth.urls')),
path('callback/', frontend_callback_redirect),
]
urlの命名が django-allauth 公式とは異なりますが、何のライブラリを用いているのか明確にするため、あえてライブラリ名で url を命名しています。
import os
from django.shortcuts import redirect
# callback_uri にはフロントエンドの任意のパスを指定していいと思います
def frontend_callback_redirect(request):
callback_uri = os.environ["FRONTEND_ORIGIN"] + "/"
return redirect(callback_uri)
allauth を利用するためのテーブルが必要になるので、実際に動かす前に migrate します。
python manage.py migrate
...
// allauth宛にログインリダイレクト
+ export async function GoogleLoginAPI () {
+ window.location.href = process.env.NEXT_PUBLIC_API_BASE + `/authentication/allauth/google/login/`;
+ };
パスの遷移
// ボタンのクリックなどで呼び出す
export async function GoogleLoginAPI () {
window.location.href = process.env.NEXT_PUBLIC_API_BASE + `/authentication/allauth/google/login/`;
};
↓↓↓
urlpatterns = [
...
path('authentication/', include("authentication.urls")),
]
urlpatterns = [
...
# allauth 内の google/login/ を呼ぶ
path('allauth/', include('allauth.urls')),
...
]
↓↓↓
Googleログイン画面でアカウントを選択後、Googleコンソールで設定した「承認済みのリダイレクトURI」にリダイレクト
http://localhost:8000/authentication/allauth/google/login/callback/
↓↓↓
# ログイン後のリダイレクト先を settings.py で指定する
LOGIN_REDIRECT_URL = "/authentication/callback/"
↓↓↓
urlpatterns = [
...
path('authentication/', include("authentication.urls")),
]
urlpatterns = [
...
path('callback/', frontend_callback_redirect),
]
# callback_uri にはフロントエンドの任意のパスを指定していいと思います
def frontend_callback_redirect(request):
callback_uri = os.environ["FRONTEND_ORIGIN"] + "/"
return redirect(callback_uri)
↓↓↓
フロントエンドへ
ソーシャルプロパイダーの追加
ここでは github でのログインを追加します。
github も Google と同様に、OAuth 認証の登録が必要です。こちらの記事のステップ1がわかりやすいと思ったのでご参照ください。
また送信元のURLとコールバック先URLを記入する必要があり、本プロジェクトでは以下のように設定しています。
- Application name: (任意)
- Homepage URL:
http://localhost:8000 - Authorization callback URL:
http://localhost:8000/authentication/allauth/github/login/callback/
Client ID と Client secret を作成して、.env ファイルに置くと良いでしょう。このプロジェクトの場合、以下のように設定しています。
GITHUB_OAUTH_CLIENT_ID=<Client ID>
GITHUB_OAUTH_CLIENT_SECRET=<Client secret>
以下のソースコードは Google ログイン実装後、Github ログインを追加した場合の差分となります。
INSTALLED_APPS = [
...
'allauth.socialaccount.providers.google',
+ 'allauth.socialaccount.providers.github',
'authentication',
]
...
### django-allauth の設定
SITE_ID = 1
SOCIALACCOUNT_LOGIN_ON_GET = True
LOGIN_REDIRECT_URL = "/authentication/callback/"
# ログイン後に追加でメールアドレス情報を問い合わせる
+ SOCIALACCOUNT_QUERY_EMAIL = True
SOCIALACCOUNT_PROVIDERS = {
'google': {
"OAUTH_PKCE_ENABLED": True,
"SCOPE": [
"openid",
"email",
"profile"
],
'APP': {
'client_id': os.environ['GOOGLE_OAUTH_CLIENT_ID'],
'secret': os.environ['GOOGLE_OAUTH_CLIENT_SECRET'],
'key': ''
},
},
+ 'github': {
+ "OAUTH_PKCE_ENABLED": True,
+ "VERIFIED_EMAILS_ONLY": True,
+ "SCOPE": [
+ "read:user",
+ "user:email",
+ ],
+ 'APP': {
+ 'client_id': os.environ['GITHUB_OAUTH_CLIENT_ID'],
+ 'secret': os.environ['GITHUB_OAUTH_CLIENT_SECRET'],
+ }
+ },
}
通常ログイン・ログアウト・ユーザー情報取得
dj-rest-auth の /login、/logout、/user の API を叩けば良いです。/login についてはACCOUNT_LOGIN_METHODS で指定したフィールドとパスワードも送ります。
// data → {email: ..., password: ...}
export async function loginAPI (data) {
const res = await api.post("/authentication/dj-rest-auth/login/", data)
};
export async function logoutAPI () {
const res = await api.post("/authentication/dj-rest-auth/logout/")
};
export async function userAPI () {
const res = await api.get("/authentication/dj-rest-auth/user/")
};
所感
他のライブラリも軽く触ってみましたが、セッションベースでソーシャルログインを実装する場合、django-allauth を最大限利用するのが保守性と拡張性の面において優秀だと思いました。多くの技術記事で書かれている理由がわかる気がします。
- ライブラリ依存度:★★★★★(
settings.pyの記述でほぼ完結します) - 拡張容易性:★★★★★
- 自由度:★★★☆☆(
settings.pyでカスタマイズしたシリアライザーなど指定できます)
django-allauthメイン + JWT
settings.py での設定だけではJWTの実装が難しかったので、frontend_callback_redirect メソッドの中でトークンをつけてレスポンスを返すようにカスタマイズします。通常のログインは dj-rest-auth で行なっているので、dj_rest_auth.views.LoginView を参考に、以下のように実装しました。
ソースコード
Django
djangorestframework
psycopg2-binary
django-cors-headers
+ djangorestframework-simplejwt
dj-rest-auth[with_social]
django-allauth[socialaccount]
...
# 認証方法とデフォルトの権限の設定
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
- "rest_framework.authentication.SessionAuthentication",
+ 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', # ここで jwt による認証を行う
],
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
),
}
...
# dj-rest-auth の設定
+ REST_AUTH = {
+ 'SESSION_LOGIN': False,
+ "USE_JWT": True,
+ 'JWT_AUTH_COOKIE': "access-token",
+ 'JWT_AUTH_REFRESH_COOKIE': "refresh-token",
+ }
import os
- from django.shortcuts import redirect
+ from django.http import HttpResponseRedirect
+ from dj_rest_auth.app_settings import api_settings
+ from dj_rest_auth.jwt_auth import set_jwt_cookies
+ from dj_rest_auth.utils import jwt_encode
# callback_uri にはフロントエンドの任意のパスを指定していいと思います
def frontend_callback_redirect(request):
callback_uri = os.environ["FRONTEND_ORIGIN"] + "/"
- return redirect(callback_uri)
+ response = HttpResponseRedirect(callback_uri)
+ if api_settings.USE_JWT:
+ access_token, refresh_token = jwt_encode(request.user)
+ set_jwt_cookies(response, access_token, refresh_token)
+ return response
所感
少し自分でコードを書く必要はありますが、大半は dj-rest-auth と django-allauth に任せられるのでとても便利です。「セッションベース↔︎JWT」の切り替えが簡素なのも魅力的です。
- ライブラリ依存度:★★★★☆
- 拡張容易性:★★★★★
- 自由度:★★☆☆☆(トークンをセットするのは自分で記述しました)
dj-rest-authメイン + JWT
dj-rest-auth にも以下のドキュメントのように、ソーシャルログインのためのソースコードが記載されています。なのでこちらも利用してみます。
POST する際、プロパイダーからの code パラメータを付ける必要があるので、上記までのフローを変更してフロントから直接プロパイダーのログイン画面をリダイレクトするようにします。(セッションベースは django-allauth を利用する方法が最も容易だと思うので、JWT で実装します)
図解
Google OAuth2 認証作成
認証フローが変わるので、以下のような修正を Google コンソールで行います。
- 承認済みのJavaScript生成元:
http://localhost:3000 - 承認済みのリダイレクトURI:
http://localhost:3000/callback/google/
ソースコード
プロジェクト立ち上げ に記載したソースコードを基準に、ソーシャルログイン認証の差分を追加していきます。
NEXT_PUBLIC_GOOGLE_OAUTH_CALLBACK_URI=http://localhost:3000/callback/google/
GOOGLE_OAUTH_CALLBACK_URI=http://localhost:3000/callback/google/
NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID=<クライアントID>
GOOGLE_OAUTH_CLIENT_ID=<クライアントID>
GOOGLE_OAUTH_CLIENT_SECRET=<クライアントシークレット>
Django
djangorestframework
psycopg2-binary
django-cors-headers
+ djangorestframework-simplejwt
+ dj-rest-auth[with_social]
+ django-allauth[socialaccount]
INSTALLED_APPS = [
...
+ 'django.contrib.sites',
"corsheaders",
"rest_framework",
+ "rest_framework.authtoken",
+ "dj_rest_auth",
+ 'dj_rest_auth.registration',
+ 'allauth',
+ 'allauth.account',
+ 'allauth.socialaccount',
+ 'allauth.socialaccount.providers.google',
"authentication",
]
MIDDLEWARE = [
...
+ "allauth.account.middleware.AccountMiddleware",
]
+ AUTHENTICATION_BACKENDS = [
+ 'django.contrib.auth.backends.ModelBackend',
+ 'allauth.account.auth_backends.AuthenticationBackend',
+ ]
+ REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
+ 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', # ここで jwt による認証を行う
+ ],
+ "DEFAULT_PERMISSION_CLASSES": (
+ "rest_framework.permissions.IsAuthenticated",
+ ),
+ }
+ ACCOUNT_LOGIN_METHODS = {'email'}
+ ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
### django-allauthの設定
+ SITE_ID = 1
+ SOCIALACCOUNT_PROVIDERS = {
+ 'google': {
+ "OAUTH_PKCE_ENABLED": True,
+ "SCOPE": [
+ "openid",
+ "email",
+ "profile"
+ ],
+ 'APP': {
+ 'client_id': os.environ.get('GOOGLE_OAUTH_CLIENT_ID'),
+ 'secret': os.environ.get('GOOGLE_OAUTH_CLIENT_SECRET'),
+ 'key': ''
+ }
+ },
+ }
###
# dj-rest-auth の設定
+ REST_AUTH = {
+ 'SESSION_LOGIN': False,
+ 'USE_JWT': True,
+ 'JWT_AUTH_COOKIE': "access-token",
+ 'JWT_AUTH_REFRESH_COOKIE': "refresh-token",
+ }
from django.urls import path, include
from authentication.views import GoogleLogin
urlpatterns = [
path('dj-rest-auth/', include("dj_rest_auth.urls")),
path('dj-rest-auth/google/', GoogleLogin.as_view(), name='google_login')
]
import os
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.views import SocialLoginView
class CustomOAuth2Client(OAuth2Client):
def __init__(
self,
request,
consumer_key,
consumer_secret,
access_token_method,
access_token_url,
callback_url,
_scope, # This is fix for incompatibility between django-allauth==65.3.1 and dj-rest-auth==7.0.1
scope_delimiter=" ",
headers=None,
basic_auth=False,
):
super().__init__(
request,
consumer_key,
consumer_secret,
access_token_method,
access_token_url,
callback_url,
scope_delimiter,
headers,
basic_auth,
)
# バリデーションの時に pkce_code_verifier を渡すようにオーバーライドで修正する
def get_access_token(self, code, *args, **kwargs):
code_verifier = getattr(self.request, "POST", {}).get("code_verifier")
return super().get_access_token(code, pkce_code_verifier=code_verifier, *args, **kwargs)
class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
callback_url = os.environ.get("GOOGLE_OAUTH_CALLBACK_URI")
client_class = CustomOAuth2Client
__init__ について
公式の OAuth2Client を利用すると引数の数の違いでエラーになるので、以下の github issues を参考に修正しています。
https://github.com/iMerica/dj-rest-auth/issues/673
src
+ ┝ authentication
+ │ ┝ pkce.js
┝ app
│ ┝ page.js
│ ┝ ...
+ │ ┝ callback
+ │ │ ┝ google
+ │ │ │ ┝ page.js
┝ lib
┝ api.js
package.json
...
npm install openid-client
import {
randomPKCECodeVerifier,
calculatePKCECodeChallenge,
randomState,
randomNonce,
} from 'openid-client';
const PKCE_KEY = {
VERIFIER: 'pkce_code_verifier',
STATE: 'oidc_state',
NONCE: 'oidc_nonce',
};
// pkce や oidc 用のパラメータ生成&保存
export async function createPkceSession() {
const code_verifier = randomPKCECodeVerifier();
const state = randomState();
const nonce = randomNonce();
sessionStorage.setItem(PKCE_KEY.VERIFIER, code_verifier);
sessionStorage.setItem(PKCE_KEY.STATE, state);
sessionStorage.setItem(PKCE_KEY.NONCE, nonce);
const code_challenge = await calculatePKCECodeChallenge(code_verifier);
return {
code_challenge,
state,
nonce,
};
};
// pkce や oidc 用のパラメータ取得&破棄
export function consumePkceSession() {
const code_verifier = sessionStorage.getItem(PKCE_KEY.VERIFIER);
const state = sessionStorage.getItem(PKCE_KEY.STATE);
const nonce = sessionStorage.getItem(PKCE_KEY.NONCE);
sessionStorage.removeItem(PKCE_KEY.VERIFIER);
sessionStorage.removeItem(PKCE_KEY.STATE);
sessionStorage.removeItem(PKCE_KEY.NONCE);
return { code_verifier, state, nonce };
};
...
// SPA → Google ログイン画面へリダイレクト
export async function googleLoginJWTSPA () {
const { code_challenge, state, nonce } = await createPkceSession();
const params = new URLSearchParams({
client_id: process.env.NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID,
redirect_uri: process.env.NEXT_PUBLIC_GOOGLE_OAUTH_CALLBACK_URI,
response_type: "code",
prompt: "consent",
scope: "openid email profile",
access_type: "offline",
code_challenge_method: "S256",
code_challenge,
state,
nonce,
});
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
};
// SPA → dj-rest-auth へAPIを叩く
export async function googleLoginJWTAPI (code) {
const { code_verifier, state, nonce } = consumePkceSession();
const redirect_uri = process.env.NEXT_PUBLIC_GOOGLE_OAUTH_CALLBACK_URI;
const body = new URLSearchParams({ code, code_verifier, redirect_uri });
const res = await api.post("/authentication/dj-rest-auth/google/", body, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
};
"use client";
import { googleLoginJWTAPI } from "@/lib/api"
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
// google から callback される
export default function CallbackPage() {
const router = useRouter();
const searchParams = useSearchParams();
// dj-rest-auth へAPIを叩いてログインする
async function handleCallback () {
const code = searchParams.get("code");
await googleLoginJWTAPI(code);
router.push("/");
};
useEffect(() => {
handleCallback();
}, []);
};
全体的にもっと丁寧にエラーハンドリングする必要がありますが、一旦最小限のコード量で実装しています。
PKCE について
PKCE とは OAuth 2.0 の認可コードフローのセキュリティを強化するための仕組みです。Google では PKCE 用のパラメータを用意しないと認可してくれませんでした。詳しくは以下のサイトをご参照ください。
https://e-words.jp/w/PKCE.html
PKCE に対応するため追加したコードは、フロントエンド にある authentication/pkce.js とバックエンドにある authentication/views.py の CustomOAuth2Client にある以下の1行です。
code_verifier = getattr(self.request, "POST", {}).get("code_verifier")
パスの遷移
// ボタンのクリックなどで呼び出す
export async function googleLoginJWTSPA () {
const params = new URLSearchParams({
...
});
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
};
↓↓↓
Googleログイン画面でアカウントを選択後、Googleコンソールで設定した「承認済みのリダイレクトURI」にリダイレクト
http://localhost:3000/callback/google/
↓↓↓
"use client";
import { googleLoginJWTAPI } from "@/lib/api"
...
// google から callback される
export default function CallbackPage() {
...
// dj-rest-auth へAPIを叩いてログインする
async function handleCallback () {
const code = searchParams.get("code");
await googleLoginJWTAPI(code); // このメソッドを呼ぶ
router.push("/");
};
useEffect(() => {
handleCallback();
}, []);
};
// SPA → dj-rest-auth へAPIを叩く
export async function googleLoginJWTAPI (code) {
...
const res = await api.post("/authentication/dj-rest-auth/google/", body, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
};
↓↓↓
urlpatterns = [
...
path('dj-rest-auth/google/', GoogleLogin.as_view(), name='google_login')
]
...
class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
callback_url = os.environ.get("GOOGLE_OAUTH_CALLBACK_URI")
client_class = CustomOAuth2Client
↓↓↓
"use client";
...
export default function CallbackPage() {
...
// dj-rest-auth へAPIを叩いてログインする
async function handleCallback () {
const code = searchParams.get("code");
await googleLoginJWTAPI(code);
router.push("/"); // ここで次のパスへ遷移
};
...
};
以下の4つ全てで同じ URI を指定する必要があります。
・Google へリダイレクトするときの redirect_uri パラメータ
・Google コンソールで指定する 承認済みのリダイレクトURI
・dj-rest-auth へ POST するときの redirect_uri パラメータ
・バックエンドの View で指定する callback_url 変数
ソーシャルプロパイダーの追加
こちらも github でのログインを追加します。
送信元のURLとコールバック先URLを記入以下のように設定します。
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/callback/github/
Client ID と Client secret を作成して、.env ファイルに置くと良いでしょう。このプロジェクトの場合、以下のように設定しています。
NEXT_PUBLIC_GITHUB_OAUTH_CALLBACK_URI=http://localhost:3000/callback/github/
GITHUB_OAUTH_CALLBACK_URI=http://localhost:3000/callback/github/
NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID=<Client ID>
GITHUB_OAUTH_CLIENT_ID=<Client ID>
GITHUB_OAUTH_CLIENT_SECRET=<Client secret>
以下のソースコードは Google ログイン実装後、Github ログインを追加した場合の差分となります。
INSTALLED_APPS = [
...
'allauth.socialaccount.providers.google',
+ 'allauth.socialaccount.providers.github',
"authentication",
]
...
SITE_ID = 1
+ SOCIALACCOUNT_QUERY_EMAIL = True
SOCIALACCOUNT_PROVIDERS = {
'google': {
"OAUTH_PKCE_ENABLED": True,
"SCOPE": [
"openid",
"email",
"profile"
],
'APP': {
'client_id': os.environ.get('GOOGLE_OAUTH_CLIENT_ID'),
'secret': os.environ.get('GOOGLE_OAUTH_CLIENT_SECRET'),
'key': ''
}
},
+ 'github': {
+ "OAUTH_PKCE_ENABLED": True,
+ "VERIFIED_EMAILS_ONLY": True,
+ "SCOPE": [
+ "read:user",
+ "user:email",
+ ],
+ 'APP': {
+ 'client_id': os.environ.get('GITHUB_OAUTH_CLIENT_ID'),
+ 'secret': os.environ.get('GITHUB_OAUTH_CLIENT_SECRET'),
+ }
+ },
}
...
from django.urls import path, include
- from authentication.views import GoogleLogin
+ from authentication.views import GoogleLogin, GithubLogin
urlpatterns = [
path('dj-rest-auth/', include("dj_rest_auth.urls")),
path('dj-rest-auth/google/', GoogleLogin.as_view(), name='google_login'),
+ path('dj-rest-auth/github/', GithubLogin.as_view(), name='github_login'),
]
import os
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
+ from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.views import SocialLoginView
...
+ class GithubLogin(SocialLoginView):
+ adapter_class = GitHubOAuth2Adapter
+ callback_url = os.environ.get("GITHUB_OAUTH_CALLBACK_URI")
+ client_class = PatchedOAuth2Client
src
┝ authentication
│ ┝ pkce.js
┝ app
│ ┝ page.js
│ ┝ ...
│ ┝ callback
│ │ ┝ google
│ │ │ ┝ page.js
+ │ │ ┝ github
+ │ │ │ ┝ page.js
┝ lib
┝ api.js
package.json
...
...
// SPA → Github ログイン画面へリダイレクト
export async function githubLoginJWTSPA () {
const { code_challenge, state, nonce } = await createPkceSession();
const params = new URLSearchParams({
client_id: process.env.NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID,
redirect_uri: process.env.NEXT_PUBLIC_GITHUB_OAUTH_CALLBACK_URI,
response_type: "code",
prompt: "consent",
scope: "read:user user:email",
access_type: "offline",
code_challenge_method: "S256",
code_challenge,
state,
nonce,
});
window.location.href = `https://github.com/login/oauth/authorize?${params.toString()}`;
};
// SPA → dj-rest-auth へAPIを叩く
export async function githubLoginJWTAPI (code) {
const { code_verifier, state, nonce } = consumePkceSession();
const redirect_uri = process.env.NEXT_PUBLIC_GITHUB_OAUTH_CALLBACK_URI;
const body = new URLSearchParams({ code, code_verifier, redirect_uri });
const res = await api.post("/authentication/dj-rest-auth/github/", body, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
};
"use client";
import { githubLoginJWTAPI } from "@/lib/api"
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
// github から callback される
export default function CallbackPage() {
const router = useRouter();
const searchParams = useSearchParams();
// dj-rest-auth へAPIを叩いてログインする
async function handleCallback () {
const code = searchParams.get("code");
await githubLoginJWTAPI(code);
router.push("/");
};
useEffect(() => {
handleCallback();
}, []);
};
各プロパイダーのコールバック URI をhttp://localhost:3000/callback/<provider/ ではなく http://localhost:3000/callback/ に固定して、その中でプロパイダーに応じた API を叩くようにしても良いですね。
所感
dj-rest-auth が主体なので認証周りを全て任せられるのがとても良いです。ただ、なるべくライブラリを頼りたい方針に合致しないほど自分でコードを書く必要があり、また PKCE などの概念を新たに理解する必要があります。
- ライブラリ依存度:★★☆☆☆
- 拡張容易性:★★★☆☆
- 自由度:★★★★★(オーバーライドを含め、通信の前後で色々できます)
おわりに
通常のログイン認証は dj-rest-auth が便利なので、それと相性がいい django-allauth をソーシャルログインのライブラリに選定しました。実際に上記の3種類を実装した結果、それぞれのライブラリを適材適所で使い分けることが最も簡潔に実装できると感じました。

