はじめに
Next.jsとRailsを使ってオリジナルのWebアプリを開発しています。
ログイン機能を備えたアプリにするために、Google認証でログインする方法を選択しました。この記事では、実装過程において調べた情報や試行錯誤しハマった経験などをまとめたいと思います。
前編、後編の2部構成でお届けします。
前編
Next.jsのNextAuth.jsを使用しGoogle認証ログイン、ログアウトに焦点を当てた内容です。
後編
ログインしたユーザーの情報をデータベースに保存する方法について焦点を当てた内容です。Rails APIを叩いてデータベースに保存する方法についてまとめています。
後編はこちら
技術スタック
フロントエンド:
- Next.js '13.2.4'
- 使用するnpm
- next-auth '4.20.1'
- axios '1.3.4'
バックエンド:
データベース:
リポジトリ
下準備
Google APIにアクセスできるように準備する
詳細は以下のブログ記事にまとめています。
認証ライブラリの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, }), ], })
clientId
とclientSecret
には、次のステップで環境変数を指定します。
.env.localファイルを生成しGoogle認証のために必要な環境変数を設定する
Google API Consoleで設定したクライアントIDとクライアントシークレットを環境変数に設定します。
ついでにNEXTAUTH_URL
とNEXT_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
null
- 通常、この位置には認証成功後に呼び出されるコールバック関数が設定される。
null
が指定されているため、デフォルトのコールバック関数が使用される。- 今回は
frontend/src/pages/api/auth/[...nextauth].ts
で設定しているsignIn()
が呼び出される。※実装は後編で行う。
prompt: 'login'
- オプションオブジェクトで、promptプロパティが
login
に設定されている。この設定により、ユーザーがすでに認証済みであっても毎回認証が求められることになる。通常、認証済みのユーザーは再度ログインする必要はないが、このオプションにより毎回ログイン画面に遷移するように設定した。
- オプションオブジェクトで、promptプロパティが
ログアウト機能を作成する
- 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
とするようです。
data
と status
の 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を叩き、データベースにログインユーザーの情報を保存する方法についてまとめたいと思います。
後編へ続きます。