Next.jsでSign in with Googleを実装する(ライブラリなし)

2024/02/11に公開

TL;DR

以下の通りやればできます
https://developers.google.com/identity/protocols/oauth2/web-server

はじめに

Sign in with Google、非公式や公式のライブラリっぽいものがいっぱいあってどれ使えばいいのかわからず、という感じで今まではFirebaseに逃げがちでした。しかし真面目にGoogleのドキュメントを読めばやるべきことは全て書いてあったので、その通りにしたら普通にできました、というのをまとめた記事です。

ついでに、Next.jsのApp Routerを使うとかなり簡潔に実装ができていい感じなので、それを前提に書きます。

Step1-2: Set authorization parameters と Redirect to Google's OAuth 2.0 server

https://accounts.google.com/o/oauth2/v2/auth に飛ばすだけなので、ページにリンクを作って置いておけば良さそうです。

import Link from "next/link";

export default function Page() {
  const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
  url.searchParams.append("client_id", process.env.GOOGLE_CLIENT_ID);
  url.searchParams.append("response_type", "code");
  url.searchParams.append("scope", "...");
  url.searchParams.append("redirect_uri", "http://localhost:3000/callback");
  url.searchParams.append("access_type", "offline");
  url.searchParams.append("prompt", "consent");

  return <Link href={url.toString()}>SignIn with Google</Link>;
}

Step4: Handle the OAuth 2.0 server response

ドキュメントにある通り、redirect_uriに指定した場所に戻ってくるときにcodeがURLについてくるので、codeを取得してトークンを取得するためのAPIに投げます。トークンを取得するAPIはこのあと作りますが、仮に POST /api/token としておきます。codeはrequest bodyに入れて投げています。

以下をcallbackで指定したページのuseEffect内で実行などすればOKです。
fetchが成功したら、トップに戻すなどしてやれば良いでしょう。

      const params = new URLSearchParams(window.location.search);
      const code = params.get("code") ?? "";

      await fetch("/api/token", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ code }),
      });

Step5: Exchange authorization code for refresh and access tokens

https://oauth2.googleapis.com/token のAPIを叩く必要があります。client secretを使うのでこれはクライアントではなくサーバーサイドで行う必要があります。

APIはRoute Handlerを使うと簡単に定義できます。今回の場合は、 /app/api/token/route.ts にPOST functionを定義すれば良いです。

以下ではaccess_tokenをcookieにセットしてレスポンスを返しています。レスポンスのbodyはおまけです。

import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const { code } = (await req.json()) as { code: string };

  const resp = await fetch("https://oauth2.googleapis.com/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      code,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: "http://localhost:3000/callback",
      grant_type: "authorization_code",
    }),
  });
  if (!resp.ok) {
    console.error(await resp.text());
    return NextResponse.json(null, { status: 500 });
  }

  const j = (await resp.json()) as {
    access_token: string;
    token_type: string;
    expires_in: number;
    refresh_token: string;
    scope: string;
  };

  cookies().set("token", j.access_token);

  return NextResponse.json(j);
}

これでSign in With Googleが動くようになったはずです。

おわりに

Next.jsは特にこういった場面でかなり便利ですね。
今までライブラリに頼りがちでしたが、ライブラリなしでスッと実装できるならそれに越したことはないなあと思いました。

(自分が欲しい情報がドキュメントのどのページに書いてあるかを理解するのが一番大変だったかもしれない…。)

Discussion