0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-11-10

はじめに

こんにちは!株式会社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ではエンジニアの採用をしております。
ご興味がある方はぜひコチラをご覧ください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?