🧚‍♀️

React Server Componentsで認証の実装例(Firebase編)

7 min read
graph TB
    D[ リモートワーカーの地位を向上 ] --> C
    E[ エンジニアリングの本質を追求 ] --> C 
    C[ VTEACHER の MISSION ]

こちらも参考に

https://zenn.dev/rgbkids/articles/7cbb158c52781d

React Server Componentsの認証

認証について

Firebaseを使った認証を考えます。
ただし、あくまで認証のみの利用とします。
FirebaseのCloudストレージなどは、基本的にクライアント側での非同期処理となり(結果としてuseEffect=副作用の多様となり)、React Server Componentsのせっかくのサーバーサイド処理(RDBに対しての処理)が整合性が取りづらくなり、アプリの実装が辛くなります。
そのため、Firestore等のDatabaseは利用しません。基本はFirebaseのAuthenticationのみを利用すします。ログイン後にuidとtoken(30分ごとにリフレッシュされる)を取得し、RDBに保存します。

このuidとtokenは、fetchの際、認証されます。

シーケンス図

sequenceDiagram
    User->>Firebase: sign in
    activate Firebase
    Firebase-->>User: uid, token
    deactivate Firebase
    User-->>PostgreSQL: uid, token

Authの使用例

import Auth from "./Auth.client";
<Auth />

これによりAuth(Firebaseの認証)がはいります。

Authのコードです。Firebaseを使っているので必ずclient.js(クライアントコンポーネント)にしてください。

個別の情報は settings.js に記述しておきます。

export const firebase_config = {
    apiKey: "",
    projectId: "",
    authDomain: "",
    databaseURL: "",
};
import firebase from 'firebase';

export const useSignIn = () => {
    let provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithRedirect(provider);
};

export const useSignInPopup = () => {
    let provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider);
};

export const useSignOut = () => {
    firebase.auth().signOut();
};

export const useCurrentUser = () => {
    return firebase.auth().currentUser;
};

export const useFirebase = () => {
    return firebase;
}

if (firebase.apps.length == 0) {
    const firebase_config = require(__dirname + '/../settings');
    const firebaseConfig = firebase_config["firebase_config"];

    firebase.initializeApp(firebaseConfig);
}

ちなみにFirebaseのバージョンが古い例なので、ただFirebaseの最新版にするだけだと物足りないと思うので、話題のSupabaseに挑戦してみたらいかがでしょう。

Authの実装コード

Firebaseを使っているので、必ずclient.js(クライアントコンポーネント)にしてください。

import {useFirebase, useSignIn, useSignOut} from './firebase';
import {useEffect, useState, useTransition} from "react";
import {useLocation} from "./LocationContext.client";
import Spinner from './Spinner';
import {useRefresh} from "./Cache.client";
import {createFromReadableStream} from "react-server-dom-webpack";

const host = location.host;
const protocol = location.protocol;

export default function Auth({lang, signInText, signOutText}) {
    const [location, setLocation] = useLocation();
    const [isPending, startTransition] = useTransition();

    const [authSetting, setAuthSetting] = useState(false);
    const [signed, setSigned] = useState(false);
    const [user, setUser] = useState(null);
    const [spinning, setSpinning] = useState(true);

    const [, startNavigating] = useTransition();
    const refresh = useRefresh();

    function navigate(response) {
        const cacheKey = response.headers.get('X-Location');
        const nextLocation = JSON.parse(cacheKey);
        const seededResponse = createFromReadableStream(response.body);
        startNavigating(() => {
            refresh(cacheKey, seededResponse);
            setLocation(nextLocation);
        });
    }

    async function handleCreateUser(user_id, token, lang) {
        let _lang = (lang) ? lang : localStorage.getItem("lang");

        const payload = {user_id, token};
        const requestedLocation = {
            selectedId: "",
            isEditing: false,
            searchText: "",
            selectedTitle: "",
            selectedBody: "",
            userId: user_id,
            token: token,
            lang: _lang,
        };
        const endpoint = `${protocol}//${host}/users/`;
        const method = `POST`;
        const response = await fetch(
            `${endpoint}?location=${encodeURIComponent(JSON.stringify(requestedLocation))}`,
            {
                method,
                body: JSON.stringify(payload),
                headers: {
                    'Content-Type': 'application/json',
                },
            }
        );
        navigate(response);
    }

    useEffect(() => {
        if (!authSetting) {
            setAuthSetting(true);

            useFirebase().auth().onAuthStateChanged(_user => {
                setSpinning(false);

                if (_user) {
                    setUser(_user);
                    setSigned(true);

                    const tokenEncode = encodeURI(_user.refreshToken);
                    let _lang = (lang) ? lang : localStorage.getItem("lang");

                    handleCreateUser(_user.uid, tokenEncode, _lang);
                }
            });
        }
    });

    async function handleSignIn() {
        setSpinning(true);
        useSignIn();
    }

    async function handleSignOut() {
        setSpinning(true);

        useSignOut();

        setAuthSetting(false);
        setSigned(false);
        setUser(null);

        let _lang = (lang) ? lang : localStorage.getItem("lang");

        startTransition(() => {
            setLocation((loc) => ({
                selectedId: "",
                isEditing: false,
                searchText: "",
                selectedTitle: "",
                selectedBody: "",
                userId: "",
                token: "",
                lang: _lang,
            }));
        });
    }

    return (
        <div className="auth">
            {signed
                ?
                <>
                    <a onClick={() => {
                        handleSignOut()
                    }}>
                        {spinning
                            ?
                            <span><Spinner active={spinning}/></span>
                            :
                            <>
                                <span className="auth-button-sign-out">{signOutText}</span>
                            </>
                        }
                    </a>
                </>
                :
                <>
                    <a onClick={() => {
                        handleSignIn()
                    }}>
                        {spinning
                            ?
                            <span><Spinner active={spinning}/></span>
                            :
                            <>
                                <span className="auth-button">{signInText}</span>
                            </>
                        }
                    </a>
                </>
            }
        </div>
    );
}

さいごに

依存関係を排除して、疎結合にしておくと、Authを他のプロジェクトでも利用できるようになります。

Discussion

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