React + Firebaseで認証機能を実装してみよう

[Ateam Lifestyle Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ateam-lifestyle)の2日目は
株式会社エイチームライフスタイル 名古屋開発部のタツミが担当します。
フロントエンドの開発をメインでやっています。
駆け出しのエンジニアですが、よろしくお願いします。

# はじめに
チームでWebサービスを開発するときにフロントエンドとバックエンドの開発担当に分かれるのはよくある光景ですが、開発のスピードやチームのリソースが求められた時にバックエンドまで開発するのは大変です。

バックエンドのサービス開発に時間を取られずに、フロントエンドの開発に専念したい。
その解決策の１つとして、Googleが提供している**Firebase**というツールの機能の１つを使ってバックエンドの開発をせずに認証機能を作っていきたいと思います。

FirebaseはiOS/AndroidアプリからWebサービスまでバックエンドを必要とするサービスで活用することができます。

# 認証機能の概要
Firebaseではデータベース、ストレージサービス、ホスティングサービス、分析ツール、プッシュ通知などWeb/アプリ開発で活用できる様々なサービスを提供しています。
今回はFirebaseの認証機能の１つである**Firebase Authentication**を使って、**React**で**認証機能**を実装していきたいと思います。

# 説明の進め方
本記事は題材ごとに全部で以下、４パートに分けて説明します。
Reactの基礎は理解している程で話を進めます。

- Part1: フォームを作成しよう
- Part2: Firebaseの認証機能を有効にしよう
- Part3: ログイン機能を実装しよう
- Part4: ログアウト機能を実装しよう

# Part1: フォームを作成しよう
https://firebase.google.com/
↑にアクセスして、Firebaseのアカウントを作成してください。

[create-react-app](https://ja.reactjs.org/docs/create-a-new-react-app.html)で新しいReactプロジェクトを作成してください。

認証用のコンポーネントを作成します。
新たにsrc/components/Auth/index.jsを作成してください。
簡単にメールアドレスとパスワードの基本的な[フォーム](https://ja.reactjs.org/docs/forms.html)を作成します。

※ このファイルに認証機能を全て実装します。実装は例ですので適宜、適切なファイルに実装してください。

```react:src/components/Auth/index.js
import React, { Component } from 'react'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  handleChange(event) {
    // テキストボックスの値を変更する度に、変数に値が格納される
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    // 後半のパートでemailとpasswordをfirebaseに登録する処理を追加
    event.preventDefault()
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
            <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <button>SUBMIT</button>
        </form>
      </div>
    )
  }
}

export default Auth
```

メールアドレスとパスワードのonChangeイベントに設定したhandleChangeアクションでテキストボックスの値が変わる度にstateに更新された値が格納されます。

後半のパートのなかで、handleSubmitアクションで格納されたstateをFirebase Authenticationに登録していく処理を追加する方法を説明します。

作成した認証コンポーネントを描画するために、App.jsを変更します。

```react:src/App.js
import React from 'react'
import './App.css'
import Auth from './components/Auth'

function App() {
  return (
    <div className="App">
      <Auth />
    </div>
  )
}

export default App
```

# Part2: Firebaseの認証機能を有効にしよう
ここからログイン機能を実装していきます。

Part1で作ったFirebaseのアカウントでログインして、コンソールへ移動を選択します。
![firebase_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/375057a3-7c78-1cec-b7f9-03e802a66fd0.png)

新しいプロジェクトを作成する必要があるので、プロジェクトを追加を選択してください。

![firebase_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/141bf823-f9cb-83de-12e9-b7216290b395.png)

プロジェクト名は何でも良いのですが、今回は認証テストとしてauth-testと命名して続行を選択します。

![firebase_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/50134369-acbc-e4bd-b937-62dbdc802c15.png)

google analyticsと連携させたい場合は「このプロジェクトでGoogle アナリティクスを有効にする」にチェックを入れますが、今回は連携させないのでチェックを外します。プロジェクトを作成を選択してください。

![firebase_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/5e463b51-1e31-ec95-b74a-b675eea6b0c7.png)

無事にauth-testプロジェクトが作成されました。
左のメニューからAuthenticationを選択して、ログイン方法を設定を選択してください。

![firebase_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/e824d83c-db93-a63a-14e9-7b757d33e11c.png)

様々な認証方法を選ぶことができますが、今回はメールアドレスとパスワードで認証機能を作っていきたいのでメール/パスワードの右の鉛筆マークを選択してください。

![firebase_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/25c96e10-69c5-adbf-bd23-5d8755f0ac90.png)

メールアドレスとパスワードの認証を有効にします。
今回はパスワードの設定を必須にしたいので、パスワードなしでログインは無効にしておきます。
保存を選択します。

![firebase_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/7a3f964a-a175-498d-a30c-12bc8770f5c6.png)

以上で、メール/パスワードの設定が有効になりました。

![firebase_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/cbd9614e-5eea-6dd1-3419-7178f1eaf979.png)

# Part3: ログイン機能を実装しよう
Firebase AuthenticationのAPIを使って、フロントエンドにログイン機能を実装します。
認証が完了するとトークンが発行されますが、トークンをstateに保存するとリロードした時にトークン情報が消えてしまうので、[localStorage](https://www.granfairs.com/blog/staff/local-storage-01)に保存する方法を実装していきます。
Firebase APIと通信する必要があるので、[axios](https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9)を使っていきます。

SignUpとSignIn機能を実装するボタン\<button\>を設置します。
Part2で作ったフォームの続きから実装していきます。
ボタンをクリックする度に、switchAuthModeHandlerが発火してisSignUpフラグのon, offの切替を行います。

```react:src/components/Auth/index.js
import React, { Component } from 'react'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.state = {
　　　// signUpとsignInフラグ
      isSignUp: true
    }
  }

  handleChange(event) {
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    event.preventDefault()
  }

　// クリックの毎にsignUpとsignInの切替を行う
　switchAuthModeHandler = () => {
    this.setState({ isSignUp: !this.state.isSignUp })
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
            <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <button>SUBMIT</button>
        </form>
　　　　{/* signUp、signInボタン */}
        <button onClick={this.switchAuthModeHandler}>
          SWITCH TO {this.state.isSignUp ? 'SignIn' : 'SignUp'}
        </button>
      </div>
    )
  }
}

export default Auth
```

続いて、認証メソッドを作成します。
Firebaseの認証機能APIのエンドポイントを紹介します。
APIを使うにはAPIキーを入力する必要があるので、キーを先に確認します。

![firebase_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/e3a9bf3a-c44d-2fdd-0246-044ed2f24306.png)

![firebase_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/6288ddf2-9263-2138-a7eb-d71a1fe39e6e.png)

SignUp用のエンドポイントとSignIn用のエンドポイントを確認します。
https://firebase.google.com/docs/reference/rest/auth#section-create-email-password
↑ URLにアクセスします

**SignUp用エンドポイント**
![API_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/87f36ac4-9c7b-97a4-dd97-c4f3612c4878.png)

**SignInのエンドポイント**
![API_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/76141c06-f43a-7148-8e1a-4d4ad1d95bd2.png)

各エンドポイントの?key=[API_KEY]の部分に先ほど確認したAPIキーを入力して使います。

認証メソッドの続きを実装します。

```react:src/components/Auth/index.js
import React, { Component } from 'react'
import axios from 'axios'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.state = {
      isSignUp: true,
      token: null,
      error: ''
    }
  }

  handleChange(event) {
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    this.auth()
    event.preventDefault()
  }

  switchAuthModeHandler = () => {
    this.setState({ isSignUp: !this.state.isSignUp })
  }

  auth = () => {
    // 認証データ
    const authDate = {
      email: this.state.email,
      password: this.state.password,
      returnSecureToken: true
    }
    // signIn用のAPIキー
    let url = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=APIキーを入力'
    // signUp用のAPIキー
    if (this.state.isSignUp) {
      url = 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=APIキーを入力'
    }
    axios.post(url, authDate)
      .then(response => {
        // 返ってきたトークンをローカルストレージに格納する
        localStorage.setItem('token', response.data.idToken)
      })
      .catch(error => {
        // Firebase側で用意されているエラーメッセージが格納される
        this.setState({ error: error.response.data.error.message })
      })
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
          <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <div>
            {this.state.error}
          </div>
          <button>SUBMIT</button>
        </form>
        <button onClick={this.switchAuthModeHandler}>
          SWITCH TO {this.state.isSignUp ? 'SignIn' : 'SignUp'}
        </button>
      </div>
    )
  }
}

export default Auth
```

これでログイン機能の実装は完了です。
サインアップすると新しくユーザが作られているのを確認することができます。
パスワードは６文字以上でないと登録できないので注意してください。

![firebase_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/b0bee524-cc99-5544-6850-d90062fd2769.png)

サインアップまたは、サインインするとlocalStrageにtokenが保存されているのを確認することができます。

![token.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/55f87bb8-8536-3f56-5901-10f30c6de079.png)

`(localStorage.getItem('token'))`でtokenが保存されている（ログインしている）場合に任意の処理を実行できます。

```
if (localStorage.getItem('token')) {
  // ログインしている場合に任意のメソッドを実行
}
```

# Part4: ログアウト機能を実装しよう
ログアウトは簡単です。
ログインの時にlocalStrageに保存したtokenを削除するだけです。

```react:src/components/Auth/index.js
import React, { Component } from 'react'
import axios from 'axios'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.state = {
      isSignUp: true,
      token: null,
      error: ''
    }
  }

  handleChange(event) {
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    this.auth()
    event.preventDefault()
  }

  switchAuthModeHandler = () => {
    this.setState({ isSignUp: !this.state.isSignUp })
  }

  auth = () => {
    const authDate = {
      email: this.state.email,
      password: this.state.password,
      returnSecureToken: true
    }
    let url = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=APIキーを入力'
    if (this.state.isSignUp) {
      url = 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=APIキーを入力'
    }
    axios.post(url, authDate)
      .then(response => {
        localStorage.setItem('token', response.data.idToken)
      })
      .catch(error => {
        this.setState({ error: error.response.data.error.message })
      })
  }

  logout = () => {
    // トークンの削除
    localStorage.removeItem('token')
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
          <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <div>
            {this.state.error}
          </div>
          <button>SUBMIT</button>
        </form>
        <button onClick={this.switchAuthModeHandler}>
          SWITCH TO {this.state.isSignUp ? 'SignIn' : 'SignUp'}
        </button><br/>
        {/* ログインしている場合だけログアウトボタンが表示される */}
        {localStorage.getItem('token') &&
          <button onClick={this.logout}>LOGOUT</button>}
      </div>
    )
  }
}

export default Auth
```

**実装後の画面イメージ**
・ サインイン、サインアップするとlocalStrageにトークンが保存される
・ リロードしてもトークンは保持し続ける
・ ログアウトを選択するとトークンが削除される

![operation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231124/3a52d046-4bf8-1ed6-1aee-a1f0e301c7a7.gif)

# まとめ
長くなりましたが、いかがでしたでしょうか？
バックエンドサービスを何らかのフレームワークを使って実装していれば、標準の認証機能を導入できるとは思いますが、自前でデータベースを用意したり、個人情報をどう管理するかを考える必要があります。
Firebase Authenticationを使うことでこれらの問題を意識することなく、認証機能を実装することができました。
Firabaseでは、導入部分でも紹介したように様々な機能が用意されているので、ぜひ活用してみてください。

[Ateam Lifestyle Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ateam-lifestyle)の3日目は、@chardenがお送りします！！どんなネタを用意してくるのか楽しみです！！
"挑戦"を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/