Roll With IT

tamakiのIT日記

【前編】NextAuth.jsを使ってGoogleログイン機能を実装する(Next.js × Railsで実践するアプリ開発)

はじめに

Next.jsとRailsを使ってオリジナルのWebアプリを開発しています。

github.com

ログイン機能を備えたアプリにするために、Google認証でログインする方法を選択しました。この記事では、実装過程において調べた情報や試行錯誤しハマった経験などをまとめたいと思います。

前編、後編の2部構成でお届けします。

前編

Next.jsのNextAuth.jsを使用しGoogle認証ログイン、ログアウトに焦点を当てた内容です。

後編

ログインしたユーザーの情報をデータベースに保存する方法について焦点を当てた内容です。Rails APIを叩いてデータベースに保存する方法についてまとめています。

後編はこちら

技術スタック

フロントエンド:
  • Next.js '13.2.4'
  • 使用するnpm
    • next-auth '4.20.1'
    • axios '1.3.4'
バックエンド:
  • Ruby '3.2.1'
  • Rails '7.0.4.2' API mode
  • 使用するgem
    • rack-cors '2.0.0'
データベース: 
リポジトリ

github.com

下準備

Google APIにアクセスできるように準備する

詳細は以下のブログ記事にまとめています。

shirotamaki.hatenablog.com

認証ライブラリのNextAuth.jsを使ってGoogleログイン機能を実装する

Next.jsプロジェクトを作成する

~/blog_code_examples
❯ node -v
v18.14.2

~/blog_code_examples
// @latest最新版を指定プロジェクトを作成する
❯ npx create-next-app@latest

// プロジェクトネームを入力
✔ What is your project named? … frontend

// TypeScriptを導入するか? →Noを選択
✔ Would you like to use TypeScript with this project? … No / Yes

// ESLinstを導入するか? →Yesを選択
✔ Would you like to use ESLint with this project? … No / Yes

// src/ディレクトリを使用するか? →Yesを選択
✔ Would you like to use `src/` directory with this project? … No / Yes

// app/ディレクトリを使用するか? →Noを選択
? Would you like to use experimental `app/` directory with this project? › No / ✔ Would you like to use experimental `app/` directory with this project? … No / Yes

// エイリアスの設定 →デフォルトで@/を設定する
✔ What import alias would you like configured? … @/*

必要なnpmをインストールする

axiosについては後編で言及します。

❯ npm install next-auth axios

API routeの設定を行う

API routeは、Dynamic Routes、Dynamic Routing、動的ルーティングとも呼ばれるしくみのことです。

Routing: Dynamic Routes | Next.js

Dynamic Routingは[props], [...props], [[...props]]と3種類の記述方法があります。

path /foo /foo/bar /foo/bar/baz
/foo/[props].ts
/foo/[...props].ts
/foo/[ [...props] ].ts

pages/api/auth[...nextauth].tsというファイルを作成しDynamic Routingを設定しています。 callbaks:には、signInが実行された時に呼び出す指示を書きます。ここでRails APIを叩いて、アプリにユーザー情報が未登録の場合はユーザー情報を保存する機能を実装したいと思います。バックエンド側の実装については後編で説明します。

以下のプログラムで認証プロバイダーをGoogleに指定します。こうすることでGoogle認証を行うことができます。

// frontend/src/pages/api/auth/[...nextauth].ts

import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
})

clientIdclientSecretには、次のステップで環境変数を指定します。

.env.localファイルを生成しGoogle認証のために必要な環境変数を設定する

Google API Consoleで設定したクライアントIDとクライアントシークレットを環境変数に設定します。

  • your-google-client-id: クライアントID
  • your-google-client-secret: クライアントシークレット

ついでにNEXTAUTH_URLNEXT_PUBLIC_API_URLも設定しておきます。

NEXTAUTH_URLは、Next.js アプリケーションでNextAuth.jsを使って認証を実装する際に必要な環境変数を指します。この環境変数は、Next.jsアプリケーションのベースURLを指定することになります。Next.jsの開発環境のサーバーはポート番号を変更したのでhttp://localhost:4000 として設定しておきます。

ちなみにこのNEXTAUTH_URL環境変数名は変更不可です。私は環境変数名を変更したことが原因でだいぶハマりました...。

NEXT_PUBLIC_API_URLは、API側のベースURLになります。Railsの開発環境のサーバーのポート番号はhttp://localhost:3000として設定しておきます。

// frontend/.env.local

GOOGLE_CLIENT_ID=<your-google-client-id>
GOOGLE_CLIENT_SECRET=<your-google-client-secret>

NEXTAUTH_URL=http://localhost:4000
NEXT_PUBLIC_API_URL=http://localhost:3000

Session Providerの設定

Session Providerを設定すると、useSession() のインスタンスで、React Context(各レベルで手動でpropsを渡すことなく、ツリー構造全体でデータを受け渡すことができるしくみのこと) を使用して、コンポーネント間でセッションオブジェクトを共有することができます。タブやウィンドウ間でセッションの更新や同期を維持することもできます。

ここで設定することで、アプリ全体でNextAuth.jsのセッション情報を共有することができます。SessionProviderにsessionプロパティを渡すことで、各ページコンポーネントでuseSession()フックを使用してセッション情報にアクセスできるようになります。また、ComponentにpagePropsを渡すことで、現在のページコンポーネントに必要なプロパティが提供されます。

ややこしいですが...要は、Sessionの内容をアプリ全体で確認できるようにするには、SessionProviderの設定が必要なので、そのための設定については_app.tsxに書く必要がありますよ!ということです。

// frontend/src/pages/_app.tsx

import { SessionProvider } from 'next-auth/react'

export default function App({ Component, pageProps: { session, ...pageProps } }) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

ログイン機能を作成する

components/Login.jsxを作成し、ログイン機能を実装します。

ファイル名はパスカルケースケースにします。Next.jsの命名規則を参考にしました。

以下、コードの動きです。

  • useSession()を使用して、ログインしているかどうかを判定する
  • NextAuth.jsのsignIn()フックを使用して、Googleでログインを実行する
    • useSession()signIn()は、NextAuth.jsが提供しているフックになる
// frontend/src/components/Login.jsx

import React from 'react'
import { useSession, signIn } from 'next-auth/react'

const Login = () => {
  const { data: session, status } = useSession()

  if (status === 'loading') {
    return <div>Loading...</div>
  }

  if (status !== 'authenticated') {
    return (
      <div>
        <p>あなたはログインしていません</p>
        <button onClick={() => signIn('google', null, { prompt: 'login' })}>
          Googleでログイン
        </button>
      </div>
    )
  }
  return null
}

export default Login

毎回ログインする度にユーザーに再認証を要求する

signIn()フックに引数を指定することで、毎回ログインする度にユーザーに再認証を要求することができます。

signIn('google', null, { prompt: 'login' })
  • google

    • 認証プロバイダーとしてGoogleを指定。これにより、ユーザーはGoogleアカウントで認証できる。
  • null

    • 通常、この位置には認証成功後に呼び出されるコールバック関数が設定される。
    • nullが指定されているため、デフォルトのコールバック関数が使用される。
    • 今回はfrontend/src/pages/api/auth/[...nextauth].tsで設定しているsignIn()が呼び出される。※実装は後編で行う。
  • prompt: 'login'

    • オプションオブジェクトで、promptプロパティがloginに設定されている。この設定により、ユーザーがすでに認証済みであっても毎回認証が求められることになる。通常、認証済みのユーザーは再度ログインする必要はないが、このオプションにより毎回ログイン画面に遷移するように設定した。

ログアウト機能を作成する

  • NextAuth.jsのsignOut()フックを使用して、ログアウトする。
// frontend/src/components/Logout.jsx

import React from 'react'
import { useSession, signOut } from 'next-auth/react'

const Logout = () => {
  const { data: session, status } = useSession()

  if (status === 'authenticated') {
    return (
      <div>
        <button onClick={() => signOut()}>ログアウト</button>
      </div>
    )
  }
  return null
}

export default Logout

indexページを用意して、ログイン機能とログアウト機能を実装する

// frontend/src/pages/index.jsx

import React from 'react'
import Head from 'next/head'
import { useSession } from 'next-auth/react'
import Login from '@/components/Login'
import Logout from '@/components/logout'

export default function Home() {
  const { data: session, status } = useSession()

  return (
    <>
      <Head>
        <title>next-rails-google-auth</title>
      </Head>
      <div>
        <h1>next-rails-google-auth</h1>

        {status === 'authenticated' ? (
          <div>
            <p>セッションの期限:{session.expires}</p>
            <p>ようこそ、{session.user.name}さん</p>
            <img src={session.user.image} alt='' style={{ borderRadius: '50px' }} />
            <div>
              <Logout />
            </div>
          </div>
        ) : (
          <Login />
        )}
      </div>
    </>
  )
}

useSession()

useSession()の役割についてもまとめておきます。

NextAuth.jsが提供しているフックです。誰かがサインインしているかどうかを確認する最も簡単な方法になります。

  const { data: session, status } = useSession()

ここはではdataという決まった値に対してsessionという別名を付けています。名前は何でも構わないのですが慣例的にsessionとするようです。

datastatus の 2 つの値を含むオブジェクトを返します。

dataは、3つの値をとることができます。session undefined null

  • セッションがまだ取得されていない場合、データはundefinedになります。
  • セッションの取得に失敗した場合、データはnullになります。
  • セッションの取得に成功した場合、データはsessionとなる。

dataの型、言い換えると…

  • fetch前はundefined
  • fetchに失敗した場合はnull
  • fetchに成功した場合はsession

statusは、 3つの可能なセッション状態に対応する列挙型です。

  • loading ロード中
  • authenticated 認証された
  • unauthenticated 認証されていない

ここまでの結果

無事、Next.js(フロント側)のログイン、ログアウト機能の実装が完了しました。

ログイン画面

Googleログイン認証画面

ログアウト画面

一度認証が完了して許可されている場合(Googleアカウントがすでにアプリに登録されている場合)は、そのままログインが完了します。初めてアプリに対してアクセスするGoogleアカウントの場合は、Googleアカウントへのログイン画面やアカウントの選択画面が表示されるので、Googleアカウントのメールアドレス、パスワードから入力し、場合によっては2段階認証の必要があります。

一旦これで、フロント側だけの実装は完了です。データベースに保存できる実装にはなっていません。

次はRails APIを叩き、データベースにログインユーザーの情報を保存する方法についてまとめたいと思います。

後編へ続きます。