🙆

「Googleでログイン」を作ってみた

に公開

はじめに

こんにちは!株式会社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とシークレットを適切に環境変数として設定しておけば
結構手軽に実装できます。

今や一般的なアカウント作成・ログイン方法なので、
知っておくと役立つかもしれません。
ご参考になれば幸いです。

Discussion