📑

Firebase の認証機能だけを使う

2023/12/03に公開

この記事は mob Advent Calendar 3日目の記事になります。

本日は Firebase の認証機能だけを使って、フロントエンドとバックエンドを実装していくながれを紹介します。

全体感

図のような感じで Firebase 側に認証をお任せして実装をします。

前提

  • フロントエンドのフレームワークは SSGモードの Next.js で実装
    • Cloud Front と S3 でホスティング
  • バックエンドは Kotlin で実装し、フレームワークは micronaut を選択
    • AWS Lambda にのせる

Firebase のプロジェクトを作る

まずは Firebase のプロジェクトを作ります。

Firebase Auth を使うために、 Authentication を開いて「始める」を選択をして、また Sign-in method を開いて適切なものを選択しておきましょう。

Client サイドを実装する

ログインやログアウト周りの実装

「プロジェクトの設定」 -> 「全般」 -> 「マイアプリ」で 「</>」(webアプリ)を選択。
手順通りにアプリの登録を行なっていきます。

$ yarn add firebase  

自分は Next.js なのでこんな感じで実装しました。

まずは Auth インスタンスを使えるように AuthProvider を準備。

authContext.ts
export const AuthContext = createContext<{
  auth: Auth;
}>(undefined);
AuthProvider.tsx
interface AuthProviderProps {
  children: ReactNode;
}

export function AuthProvider(props: AuthProviderProps) {
  const app = initializeApp({
    // firebase側で表示されてる appKey など
  });
  const auth = getAuth(app);
  return (
    <AuthContext.Provider value={{ auth }}>
      {props.children}
    </AuthContext.Provider>
  );
}
useAuth.ts
function useAuth(): Auth {
  const { auth } = useContext(AuthContext);
  return auth;
}

export default useAuth;

ログイン状態の検証を AuthProvider を使って _app.tsx でやりつつ、 認証してれば ログイン後のコンポーネントを、してなければログインコンポーネントを表示する。

_app.tsx
function Body(props: BodyProps) {
  const auth = useAuth();
  const [loggedIn, setLoggedIn] = useState<boolean>(null);
  useEffect(() => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        setLoggedIn(true);
      } else {
        setLoggedIn(false);
      }
    });
  }, []);
  if (loggedIn === null) {
    return <></>;
  } else if (!loggedIn) {
    const provider = new GoogleAuthProvider();
    const handleLogin = () => {
      signInWithRedirect(auth, provider);
    };
    return <Login handleLogin={handleLogin} />;
  } else {
    return <LoggedInBody>{props.children}</LoggedInBody>;
  }
}

実装できたら実際にログインログアウトを動作確認してみましょう。

自前のAPI に通信する時に Header に認証情報を付与する

こんな感じで Authorization Header に Bearer xxxx の形になるように付与します。

useApiClient.ts
export default function useApiClient() {
  const { bearerToken } = useContext(BearerContext);
  return createClient<paths>({
+    headers: { Authorization: `Bearer ${bearerToken}` },
    baseUrl: "https://{apipath}",
  });
}

bearerToken は auth.currentUser.getIdToken() で取得できます。

バックエンドを実装する

micronaut を使っているので、その前提で紹介します。

json ファイルをダウンロード

firebase-admin を使用するため、 サービスアカウントの json ファイルをダウンロードします。

Firebaseコンソールを開き、「プロジェクトの設定」 -> 「サービスアカウント」と開き「新しい秘密鍵を生成」を押せば json ファイルをダウンロードできます。

ダウンロードしたら、その json ファイルをプロジェクトの src/main/resources/ 配下に配置します。

実装

まずは firebase-admin の依存を追加します。

implementation("com.google.firebase:firebase-admin:7.0.1")

token の verify 機能をもつ FirebaseAuthService.kt を実装します。

FirebaseAuthService.kt
@Singleton
class FirebaseAuthService {

  @PostConstruct
  fun onSetUp() {
    try {
      val serviceAccount = javaClass.getClassLoader().getResourceAsStream("serviceAccount.json")
      val options = FirebaseOptions.builder()
        .setCredentials(GoogleCredentials.fromStream(serviceAccount))
        .build()

      if (FirebaseApp.getApps().isEmpty()) {
        FirebaseApp.initializeApp(options)
      }
    } catch (e: Exception) {
      ...
    }
  }

  fun verifyToken(authorizationHeader: String): FirebaseToken? {
    return try {
      FirebaseAuth.getInstance().verifyIdToken(authorizationHeader.substring("Bearer ".length))
    } catch (e: Exception) {
      null
    }
  }
}

onSetUp() で Firebase の初期化します。 verifyToken で トークンのverify を行います。

そして、下記のように FirebaseAuthService を用いて verifyToken をして、 verify に失敗したら、unauthorized を返却します。

Controller.kt
@Controller("/api")
class Controller {
  ...

  @Inject
  lateinit var firebaseAuthService: FirebaseAuthService

  fun index(
    @Header("Authorization") authorizationHeader: String,
  ): HttpResponse<Result> {
    firebaseAuthService.verifyToken(authorizationHeader) ?: return HttpResponse.unauthorized()

    ...
    return HttpResponse.ok(result)
  }
}

これで、一通りの実装が完了しました。

Google認証などは自前でやろうとすると結構やることが多くて面倒なのですが、Firebase を使うことでかなり簡単に実装することができました。

Discussion