💪

【Next.js x Firebase】OAuth認証後のonAuthStateChangedのラグを誤魔化したい

2022/03/06に公開約3,500字

この記事の前提

  • Next.js
  • Firebase(v9)
  • 認証機能は/loginページに実装する

※実用的な話ではないのでネタとしてお楽しみください

前置き

FirebaseのOAuth認証メソッドの一つであるsignInWithRedirectを使うと、認証用の別サイトにリダイレクトしてしまう("With Redirect" やからな)。その上、OAuth認証後に戻ってくるのはOAuth認証前のURLだから、認証後にトップページに戻ってくることもできない。

もちろんログインボタンを押したらrouter.push()でそのままリダイレクトさせる、なんてことも出来ない。しかし、Global Stateと組み合わせればOAuth認証後の自動リダイレクトを間接的に実装することができる。

自動リダイレクト

  1. 初期値がnullのGlobal Stateを作ってあげて(ここではRecoilを使っている)、
const User = atom({
  key: "user",
  default: null
});
  1. onAuthStateChangedのコールバックでユーザー情報を取得し、
useEffect(() => {
  onAuthStateChanged(auth, (user) => {
    setUser(user);
  });
}, [])
  1. ログインを実装するページで、userがfalsyでないならトップにリダイレクトするようにすれば、
const { user } = useUser();

useEffect(() => {
  user && router.push("/")
}, [user]);

OAuth認証画面から戻ってきて、DOMマウント後onAuthStateChangedのコールバックが発火したときに好きなページにリダイレクトさせることができる。結構シンプルやね。

『認証中』画面が欲しい

しかし実はリダイレクトだけの実装には穴があって、DOMのマウントからonAuthStateChangedコールバックの発火までラグがあり、その間ログイン画面が剝き出しになってしまう。う~んカッコよくないな~。

これはリダイレクト元のURLに戻ってくるOAuth認証の仕様上どうしようもないので、『リダイレクトするまで認証中です的な画面で覆いたいよな』という考えに至った。


Vercelのリダイレクト画面

それではやっていきましょう。

Authenticating...

Reactにおいて画面切り替えといえばuseStateが真っ先に思い浮かぶが、OAuth認証のためにアンマウントしているせいでuseRouter同様その手は使えない。

そうなると、『ログインボタンからアクセスしたときにはあって、認証画面から戻ってきたときにはないパラメーター(もしくは逆)』を作る必要がある。そんなん作れるっけ…?

next/linkがあるじゃない

ここで「確かnext/linkに見せかけのURLを設定する機能があったような…?」という記憶がおぼろげながら浮かんできた。よし、next/linkをおさらいしてみよう。

<Link
  href={{
    pathname: "...",
    query: {}
  }}
  prefetch={}
  as="..."
  // ...その他いろいろ
>
  <a>This is Link</a>
</Link>

ここでasの公式ドキュメントを見てみよう。

as ― Optional decorator for the path that will be shown in the browser URL bar. Before Next.js 9.5.3 this was used for dynamic routes, check our previous docs to see how it worked. Note: when this path differs from the one provided in href the previous href/as behavior is used as shown in the previous docs. (全文)

意訳
asで指定した文字列はブラウザに表示されるパスになるよ。Next 9.5.3まではDynamic Routesへの遷移で使ってたけど、今はhrefだけで行けるようになったよ。

お?asでブラウザに表示されるURLを設定できる?ひょっとしてURLからクエリを隠して遷移できるってこと?というわけで試してみる。

asで隠したクエリは渡せるか

このようなLinkコンポーネントを作ってみる。

<Link
  href={{
    pathname: "/login",
    query: { nullUser: "true" }  // 便宜上
  }}
  as="/login"
>
  <a>Login</a>
</Link>


結果@login.tsx

...行けるやん...!!!

JSによる遷移では/login{ nullUser: "true" }を受け取ることができるが、外部からのリダイレクトでアクセスしたときは受け取ることができない。これで「ログインボタンからアクセスしたときは~」が実装できる。

実装

じゃあ実際に作ってみよう。nullUserの値でLoginViewAuthenticatingを切り替えている。

const LoginView: VFC = () => {
  const login = () => {
    signInWithRedirect(auth);
  };
  return (
    //...
  );
};

const LoginPage: VFC = () => {
  const { push, query } = useRouter()
  useEffect(() => {
    user && push("/")
  }, [user])

  const { nullUser }: { nullUser: "true" } = query;
  return nullUser ? <LoginView /> : <Authenticating />;
};
export default LoginPage;

無理やりですが


実際に作ってみたやつ

かなり力ずくの方法だとは思うが、Next.jsとFirebaseでOAuth認証後に『認証中』を表示してみた。もちろんこの方法にも穴はあって、/loginでリロードされると強制認証中になってしまう。まあログインページでリロードする人はそんなにいないと思うけど…

Discussion

ログインするとコメントできます