Firebaseの技術情報サイト

Firebaseの技術情報サイト

Firebase活用の為の技術情報サイトです

Firebaseを使ってReactのログイン機能を作る方法!

Firebaseを使ってReactのログイン機能を作る方法!

先日、Firebaseのログイン機能の使い方について記事を書きました。今日は実践編として、Reactを利用して実際にログイン機能として実装する例を紹介します。

ログインの機能に集中

今回はログイン(ユーザー認証)の機能に集中して解説します。

今回実装するログイン機能を作る上で想定している事は以下の通りです。

  • 利用者が自分でアカウントを作成可能(サインアップ機能)
  • パスワードを忘れた場合はパスワードリセットが可能
  • サービスの利用にはE-Mailアドレスの確認が必要
  • E-Mailアドレスの再確認のメールの再送も可能

という前提でログイン機能を実装する事にしました。

作るのは「ログインページ」です。

まずは、画面の遷移を考える

Webアプリ(Webサービス)にアクセスした時には、サインイン(ログイン画面)に行くようにします。つまり、ログイン画面がこのログイン機能の入り口になります。

この画面に来た時の利用者の操作の可能性は

  • 既にアカウントを持っている人は普通にサインイン(ログイン)
  • アカウントを持っていない人はサインアップ(新規登録)
  • アカウントを持っているけれども、パスワードを忘れてしまった人(パスワードのリセット)
  • E-Mailの確認をしていないが、確認のE-Mailが見当たらない人

この4つの可能性があります。

最後のEmail確認メールの再送は頻度は少ないので最初のログイン(サインイン)の画面からは外す事にします。

サインイン画面(ログイン画面)

以下のようなUIが考えられます。

signin

これで、

  • Email/パスワードを入力すれば普通にログイン
  • 「Sign Up」をクリックすれば新規登録
  • 「Forget Password」をクリックすればパスワード再設定の画面へ

この画面から外した、E-Mail確認の再送は「Sign Up」の画面から行う事にします。

サインインする場合は、

  • E-Mailアドレス
  • パスワード

入力する必要があります。

サインアップ(新規登録画面)

新規登録画面からは、E-Mail確認の再送画面に行けるようにしておきます。 キャンセルされた場合と、サインアップが実行された場合はサインイン(ログイン)画面に戻るようにしておきます。

新規登録には

  • E-Mailアドレス
  • パスワード

が必要になりますが、パスワードは確認の為、2回入力してもらう事にしました。

signup

パスワードリセット画面

パスワードをリセットする場合は、登録したE-Mailアドレスのみが必要です。 キャンセルもしくは実行すると、サインイン(ログイン)画面に戻ります。

password_reset

E-Mail確認再送画面

E-Mail確認のメッセージを送るにはサインインする事が必要なので、

  • E-Mailアドレス
  • パスワード

を入力してもらいます。キャンセル及び実行時は、サインイン(ログイン)画面に戻るようにします。

verify_email

画面の遷移は?

そうするとこのログイン(ユーザー認証)機能の実現には4つの画面が必要になって、以下の感じで画面が切り替わります。

State Transition

これが、ログイン画面実装の概要です。

Reactでの実装は?

いろいろやり方はありますが、今回紹介する方法は「ログインページ」を作ってログインの画面の制御はこのページで行うようにします。

ログインページからは:

  • ログインフォーム
  • サインアップフォーム
  • パスワードリセットフォーム
  • パスワード確認フォーム

の4つの部品(Component)を作って呼び出すようにするとデザインがしやすくなります。

フォームの中身はほぼ同じ!

今回の4つのフォームは基本的にE-Mailアドレスとパスワードの入力とボタンの処理になります。(パスワードリセットのフォームのみE-Mailアドレスのみ)

なので、記事では、「ログインフォーム」の例だけ紹介します。

HTMLはCSSを簡略化するためにBootstrapを使っています。

入力された値の取り込みは、Reactのリファレンスオブジェクトを作って、HTMLのDOMに割りてて値を取得する方法を使っています。このリファレンスオブジェクトは親のコンポーネント(この場合、ログインページ)で作成して、子のコンポーネントに渡しているので、親のコンポーネントで値を取得できるメリットがあります。

イベントも、親のコンポーネントで処理しているのでフォームの実装はシンプルです。

このフォームはこの実装に依存した処理は行っていないので部品として再利用が簡単にできます

このサンプルコードはTypescriptで記述しているので型も分かり易くなっています。

import React from "react";
import "./styles/loginForm.css";
interface IProps {
  email: React.RefObject<HTMLInputElement>;
  message: string;
  password: React.RefObject<HTMLInputElement>;
  resetPassword:
    | ((event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void)
    | undefined;
  signIn:
    | ((event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void)
    | undefined;
  signUp:
    | ((event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void)
  | undefined;
  title:string
}
interface IState {}
export default class LoginForm extends React.Component<IProps, IState> {
  render() {
    return (
      <React.Fragment>
        <h1 className="title">{this.props.title}</h1>
        <p>{this.props.message}</p>
        <div className="login_form">
          <div className="form-group">
            <label>E-Mail</label>
            <input
              type="email"
              className="form-control"
              ref={this.props.email}
              placeholder="Enter E-Mail Address"
            />
          </div>
          <div className="form-group">
            <label>Password</label>
            <input
              type="password"
              className="form-control"
              ref={this.props.password}
              placeholder="Enter password"
            />
          </div>
          <button className="btn btn-primary" onClick={this.props.signIn}>
            Sign In
          </button>
          <button className="btn btn-link" onClick={this.props.signUp}>
            Sign Up
          </button>
          <button className="btn btn-link" onClick={this.props.resetPassword}>
            Forget Password
          </button>
        </div>
      </React.Fragment>
    );
  }
}

ログインページで画面表示の遷移をコントロール

画面の表示の切り替えはログインページで一括して行っています。

ログインページの実装例です。

全てのフォームのリファレンスオブジェクトをこのモジュールで作って、子のコンポーネントに渡しているので全ての値をこのモジュールで簡単に取得できます。

イベントも一括してこのモジュールで処理しているので、REDUXを導入しなくても、この部分の処理は十分にできます。ただ、大きなアプリの実装などの場合は、ログインの情報はREDUXなどで一括管理した方が実装はシンプルになります。 今回はログイン機能の実装だけなので、特にREDUXは利用していません。

import React from "react";
import { MyFirebase } from "../lib/firebase";
import LoginForm from "../components/loginForm";
import ResendVerificationMailForm from "../components/resendVerificationMail";
import ResetPasswordForm from "../components/resetPasswordForm";
import SignUpForm from "../components/signupForm";

interface IProps {}
interface IState {
  mode: string;
}
/** Debug mode */
const DEBUG = process.env.REACT_APP_DEBUG === "true" ? true : false;
//
//  Titles of forms
//
/** Title: Sign In form */
const TITLE_SIGN_IN = "サインイン";
/** Title: Sing Up form */
const TITLE_SIGN_UP = "新規登録";
/** Title: Password Resetting form */
const TITLE_RESET_PASSWORD = "パスワードリセット";
/** Title: Resending E-Mail verification form */
const TITLE_RESEND_MAIL = "E-Mail確認メールの再送";
//
//  Message of forms
//
/** Message: Sign In form */
const MSG_SIGN_IN = "E-Mailアドレスとパスワードを入力してください";
/** Message: Sing Up form */
const MSG_SIGN_UP =
  "新規登録します。E-Mailとパスワードを入力してください。登録とE-Mailの確認後ご利用可能になります。";
/** Message: Password resetting form */
const MSG_RESET_PASSWORD =
  "パスワードをリセットする為のE-Mailを送ります。E-Mailアドレスを入力してください。";
/** Message Resending E-Mail verification form */
const MSG_RESEND_MAIL =
  "E-Mailアドレスの確認のメールを再送します。登録のE-Mailとパスワードを入力してください。";
//
// Display mode for this page
//
/** Login form */
const LOGIN = "login";
/** Sign Up form */
const SIGNUP = "signup";
/** Resetting password form */
const RESET_PASSWORD = "reset_password";
/** Resending E-Mail verification form */
const RESEND_VERIFY_MAIL = "resend_verify_mail";
/** Class for Login Page */
export default class LoginPage extends React.Component<IProps, IState> {
  /** React Input reference for E-mail field in resending E-Mail verification form  */
  private resendEmail: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** React Input reference for Password field in resending E-Mail verification form  */
  private resendPassword: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** React Input reference for E-mail field in resetting password form  */
  private resetEmail: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** React Input reference for Password field in resetting password form  */
  private signInEmail: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** React Input reference for E-Mail field in Sign In form  */
  private signInPassword: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** React Input reference for Password field in Sign In form  */
  private signUpEmail: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** React Input reference for E-Mail field in Sign Up form  */
  private signUpPassword: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** React Input reference for Password field in Sign Up form  */
  private signUpPassword2: React.RefObject<HTMLInputElement> = React.createRef<
    HTMLInputElement
  >();
  /** Constructor to initialize state value */
  constructor(props: any) {
    super(props);
    this.state = {
      mode: LOGIN,
    };
  }
  /** Cancel event handler (back to Login form) */
  cancel(): void {
    this.setState({
      mode: LOGIN,
    });
  }
  /** Event handler for resend E-Mail verification button */
  pressResend(): void {
    this.setState({
      mode: RESEND_VERIFY_MAIL,
    });
  }
  /** Event handler for resetting password button */
  pressResetPassword(): void {
    this.setState({
      mode: RESET_PASSWORD,
    });
  }
  /** Event handler for switching to sing up form button */
  pressSignUp(): void {
    this.setState({
      mode: SIGNUP,
    });
  }
  /** Resinding E-Mail verification message */
  resendEvent(): void {
    if (this.resendEmail.current && this.resendPassword.current) {
      const email = this.resendEmail.current.value;
      const password = this.resendPassword.current.value;
      if (DEBUG) {
        console.log(email, password);
      }
      MyFirebase.resendMail(email, password);
    }
  }
  /** Sending Password reset message */
  resetEvent(): void {
    if (this.resetEmail.current) {
      const email = this.resetEmail.current.value;

      if (DEBUG) {
        console.log(email);
      }
      MyFirebase.resetPassword(email);
    }
  }
  /** Event handler for Sign-in */
  signInEvent(): void {
    if (this.signInEmail.current && this.signInPassword.current) {
      const email = this.signInEmail.current.value;
      const password = this.signInPassword.current.value;
      if (DEBUG) {
        console.log(email, password);
      }
      MyFirebase.signin(email, password);
    }
  }
  /** Event handler for Sign-up */
  signUpEvent(): void {
    if (
      this.signUpEmail.current &&
      this.signUpPassword.current &&
      this.signUpPassword2.current
    ) {
      const email = this.signUpEmail.current.value;
      const password = this.signUpPassword.current.value;
      const password2 = this.signUpPassword2.current.value;
      if (DEBUG) {
        console.log(email, password, password2);
      }
      if (password !== password2) {
        alert("Passwords do not match! Please re-enter passwords!");
      } else {
        MyFirebase.signup(email, password);
      }
    }
  }
  /** Rendering method for this component */
  render(): JSX.Element {
    let content: JSX.Element;
    switch (this.state.mode) {
      case LOGIN: // Login form
      default:
        content = (
          <LoginForm
            title={TITLE_SIGN_IN}
            message={MSG_SIGN_IN}
            resetPassword={() => this.pressResetPassword()}
            signIn={() => this.signInEvent()}
            signUp={() => this.pressSignUp()}
            email={this.signInEmail}
            password={this.signInPassword}
          />
        );
        break;
      case SIGNUP: // Sign-up form
        content = (
          <SignUpForm
            title={TITLE_SIGN_UP}
            message={MSG_SIGN_UP}
            cancel={() => this.cancel()}
            resend={() => this.pressResend()}
            signUp={() => this.signUpEvent()}
            email={this.signUpEmail}
            password={this.signUpPassword}
            password2={this.signUpPassword2}
          />
        );
        break;
      case RESET_PASSWORD: // Reseting password form
        content = (
          <ResetPasswordForm
            title={TITLE_RESET_PASSWORD}
            message={MSG_RESET_PASSWORD}
            cancel={() => this.cancel()}
            reset={() => this.resetEvent()}
            email={this.resetEmail}
          />
        );
        break;
      case RESEND_VERIFY_MAIL: // Resending E-Mail verification form
        content = (
          <ResendVerificationMailForm
            title={TITLE_RESEND_MAIL}
            message={MSG_RESEND_MAIL}
            cancel={() => this.cancel()}
            resend={() => this.resendEvent()}
            email={this.resendEmail}
            password={this.resendPassword}
          />
        );
        break;
    }
    return (
      <React.Fragment>
        <div className="login_page">{content}</div>
      </React.Fragment>
    );
  }
}

Firebaseの処理は?

先日の記事で紹介したような、Firebaseのログイン(ユーザー認証)に関する記述は、別のモジュールにまとめてあります。このモジュールは別の実装でも簡単に使いまわせるため、実際のUIと分けた方が便利です。

ここの部分は前回の解説で書いているのでそちらを参照してください。

Firebaseのプロジェクト情報は、「.env」にまとめてあるので、このファイルはプロジェクトとの依存関係は殆どありません。他のプロジェクトで使いまわししやすいように工夫しています。正しく設定されていない場合はブランクになるようにしているので、エラーになります。

プロジェクト依存の部分は、限られたファイルにまとめる事で流用が簡単になります。

またメッセージなど後で変更する可能性が高い部分は、コードに直接は書かずに、定数の定義ファイルやコードの先頭にまとめて後で変更しやすいようにしています。

import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import "firebase/storage";
import "firebase/analytics";
import * as CONSTANT from "./constants";
const DEBUG: boolean = process.env.REACT_APP_DEBUG === "true" ? true : false;
const MSG_EMAIL_VERIFIATION_SENT: string =
  "E-Mail確認の為のメッセージを登録されたE-Mailアドレスに発送しました。ご確認ください!";
const MSG_PASSWORD_RESET_SENT: string =
  "パスワードリセットの為のメールを登録されたE-Mailアドレスに発送しました。ご確認ください!";
// Your web app's Firebase configuration
// Configuration value is defined in ".env" file in the project folder
const firebaseConfig = {
  apiKey: process.env.REACT_APP_apiKey ? process.env.REACT_APP_apiKey : "",
  authDomain: process.env.REACT_APP_authDomain
    ? process.env.REACT_APP_authDomain
    : "",
  databaseURL: process.env.REACT_APP_databaseURL
    ? process.env.REACT_APP_databaseURL
    : "",
  projectId: process.env.REACT_APP_projectId
    ? process.env.REACT_APP_projectId
    : "",
  storageBucket: process.env.REACT_APP_storageBucket
    ? process.env.REACT_APP_storageBucket
    : "",
  messagingSenderId: process.env.REACT_APP_messagingSenderId
    ? process.env.REACT_APP_messagingSenderId
    : "",
  appId: process.env.REACT_APP_appId ? process.env.REACT_APP_appId : "",
  measurementId: process.env.REACT_APP_measurementId
    ? process.env.REACT_APP_measurementId
    : "",
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();
export default firebase;
//
//  Firebase Utility methods
//
export class MyFirebase {
  // ==========================================================================
  //  Methods for Firebase Authentication
  // ==========================================================================
  /** Gets the current user E-mail address
   * @return Returns email address of the current user (string)
   */
  static getEmail(): string {
    const user = firebase.auth().currentUser;
    if (user && user.email) {
      return user.email;
    } else {
      return CONSTANT.GUEST_USER;
    }
  }
  /** Checks if the current user is an administrator
   * @return Returns true if the current user is an administrator, otherwise returns false (boolean)
   */
  static isAdmin(): boolean {
    const user = firebase.auth().currentUser;
    if (user && user.uid === process.env.REACT_APP_ADMIN_UID) {
      return true;
    } else {
      return false;
    }
  }
  /** Initialize Firebase authentication
   * @param option Authentication state persistence option (string)
   *  DOMAIN based persistence: firebase.auth.Auth.Persistence.LOCAL
   *  SESSION based persistence: firebase.auth.Auth.Persistence.SESSION
   *  No persistence: firebase.auth.Auth.Persistence.NONE
   */
  static intAuth(option: string): void {
    firebase
      .auth()
      .setPersistence(option)
      .then(() => {})
      .catch((error: any) => {
        // Handle Errors here.
        const errorCode = error.code;
        const errorMessage = error.message;
        if (DEBUG) {
          console.log(errorCode, errorMessage);
        }
      });
  }
  /** Resends E-Mail verification message
   * @param email E-mail address for this Firebase project account (string)
   * @param password Password of this Firebase project account (string)
   */
  static resendMail(email: string, password: string): void {
    firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then((user: firebase.auth.UserCredential) => {
        const currentUser: firebase.User | null = firebase.auth().currentUser;
        if (user && currentUser) {
          currentUser
            .sendEmailVerification()
            .then(() => {
              // Successful to an send email verification message
              firebase.auth().signOut();
              alert(MSG_EMAIL_VERIFIATION_SENT);
            })
            .catch((error) => {
              // Failed to send an email verification message
              firebase.auth().signOut();
            });
        } else {
          // This supposed not to happen
          if (DEBUG) {
            console.log("firebase: singup():");
            console.log(user);
            console.log(currentUser);
          }
        }
      })
      .catch((error: any) => {
        alert(error.message);
      });
  }
  /** Sends an E-Mail to proceed resetting password
   * @param email E-mail address for this Firebase project account (string)
   */
  static resetPassword(email: string): void {
    firebase
      .auth()
      .sendPasswordResetEmail(email)
      .then(() => {
        // Successful to send a message for restting the password
        alert(MSG_PASSWORD_RESET_SENT);
      })
      .catch((error: any) => {
        // Failed to send a message for resetting the password
        alert(error.message);
      });
  }
  /** Sign-In with E-Mail address and Password (Asynchronous)
   * @param email E-mail address for this Firebase project account (string)
   * @param password Password of this Firebase project account (string)
   * @return Returns true if sign-in is successful, otherwise returns false (boolean)
   */
  static signin(email: string, password: string): Promise<boolean> {
    return new Promise((resolve) => {
      firebase
        .auth()
        .signInWithEmailAndPassword(email, password)
        .then((user: firebase.auth.UserCredential) => {
          const currentUser: firebase.User | null = firebase.auth().currentUser;
          if (!currentUser || !currentUser.emailVerified) {
            firebase
              .auth()
              .signOut()
              .then(() => {
                resolve(false);
              })
              .catch((error: any) => {
                resolve(false);
              });
          } else {
            resolve(true);
          }
        })
        .catch((error: any) => {
          alert(error.message);
          resolve(false);
        });
    });
  }
  /** Sign-up with E-Mail address and password
   * @param email E-mail address for this Firebase project account (string)
   * @param password Password of this Firebase project account (string)
   */
  static signup(email: string, password: string): void {
    firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then((user: firebase.auth.UserCredential) => {
        const currentUser: firebase.User | null = firebase.auth().currentUser;
        if (user && currentUser) {
          currentUser
            .sendEmailVerification()
            .then(() => {
              // Successful to send an email verification message
              firebase.auth().signOut();
              alert(MSG_EMAIL_VERIFIATION_SENT);
            })
            .catch((error) => {
              // Failed to send an email verification message
              firebase.auth().signOut();
            });
        } else {
          // This supposed not to happen
          if (DEBUG) {
            console.log("firebase: singup():");
            console.log(user);
            console.log(currentUser);
          }
        }
      })
      .catch((error: any) => {
        alert(error.message);
      });
  }
}

ログインした後はどうなる?

さて、疑問に残るのがログインに成功したらどうするのかという事です。 これは、Firebaseのログインのステータス変更のイベント検出の機能を使って実装します。

Reactのアプリのトップレベル(App.tsx)の初期化でイベントリスナーを起動しておくと、ステータスが変わると自動的に設定したページに飛ぶようにしています。この部分はログインの部分とは独立に作る事が出来るので便利です。

init(): void {
    MyFirebase.intAuth(firebase.auth.Auth.Persistence.SESSION);
    firebase.auth().onAuthStateChanged((user:firebase.User|null) => {
      if (user) {
        this.setState({
          login: true,
        });
        <Link to="/"></Link>;
      } else {
        // No user is signed in.
        this.setState({
          login: false,
        });
        <Link to="/login"></Link>;
      }
    });
  }

まとめ

Reactでのログイン機能の実装例を紹介しました。 実際にWebアプリやWebサービスでログイン機能を実装する場合、思ったよりも複雑な処理になる場合が多くなります。シンプルなログイン機能は簡単に実装できますが、実際のサービスに合わせた形で実装するには少し慣れが必要です。

ただし、必要な機能はFirebaseがほぼサポートしているので、Firebaseを利用する場合は、開発者は画面の遷移に集中する事ができるので開発効率は上がります。

ログインの処理だけではなく、フロントエンドの開発をする場合、画面の遷移を予め図にして考えると、全体を見通しやすい設計ができます。 思い付きで作ると、後からメインテナンスが大変なコードになりやすいので、最初に全体の画面の流れをよく理解したうえで設計する事が大切です。