LoginSignup
1
1

More than 3 years have passed since last update.

Node.js + FirebaseでStatelessなAPIを実装する

Posted at

留意事項

メモ書き程度なので詳細な説明は割愛します。

使う技術やサービス

  • Firebase Authentication
  • Node.js(FWはExpress)

Firebase側の準備

プロジェクト作成し、Authentication -> Sign-in MethodでGoogleを有効にしておきます。
Firebase側の準備はこれで終わりです。

実装

firebase-toolsをグローバルではなくローカルにインストールして進めていきます(ちょっと訳あり)。

準備

適当なディレクトリを作って

$ npm init -y
$ npm i firebase-tools
$ npx firebase login

ログインが終わったら

$ npx firebase init

以下、ログ

?  Are you ready to proceed? (y/N)  y

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.
 ( ) Database: Deploy Firebase Realtime Database Rules
 ( ) Firestore: Deploy rules and create indexes for Firestore
 (*) Functions: Configure and deploy Cloud Functions
 (*) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
 ( ) Emulators: Set up local emulators for Firebase features

? Please select an option:
> Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project

? Select a default Firebase project for this directory: (Use arrow keys)
> hogehoge-xxxxx (hogehoge)

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? (Use arrow keys)
> JavaScript
  TypeScript

? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) n
+  Wrote functions/package.json
+  Wrote functions/index.js
+  Wrote functions/.gitignore

? Do you want to install dependencies with npm now? (Y/n) y

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

+  Firebase initialization complete!

バックエンド

functionsディレクトリ内で作業する。

$ npm i express cookie-parser

index.js

const admin = require("firebase-admin");
const cookieParser = require("cookie-parser")();
const express = require("express");
const functions = require("firebase-functions");

const api = express();

admin.initializeApp();

const validateFirebaseIdToken = async (req, res, next) => {
  if (
    (!req.headers.authorization ||
      !req.headers.authorization.startsWith("Bearer ")) &&
    !(req.cookies && req.cookies.__session)
  ) {
    res.status(403).send("Unauthorized");
    return;
  }

  let idToken;
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith("Bearer ")
  ) {
    console.log('Found "Authorization" header');
    idToken = req.headers.authorization.split("Bearer ")[1];
  } else if (req.cookies) {
    console.log('Found "__session" cookie');
    idToken = req.cookies.__session;
  } else {
    res.status(403).send("Unauthorized");
    return;
  }

  try {
    const decodedIdToken = await admin.auth().verifyIdToken(idToken);
    console.log("ID Token correctly decoded", decodedIdToken);
    req.user = decodedIdToken;
    next();
    return;
  } catch (err) {
    console.error(err);
    res.status(403).send("Unauthorized");
    return;
  }
};

api.use(cookieParser);
api.use(validateFirebaseIdToken);

api.get("/", (req, res) => {
  res.status(200).send(JSON.stringify(req.user));
});

exports.api = functions.https.onRequest(api);

フロントエンド

publicディレクトリ内で作業する。

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Sample</title>
  </head>
  <body>
    <button id="demo-sign-in-button" style="display: none;">
      Sign in with Google
    </button>
    <button id="demo-sign-out-button" style="display: none;">
      Sign out
    </button>

    <script src="/__/firebase/7.14.2/firebase-app.js"></script>
    <script src="/__/firebase/7.14.2/firebase-auth.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script src="index.js"></script>
  </body>
</html>

index.js

document.addEventListener("DOMContentLoaded", function () {
  signInButton = document.getElementById("demo-sign-in-button");
  signOutButton = document.getElementById("demo-sign-out-button");

  signInButton.addEventListener("click", signIn);
  signOutButton.addEventListener("click", signOut);
  firebase.auth().onAuthStateChanged(onAuthStateChanged);
});

const onAuthStateChanged = (user) => {
  if (user) {
    console.info(user);
    signInButton.style.display = "none";
    signOutButton.style.display = "block";
    startFunctionsCookieRequest();
  } else {
    console.info("!user");
    signInButton.style.display = "block";
    signOutButton.style.display = "none";
  }
};

const signIn = () => {
  firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider());
};

const signOut = () => {
  firebase.auth().signOut();
  document.cookie = "__session=";
};

const startFunctionsCookieRequest = () => {
  firebase
    .auth()
    .currentUser.getIdToken(true)
    .then(function (token) {
      document.cookie = "__session=" + token + ";max-age=86400";
    });
};

プロダクト開発ではAuthorization: Bearer <token>を使うようにしよう(手抜き)。

動作確認

プロジェクトルートディレクトリで作業する。

$ npx firebase emulators:start

Hostingは5000番ポート、Functionsは5001番ポートで起動しますのでそれぞれ確認。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1