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用のメインファイルとする。
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メソッドを追加する。
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.post("/email", (req, res) => {
console.log("email endpoint called");
res.json({
message: "Hello,world",
});
});
src/index.js
のjsxに以下のコードを追加する。
<button onClick={() => post()}>ポストする</button>
post関数は以下
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);
});
}
ボタンをクリックしてみるとエラーが出る。
オリジン間リソース共有(CORS: Cross-Origin Resource Sharing)の制限によりエラーが発生していることがわかる。そこでcors
を導入する。
yarn add cors
cors
を利用するように変更
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がクライアントサイドで取得できていることがわかる。
ここまでで、Next.jsのクライアントサイドとバックエンドがNext.jsのAPIサーバーの機能を通じで疎通するところまで確認できた。
Firebase関連のコードを書いていく
ここから何をやっていくかざっくりと考えると
- クライアントサイドで、ログイン前/ログイン後画面を作る
- firebaseで認証する
- バックエンドでログイン情報を受け取って、メールを取得。それを使って認証リンクを送信する
クライアントサイド
まずクライアントサイドから取り組む。Reactでのfirebaseの導入は以下がわかりやすい。
firebase
はクライアントサイド、firebase-admin
はバックエンドで利用する。
yarn add firebase firebase-admin
クライアントサイドの認証情報
ルートにfirebase.js
を作成する。またFirebaseの設定情報は.env
に保存する。
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
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
とする。
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>
);
}
ここまでのコードでは以下のように動く。
Firebase Authenticationから登録されていることがわかる。
Expressサーバーで認証リンクを送信
ここからの要件としては
- クライアントサイドからメールアドレスを受け取る
- メールアドレスから認証リンクを生成(Firebase Admin SDK)
- 認証リンクを含むメールを送信(nodemailer)
ExpressサーバーにEmailアドレスをPOSTで渡す
Express側で認証用のメールを送信するのでPOSTでメールアドレスを送る。
クライアントサイドの、ユーザー登録に関わるコードを以下のように変更する。
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.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/json
とapplication/x-www-form-urlencoded
をパースするミドルウェアを追加する必要がある。
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アカウントが送信元となる。
サーバー側のコードは以下。
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
画面に遷移する。
この間に、バックエンドのコンソールで以下のように出力される。
req
{ email: '<メールアドレス>' }
uid
IUPFZcyiRya5y3DIdqSs6A1CJAx1
emailLink
https://<firebase-project-id>.com/__/auth/action?mode=verifyEmail&oobCode=NI4UTUhfi9ZeHfvPg7t2dj04bC9x65TNj2HnR1Xsi1sAAAF74kqSzQ&apiKey=AIzaSyA9J-hMBpQkXViG6i2T52kIKLWX6w9cQpQ&lang=ja
リンクをクリックすると、画面遷移してメールアドレスが認証される。
home
画面に遷移してページをリロードしてみると、user
オブジェクトのemailVerified
プロパティの値がtrue
になっていることがわかる。
参考
メールでのアクティベーションについて
firebaseまわり
- https://blog.katsubemakito.net/firebase/firebase-authentication-email-web2
- https://medium.com/firebase-developers/generating-email-action-links-with-the-firebase-admin-sdk-4b9d5e2cf914
- https://firebase.google.com/docs/auth/admin/email-action-links
- https://firebase.google.com/docs/auth/custom-email-handler
- Node.js and Firebase: Generating Email Verification Link
mailer
- https://nodejs.keicode.com/nodejs/nodemailer.php
- https://dev.to/eswari/how-to-send-email-with-attachments-in-node-js-using-nodemailer-3g4i
fetch
- https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
- https://blog.katsubemakito.net/html5/fetch1
メール認証
https://qiita.com/YamadaTakahito/items/182260a3cf240327d765
https://colo-ri.jp/develop/2012/07/web-how-to-mail-activation.html