はじめに
こんにちは!株式会社BTM 札幌ラボの齊藤です!
アプリにログインする手軽な手段として
Googleアカウント(Gmail)を利用した「Googleでログイン」を
行う方も多いのではないでしょうか?
いわゆるOAuth2.0という認可の仕組みを利用したもので、
今や一般的な手段ですね。
このGoogleアカウントを使用した会員登録やログインを
実際に実装してみるとどんな感じになるのか。
そんなところから、簡単に自分で作成をしてみました。
使用技術
- Next.js:15.3.4
- Django:5.2.5
- MySQL:8.0
全てDocker使用の仮想環境にて構築
Google Cloud Consoleの設定
Google の OAuth2 クライアント ID/シークレットを取得する
-
Google Cloud Console (https://console.cloud.google.com/) にアクセスし、Googleアカウントでログイン
-
新しいプロジェクトを作成する
-
左側メニュー「API とサービス」→「認証情報」を開く
-
「+認証情報を作成」→「OAuth クライアント ID」を選択
初回は「OAuth同意画面」を設定する必要があります。
ユーザータイプ:
“外部” → 社内や一般ユーザー向け
“内部” → G Suite 組織内限定
必要事項(アプリ名、サポートメール、開発者メールなど)を入力して保存
-
「アプリケーションの種類」で「ウェブアプリケーション」を選択
-
承認済みのリダイレクト URI に
http://localhost:3000/api/auth/callback/googleを入力 -
「作成」ボタン押下。この際、クライアント ID とクライアントシークレットが発行されるので控えておく
環境変数の設定
- Django内の.env ファイルにて発行されたクライアント ID を設定
GOOGLE_CLIENT_ID = "xxxxxxxxxxxx.apps.googleusercontent.com"
- Next.js内の.env.localで、発行されたクライアント ID/シークレットを設定
- Dockerファイルの設定に応じて、Django側へのAPI用のURLを設定
GOOGLE_ID="xxxxxxxxxxxx.apps.googleusercontent.com"
GOOGLE_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
DJANGO_API_URL="http://xxxxx:8000"
「Googleでログイン」の実装
前提
Googleアカウントのメールアドレスがバックエンド(Django)のusersテーブルに
登録済の場合のみ、ログインができる仕組みとする
ディレクトリ構成(抜粋)
ログインと直接関わらない箇所、及びDjangoのモデルファイルは割愛する
project-directory/
├─ frontend/
│ └─ app/
│ ├─ api/
│ │ └─ auth/
│ │ └─ [...nextauth]/
│ │ └─ route.ts
│ └─ login/
│ └─ page.tsx
└─ backend/
└─ app/
└─ accounts/
├─ urls.py
├─ views.py
└─ utils.py
frontend/app/login/page.tsx
ログイン画面のテンプレート
'use client';
import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useSearchParams } from "next/navigation";
import LoginIcon from '@mui/icons-material/Login';
import Button from '@mui/material/Button';
export default function LoginPage() {
const searchParams = useSearchParams();
let callbackUrl = searchParams.get("callbackUrl") || "/";
const handleGoogleLogin = () => {
signIn("google", { callbackUrl });
};
return (
<>
<div className="container mt-5" style={{ maxWidth: 400 }}>
<h1 className="mb-4">ログイン</h1>
<hr />
<Button
variant="contained"
color="primary"
fullWidth
endIcon={<LoginIcon />}
onClick={handleGoogleLogin}
className="w-100"
>
Googleでログイン
</Button>
</div>
</>
);
}
frontend/app/api/auth/[...nextauth]/route.ts
バックエンド側へのルーティング
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
const djangoApiUrl = process.env.DJANGO_API_URL;
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID!,
clientSecret: process.env.GOOGLE_SECRET!,
}),
],
session: {
strategy: 'jwt',
},
callbacks: {
// Gmailが登録済か確認
async signIn({ account, profile }) {
if (account?.provider === "google" && account.id_token) {
// Googleのメールアドレス
const email = profile?.email;
const res = await fetch(`${djangoApiUrl}/api/check-email/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
// 登録されていなければログイン拒否
if (!res.ok) return false;
const data = await res.json();
if (!data.is_registered) return false;
}
// 登録済みなら許可
return true;
},
async jwt({ token, account, user }) {
if (account && account.provider === "google" && account.id_token) {
const tokenRes = await fetch(`${djangoApiUrl}/api/token/google/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id_token: account.id_token }),
});
if (tokenRes.ok) {
const jwtTokens = await tokenRes.json();
token.access = jwtTokens.access;
token.refresh = jwtTokens.refresh;
} else {
console.error("トークン取得失敗:", await tokenRes.text());
}
// GoogleのIDトークンでDjangoのユーザー情報を取得
const res = await fetch(`${djangoApiUrl}/api/google/`, {
method: "GET",
headers: {
"Authorization": `Bearer ${account.id_token}`,
},
});
token.id_token = account.id_token;
if (res.ok) {
const userData = await res.json();
token.id = userData.id;
token.email = userData.email;
token.username = userData.username;
}
}
return token;
},
async session({ session, token }) {
// セッションにJWTトークンや権限情報を含める
session.user.id = typeof token.id === "number" ? token.id : Number(token.id);
session.user.email = token.email;
session.user.username = token.username;
return session;
},
},
pages: {
signIn: "/login",
},
});
export { handler as GET, handler as POST };
backend/apps/accounts/urls.py
バックエンド(Django)側のルーティング
from django.urls import path
from .views import CheckEmailView
from .views import GoogleTokenView
from .views import GoogleView
urlpatterns = [
path('api/check-email/', CheckEmailView.as_view()),
path("api/token/google/", GoogleTokenView.as_view()),
path('api/google/', GoogleView.as_view()),
]
backend/apps/accounts/views.py
バックエンド側の処理
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from rest_framework.permissions import AllowAny
from django.contrib.auth import get_user_model
from apps.accounts.utils import verify_google_token
from rest_framework_simplejwt.tokens import RefreshToken
import google.auth.transport.requests
import google.oauth2.id_token
User = get_user_model()
class CheckEmailView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def post(self, request):
email = request.data.get("email")
exists = User.objects.filter(email=email).exists()
return Response({"is_registered": exists}, status=status.HTTP_200_OK)
class GoogleTokenView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def post(self, request):
google_id_token = request.data.get("id_token")
if not google_id_token:
return Response({"detail": "ID token is required."}, status=status.HTTP_400_BAD_REQUEST)
id_info = verify_google_token(google_id_token)
if not id_info:
return Response({"detail": "Invalid Google ID token."}, status=status.HTTP_401_UNAUTHORIZED)
email = id_info.get("email")
if not email:
return Response({"detail": "Email not found in ID token."}, status=status.HTTP_400_BAD_REQUEST)
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({"detail": "User does not exist."}, status=status.HTTP_401_UNAUTHORIZED)
# JWT発行
refresh = RefreshToken.for_user(user)
return Response({
"access": str(refresh.access_token),
"refresh": str(refresh),
}, status=status.HTTP_200_OK)
class GoogleView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return Response({'detail': '認証情報がありません'}, status=status.HTTP_401_UNAUTHORIZED)
token = auth_header.split(' ')[1]
idinfo = verify_google_token(token)
if not idinfo:
return Response({'detail': 'トークンが無効です'}, status=status.HTTP_401_UNAUTHORIZED)
email = idinfo['email']
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({'detail': 'ユーザーが見つかりません'}, status=status.HTTP_404_NOT_FOUND)
return Response({
'id': getattr(user, 'id'),
'email': user.email,
'username': user.username,
})
backend/apps/accounts/utils.py
from google.oauth2 import id_token
from google.auth.transport import requests
from django.conf import settings
def verify_google_token(token):
try:
idinfo = id_token.verify_oauth2_token(token, requests.Request(), settings.GOOGLE_CLIENT_ID)
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise ValueError('Wrong issuer.')
return idinfo
except Exception as e:
print("Google token verify error:", e)
return None
さいごに
Google Cloud Consoleで設定し、そこで発行された
クライアント IDとシークレットを適切に環境変数として設定しておけば
結構手軽に実装できます。
今や一般的なアカウント作成・ログイン方法なので、
知っておくと役立つかもしれません。
ご参考になれば幸いです。
株式会社BTMではエンジニアの採用をしております。
ご興味がある方はぜひコチラをご覧ください。