Tags:
React
Firebase Authentication
Firebase Authentication の認証を signInWithRedirect にする
Firebase Authentication の認証を signInWithRedirect にする実装について。
開発環境
- firebase: 9.9.1
概要
Firebase Authentication による認証について、『JavaScript による Google を使用した認証』のページに
ユーザーに Google アカウントでログインするよう促すために、ポップアップ ウィンドウを表示するか、ログインページにリダイレクトします。モバイル デバイスではリダイレクトすることをおすすめします。
と書いてあるので signInWithPopup
を使用した認証から signInWithRedirect
に変更した。
実装の方針
既に決めている仕様として
- /login にログイン済のユーザーがアクセスした場合、/home にリダイレクトしたい
- /login でログイン操作を実行した場合、サーバーサイドで customClaim を付与するので、/home にリダイレクトする前に customClaim を読み込み直したい
というのがあった。
signInWithRedirect
を使用して /login 画面内だけで実装しようとするとどうしても前述の仕様達成が難しかったので、下記のように実装することにした。
- /login で「ログイン」ボタンを押下すると、/login/loading に画面遷移する
- /login/loading ではユーザーが
signInWithRedirect
で返ってきた状態かどうかを判別する。 signInWithRedirect
から返ってきた状態ならサーバーサイドへのログイン処理を実行する。signInWithRedirect
から返ってきた状態でなく、未認証状態の場合は Google のログインページへリダイレクトする。
なおブラウザの履歴等から直接 /login/loading に飛んできた認証済ユーザーがいる場合、/login/loading では何も起こらないようにした。
実装
関係ない部分なるべく削った実装例。
useCurrentUser
ユーザーの認証状態を取得するカスタムフック。
ログイン画面とローディング画面に出てくるので実装を掲載する。
import { getAuth, onAuthStateChanged } from 'firebase/auth'
import type { User } from 'firebase/auth'
import { useState, useEffect } from 'react'
import { app } from '@/config'
export type UserType = User | null
export const useCurrentUser = () => {
const auth = getAuth(app)
const [user, setUser] = useState<UserType>(null)
const [isAuthChecking, setIsAuthChecking] = useState(true)
const [role, setRole] = useState<string | undefined>(undefined)
useEffect(() => {
const authStateChanged = onAuthStateChanged(auth, async (user) => {
setUser(user)
if (user) {
const claims = (await user.getIdTokenResult())?.claims
const role = claims?.['role']
if (role) {
setRole(role as string)
}
} else {
setRole(undefined)
}
setIsAuthChecking(false)
})
return () => {
authStateChanged()
}
}, [auth])
return {
user,
isAuthChecking,
role,
}
}
ログイン画面(/login)
既にログイン済だったら /home にリダイレクトする。
まだログインしていない場合はログインボタンを押下されたら /login/loading にリダイレクトする。
import { Heading, Spinner } from '@chakra-ui/react'
import type { NextPageWithLayout } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import LoginButton from '@/components/pages/login/LoginButton'
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { Layout } from '@/layout/Layout'
const Login: NextPageWithLayout = () => {
const { isAuthChecking, user: currentUser } = useCurrentUser()
const router = useRouter()
const login = () => {
router.push('/login/loading')
}
useEffect(() => {
// ログイン済だったらホームに飛ぶ
if (currentUser) {
router.push('/home')
}
}, [currentUser, router])
if (isAuthChecking || currentUser) {
return <Spinner size='xl' />
}
return (
<div>
<Head>
<title>ログイン</title>
</Head>
<main>
<Heading as='h1' size='4xl' mt={2} mb={2}>
ログイン
</Heading>
<LoginButton onClick={login} />
</main>
</div>
)
}
Login.getLayout = (page) => <Layout>{page}</Layout>
export default Login
ローディング画面(/login/loading)
Google 認証画面にリダイレクトし、戻ってきたらサーバーサイドの認証処理を実行して /home へリダイレクトする。
import { Spinner, useToast } from '@chakra-ui/react'
import {
getAuth,
onAuthStateChanged,
signInWithRedirect,
getRedirectResult,
signOut,
} from 'firebase/auth'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useEffect, useState, useCallback } from 'react'
import { provider } from '@/config'
import { CONSTANT } from '@/constants'
import { useLoginMutation } from '@/graph/guest/login.generated'
import { LoginInput } from '@/graph/types.generated'
import { useCurrentUser } from '@/hooks/useCurrentUser'
const LoginLoading = () => {
const { isAuthChecking, user: currentUser } = useCurrentUser()
const [isError, setIsError] = useState(false)
const auth = getAuth()
const router = useRouter()
const toast = useToast()
const { mutateAsync } = useLoginMutation({
onError: async (e: any) => {
console.error(e)
setIsError(true)
toast({
title: 'ログインに失敗しました',
status: 'error',
isClosable: true,
})
await signOut(auth).catch((error) => {
alert(`ログアウトに失敗しました。エラーコード:${error.code}`)
})
router.push('/login')
},
onSuccess: () => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user) {
// カスタムクレームを更新するためにユーザーを読み込みなおしてからトークンリフレッシュ
await user.reload()
await user.getIdTokenResult(true)
router.push('/home')
} else {
setIsError(true)
alert('ログインに失敗しました')
router.push('/login')
}
})
unsubscribe()
},
})
const mutateLogin = useCallback(async () => {
const input: LoginInput = {
token: (await currentUser?.getIdToken()) ?? '',
}
await mutateAsync({
input: input,
})
}, [mutateAsync, currentUser])
useEffect(() => {
if (isAuthChecking) {
return
}
const login = async () => {
const result = await getRedirectResult(auth)
if (!result?.user) {
if (!currentUser && !isError) {
await signInWithRedirect(auth, provider)
}
} else {
if (!isError) {
mutateLogin()
}
}
}
login()
}, [isAuthChecking, currentUser, isError, auth, mutateLogin])
return (
<div>
<Head>
<title>ログイン</title>
</Head>
<main>
<Spinner size='xl' />
</main>
</div>
)
}
export default LoginLoading
以上