5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Next.jsでfirebase adminを使ってメール認証本文をカスタムする

Posted at

Firebase Authenticationという大変便利なサービスがある。ユーザー認証とデータ保存が簡単にできるようになる。

使い方の記事もたくさんある

ユーザー登録時のよくあるUXとしては、

  • メールアドレスとパスワード登録
  • メールアドレスに認証リンクが届く
  • クリックすると、ログイン後

これは、新規登録したいメールアドレス宛にアクティベーションメールを送信する。アクティベーションメールは、メール中にリンクを張りそのリンク先にアクセスされたら認証するというもの。有効なメールアドレスかどうかを検証するスタンダードな方法である。

Firebaseでもこの機能が提供されているのだが、Firebaseでは、スパム防止のため認証メールのメール本文を編集できない(Firebaseではテンプレートと呼ばれる)。

日本語のテンプレートは現在以下のようになっている。DISPLAY_NAMEは値を渡さなければ、その行が表示されないので、「様」だけが文頭に表示されるということはないのでご安心を。

%DISPLAY_NAME% 様

メールアドレスを確認するには、次のリンクをクリックしてください。

https://<firebase project id>.firebaseapp.com/__/auth/action?mode=action&oobCode=code

このアドレスの確認を依頼していない場合は、このメールを無視してください。

よろしくお願いいたします。

%APP_NAME% チーム

このメールの文章を変更したいというのが今回の記事の内容。

Next.jsを使っておこなっていく。

プロジェクトの作成

create-next-appでプロジェクトを作成する。

APIエンドポイントを作ってクライアントサイドから疎通する

APIサーバーを立てる

NodeのWebフレームワークを導入する。

yarn add express

serverというフォルダを作成して、server/index.jsをバックエンドのWeb server用のメインファイルとする。

server/index.js
const express = require("express");
const next = require("next");

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const port = process.env.PORT || 3000;

app.prepare().then(() => {
  const server = express();

  server.all("*", (req, res) => {
    return handle(req, res);
  });
  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on localhost:${port}`);
  });
});

エンドポイントとしてPOSTメソッドを追加する。

server/index.js
app.prepare().then(() => {
  const server = express();

  // 追加
  server.post("/email", (req, res) => {
    res.send("email endpoint called");
  });

  server.all("*", (req, res) => {
    return handle(req, res);
  });
  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on localhost:${port}`);
  });
});

curl -X POST localhost:3000/email       

とすると以下レスポンスが返ってくる。

$ curl -X POST localhost:3000/email 
email endpoint called%                          

ひとまず、 APIエンドポイントを用意できた。

クライアントサイドからFetchでAPIエンドポイントを叩く

先ほど作ったserver/index.jsのコードを多少変更する。

server/index.js
  server.post("/email", (req, res) => {
    console.log("email endpoint called");
    res.json({
      message: "Hello,world",
    });
  });

src/index.jsのjsxに以下のコードを追加する。

src/index.js
        <button onClick={() => post()}>ポストする</button>

post関数は以下

src/index.js
  const post = () => {
    fetch("http://127.0.0.1:3000/email", {
      headers: {
        "Content-Type": "application/json; charset=utf-8",
      },
      method: "POST",
      body: JSON.stringify({ hoge: "fuga" }),
    })
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        console.log(data);
      });
  }

スクリーンショット 2021-09-14 9.39.37.png

ボタンをクリックしてみるとエラーが出る。

スクリーンショット 2021-09-14 9.39.45.png

オリジン間リソース共有(CORS: Cross-Origin Resource Sharing)の制限によりエラーが発生していることがわかる。そこでcorsを導入する。

yarn add cors

corsを利用するように変更

server/index.js
app.prepare().then(() => {
  const server = express();

  server.use(cors());

  server.post("/email", (req, res) => {
    console.log("email endpoint called");
    res.json({
      message: "Hello,world",
    });
  })
  // .. 略
})

修正完了した。
クリックすると、API Endpointのjsonがクライアントサイドで取得できていることがわかる。

スクリーンショット 2021-09-14 9.50.38.png

ここまでで、Next.jsのクライアントサイドとバックエンドがNext.jsのAPIサーバーの機能を通じで疎通するところまで確認できた。

Firebase関連のコードを書いていく

ここから何をやっていくかざっくりと考えると

  • クライアントサイドで、ログイン前/ログイン後画面を作る
    • firebaseで認証する
  • バックエンドでログイン情報を受け取って、メールを取得。それを使って認証リンクを送信する

クライアントサイド

まずクライアントサイドから取り組む。Reactでのfirebaseの導入は以下がわかりやすい。

firebaseはクライアントサイド、firebase-adminはバックエンドで利用する。

yarn add firebase firebase-admin

クライアントサイドの認証情報

ルートにfirebase.jsを作成する。またFirebaseの設定情報は.envに保存する。

firebase.js
import * as firebase from "firebase/app";
import "firebase/auth";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_DATABASE_URL,
  projectId: process.env.NEXT_PUBLIC_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_APP_ID,
};

if (firebase.apps.length === 0) {
  firebase.initializeApp(firebaseConfig);
}

export const auth = firebase.auth();

注意点としては、環境変数名はなんでもいいわけではない。ブラウザでプロジェクトの環境変数を取り込むには、NEXT_PUBLIC_が必要。

In order to expose a variable to the browser you have to prefix the variable with NEXT_PUBLIC_

ユーザー新規登録画面/ログイン後画面を作る

Firebase Authを使うにあたって、

  • ユーザー新規登録画面: pages/index.js
  • ログイン後画面: pages/home.js

を作成する。ログイン後画面では、ログアウトの処理を追加する。

どうしてこういうことをするかというと、 Firebase Authでのログイン/セッション情報はブラウザのIndexedDBに保存されてる。これが残ったままFirebase Authからユーザー登録情報を削除すると、Firebaseにはユーザー情報がないのにブラウザにはそれが残ったままとなり誤作動してしまうため。

pages/index.js

pages/index.jsx
import React, { useState } from "react";
import styles from "../styles/Home.module.css";

import { auth } from "./firebase";

import router from "next/router";

export default function Home() {
  const [email, setEmail] = useState("");
  const [password, setpassword] = useState("");

  const handleSubmit = (event) => {
    event.preventDefault();

    auth
      .createUserWithEmailAndPassword(email, password)
      .then(() => {
        auth.onAuthStateChanged((user) => {
          if (user) {
            // 認証メールを送る場合はこの関数を使う。
            // user.sendEmailVerification()
          }
        });
      })
      .then(() => {
        router.push("/home");
      })
      .catch((error) => {
        alert(error);
      });
  };

  return (
    <div>
      <main className={styles.main}>
        <form onSubmit={handleSubmit}>
          <label htmlFor="email">Email</label>
          <input
            type="email"
            id="email"
            aria-describedby="emailHelp"
            required
            name="email"
            onChange={(event) => setEmail(event.target.value)}
          >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            aria-describedby="passwordHelp"
            placeholder=""
            required
            name="password"
            value={password}
            onChange={(event) => setpassword(event.target.value)}
          >
          <button type="submit">Register</button>
        </form>
      </main>
    </div>
  );
}

アカウント作成後の画面も作る。pages/home.jsとする。

pages/home.jsx
import React, { useState, useEffect } from "react";

import { auth } from "./firebase";

import router from "next/router";

export default function Home() {
  const [user, setUser] = useState(null);
  useEffect(
    () =>
      auth.onAuthStateChanged((user) => {
        if (user) {
          console.log(user);
          setUser(user);
        } else {
          router.push("/");
        }
      }),
    []
  );

  const handleLogout = () => {
    auth.signOut();
    router.push("/");
  };
  return (
    <div>
      <h1>ログイン後画面</h1>
      {user && (
        <>
          <div>{user.email}</div>
          <div>Email Verified: {user.emailVerified ? "yes" : "no"}</div>
        </>
      )}

      <button onClick={handleLogout}>ログアウト</button>
    </div>
  );
}

ここまでのコードでは以下のように動く。

a.gif

Firebase Authenticationから登録されていることがわかる。

スクリーンショット 2021-09-14 11.19.36.png

Expressサーバーで認証リンクを送信

ここからの要件としては

  • クライアントサイドからメールアドレスを受け取る
  • メールアドレスから認証リンクを生成(Firebase Admin SDK)
  • 認証リンクを含むメールを送信(nodemailer)

ExpressサーバーにEmailアドレスをPOSTで渡す

Express側で認証用のメールを送信するのでPOSTでメールアドレスを送る。

クライアントサイドの、ユーザー登録に関わるコードを以下のように変更する。

pages/index.jsx
  const handleSubmit = (event) => {
    event.preventDefault();

    auth
      .createUserWithEmailAndPassword(email, password)
      .then(() => {
        fetch("http://127.0.0.1:3000/email", {
          headers: {
            "Content-Type": "application/json; charset=utf-8",
          },
          method: "POST",
          body: JSON.stringify({ email: email }),
        })
          .then((res) => {
            return res.json();
          })
          .then((data) => {
            console.log(data);
          });
      })
      .then(() => {
        router.push("/home");
      })
      .catch((error) => {
        const errorCode = error.code;
        const errorMessage = error.message;
        alert(`error code${errorCode} ${errorMessage}`);
      });
  };

バックエンドのコードで、POSTメソッドのrequest bodyが拾えるかどうかデバッグする。

server/index.js
  server.post("/email", (req, res) => {
    console.log("req");
    console.log(req.body);
    console.log("email endpoint called");
    res.json({
      message: "Hello,world",
    });
  });

すると以下のように出力されていて取得できていないことがわかる。

event - build page: /
wait  - compiling...
event - compiled successfully
req
undefined

これはExpressが元々そういう挙動のためである。

req.body
Contains key-value pairs of data submitted in the request body. By default, it is undefined, and is populated when you use body-parsing middleware such as express.json() or express.urlencoded().

application/jsonapplication/x-www-form-urlencodedをパースするミドルウェアを追加する必要がある。

server/index.js
app.prepare().then(() => {
  const server = express();

  // 追加
  server.use(express.json()); // for parsing application/json
  server.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

  server.use(cors());

  server.post("/email", (req, res) => {
    console.log("req");
    console.log(req.body);
    console.log("email endpoint called");
    res.json({
      message: "Hello,world",
    });
  });
  // ..略
});

結果として、以下のように出力されるようになった。

req
{ email: '<メールアドレス>' }
email endpoint called

認証リンクを発行してメールを送る

まず、認証リンクを生成したいのでこの辺のことをおこなっていく。

Firebaseの画面から認証キーのjsonファイルをダウンロードしてプロジェクトのルートに置いておく。このファイルは特に編集しない。

server/index.jsで認証情報を読み込ませる必要がある。

以下の記事を参考にする。環境変数のGOOGLE_APPLICATION_CREDENTIALSに登録してもいいが、PCに環境変数を登録したくなかったので工夫する。

Expressからメールを送信したい。nodemailerを導入する。これはNode.jsからメール送信を可能にするモジュールで以下の記事を参考にした。

yarn add nodemailer

メールサーバー(STMPサーバー)が必要なので、gmailを利用する。googleアカウントを用意してアプリパスワードを発行する。これを使うと、そのgmailアカウントが送信元となる。

サーバー側のコードは以下。

server/index.js
const express = require("express");
const next = require("next");

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const port = process.env.PORT || 3000;

const cors = require("cors");

const admin = require("firebase-admin");
const nodemailer = require("nodemailer");
const serviceAccount = require("../<認証キー>.json");

const cert = {
  projectId: serviceAccount.project_id,
  clientEmail: serviceAccount.client_email,
  privateKey: serviceAccount.private_key.replace(/\\n/g, "\n"),
};

admin.initializeApp({
  credential: admin.credential.cert(cert),
  databaseURL: "https://<firebaseのプロジェクトID>.firebaseio.com",
});

const transporter = nodemailer.createTransport({
  service: "gmail",
  port: 465,
  secure: true, // SSL
  auth: {
    user: "<gmailアドレス>", // メールアドレス
    pass: "<アプリパスワード>", // アプリパスワード
  },
});

app.prepare().then(() => {
  const server = express();
  server.use(express.json()); // for parsing application/json
  server.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

  server.use(cors());

  server.post("/email", (req, res) => {
    console.log("req");
    console.log(req.body);
    const { email } = req.body;
    try {
      admin
        .auth()
        .generateEmailVerificationLink(email)
        .then(async (emailLink) => {
          const uid = await (await admin.auth().getUserByEmail(email)).uid;
          console.log("uid");
          console.log(uid);
          console.log("emailLink");
          console.log(emailLink);
          await await transporter.sendMail({
            from: "test",
            to: email,
            subject: "This is the title.",
            html: `Hello, please click here.<br><a href="${emailLink}">${emailLink}</a>`,
          });

          return res.status(200).json({ status: "ok", message: "Email sent." });
        })
        .catch((error) => {
          return res.json(error);
        });
    } catch (error) {
      return res.json(error);
    }
  });

  server.all("*", (req, res) => {
    return handle(req, res);
  });
  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on localhost:${port}`);
  });
});

説明していくと、以下のコードで

  • nodemailerでSTMPの設定
  • firebase admin sdkの認証

をしている。

const nodemailer = require("nodemailer");
const serviceAccount = require("../<認証キー>.json");

const cert = {
  projectId: serviceAccount.project_id,
  clientEmail: serviceAccount.client_email,
  privateKey: serviceAccount.private_key.replace(/\\n/g, "\n"),
};

admin.initializeApp({
  credential: admin.credential.cert(cert),
  databaseURL: "https://<firebaseのプロジェクトID>.firebaseio.com",
});

const transporter = nodemailer.createTransport({
  service: "gmail",
  port: 465,
  secure: true, // SSL
  auth: {
    user: "<gmailアドレス>", // メールアドレス
    pass: "<アプリパスワード>", // アプリパスワード
  },
});

Emailでの認証リンクは、firebase.auth().generateEmailVerificationLink()で発行できる。

動作の確認

ユーザーアカウント作成すると、home画面に遷移する。

スクリーンショット 2021-09-14 12.20.03.png

この間に、バックエンドのコンソールで以下のように出力される。

req
{ email: '<メールアドレス>' }
uid
IUPFZcyiRya5y3DIdqSs6A1CJAx1
emailLink
https://<firebase-project-id>.com/__/auth/action?mode=verifyEmail&oobCode=NI4UTUhfi9ZeHfvPg7t2dj04bC9x65TNj2HnR1Xsi1sAAAF74kqSzQ&apiKey=AIzaSyA9J-hMBpQkXViG6i2T52kIKLWX6w9cQpQ&lang=ja

登録時のメールアドレスに以下のようにメールが届いている。
スクリーンショット 2021-09-14 12.19.37.png

リンクをクリックすると、画面遷移してメールアドレスが認証される。

スクリーンショット 2021-09-14 12.14.26.png

home画面に遷移してページをリロードしてみると、userオブジェクトのemailVerifiedプロパティの値がtrueになっていることがわかる。

スクリーンショット 2021-09-14 12.20.25.png

参考

メールでのアクティベーションについて

firebaseまわり

mailer

fetch

メール認証
https://qiita.com/YamadaTakahito/items/182260a3cf240327d765
https://colo-ri.jp/develop/2012/07/web-how-to-mail-activation.html

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?