Firebase AuthenticationとGAEで30分くらいでログイン機能を実装してみる

## TL;DR
今からFirebase Authenticationを使ってソーシャルログイン機能を30分くらいで検証用に実装します。
各種SNSのデベロッパーサイトへの登録はしてあるものとします。
要件として作成するのは、トップページと、ログイン後ページの２画面のシンプルな構成。

<img width="569" alt="スクリーンショット 2019-12-03 22.13.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/289ee6dc-792d-6d20-c18a-b9fccaba8e05.png">

コードは置いといた。(secret.yamlは準備してくれ・・・)
https://github.com/spre55/firebase-auth-sample/



### 今回使う主な技術・ツール・サービス

 - HTML/CSS/JavaScript
 - Go 1.13
 - Firebase Authentication
 - Google App Engine (Standard Environment)

Google App Engine(以下GAE)はGCP上で提供されるPaaSです。
デプロイ時にバージョンを指定でき、トラフィックを別バージョンに切り返すことで、Blue/Greenデプロイなども簡単に行えます。
今回はGAEのStandard Environmentを使います。Goをサポートしているので、バックエンドはGoを使ってみます。

更に、今回はソーシャルログイン機能ということで、認証を簡単に実装できるようにしてくれるFirebase Authenticationを使います。
アプリはもちろん、クライアントSDKはWebもサポートしているので、JavaScriptを使用して、簡単に認証機能を実装することができます。複雑な要件がなければ、ドキュメントのコピペで終わることでしょう。

###実装する機能

 - メール/パスワードサインアップ・ログイン
 - Twitterログイン
 - Googleログイン
 - Facebookログイン
 - Githubログイン
 - ログアウト

## アプリケーションの構築

今回は以下のようなシステム構成を目指します。
赤い部分が我々がコードで表現するアプリケーション部分です。
それ以外の部分は各種コンソール画面から、ボタンポチポチで実現できます。

![firebase_gae.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/f39debc9-aa9d-df01-79bb-97101c28c03c.png)



### 各種ソーシャルプロバイダの登録
まずは以下のソーシャルプロバイダのデベロッパーサイトへ登録し、APIキーなどを使用可能な状態にしてください。詳細な手順は省きます。

- Twitter Developers : https://developer.twitter.com/content/developer-twitter/ja.html
- Facebook for Developers : https://developers.facebook.com/?locale=ja_JP
- Github developers: https://github.com/settings/developers

### GAE/Go で Hello World !
以下からGoogle Consoleにログインしてチュートリアルを行えば、10分経たずで全世界にHello Worldできるかと思います。(実際にタイムアタックを試したら、クレジットカード登録も含め8分程度でできました)
https://cloud.google.com/appengine/?hl=ja

まずは、チュートリアルを終え、GoのアプリケーションをGoogle App Engineでデプロイしてみましょう。話はそれから。

GCPは一年間の無料期間が設定されてます。まだ触ってない人はこれを機にいじってみましょう。

ドキュメントに記載されている[クイックスタート](http://qiita.com/)にもあると思いますが、公式のGithubアカウントにて、最小構成のhelloworldアプリやその他もろもろが公開されているので、これをそのままクローンしてデプロイできますね。
https://github.com/GoogleCloudPlatform/golang-samples/tree/master/appengine/go11x/helloworld


### Firebase Consoleでの認証の設定

https://console.firebase.google.com/u/0/

Firebaseのクラウドコンソールにアクセスしてみましょう。

<img width="1318" alt="スクリーンショット 2019-12-03 2.46.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/ede61f0b-a394-ad12-e61d-428d7ae02701.png">


"ログイン方法"のタブにいくと、各種プロバイダのログイン方法が指定できますので、必要なものを有効にしましょう。今回は最初に述べた、**「メール/パスワード」、「Google」、「Twitter」、「Facebook」、「Github」**の５つを有効にしましょう。
ここで有効にするために、プロバイダによっては、APIキー及び、シークレットキーなどを求められるので、最初に各種デベロッパーサイトで設定して入手したAPIキーとシークレットキーを入力しましょう。

<img width="1319" alt="スクリーンショット 2019-12-03 2.47.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/308c59b9-a5e4-f282-4423-0bb5725fdcb2.png">



### とりあえず画面を２つつくる
すでに作成したGAEにてHello Worldを表示するアプリケーションが存在する前提で進みます。

とりあえずGoからサーバサイドレンダリングでHTMLの内容を表示するだけでいいので、一旦、今回作りたいトップページと、ログイン後ページのガワを作ります。

構成はこんな感じ。

```shell

├── app.yaml
├── main.go
└── templates
    ├── my.html
    └── signup.html
```

各ファイルは以下の通り。

```app.yaml
runtime: go113
```

templateディレクトリを作り、その配下に
サインアップ前のトップページと、

```signup.html
<!DOCTYPE html>
<html>
<body>
    <h1>Welcome to Sign Up Page!</h1>
</body>
</html>
```
サインアップ後に入れるマイページを作って、

```my.html
<!DOCTYPE html>
<html>
<body>
    <h1>Welcome to My Page!</h1>
</body>
</html>
```

main.goには、ルーティングまわりを記述。

```main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"text/template"
)

func main() {
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/my", myHandler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

// indexHandler ... サインアップページ(トップページ)
func indexHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}

	t := template.Must(template.ParseFiles("templates/signup.html"))
	if err := t.ExecuteTemplate(w, "signup.html", nil); err != nil {
		log.Fatal(err)
	}

}

// myHandler ... サインアップ後のマイページ
func myHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/my" {
		http.NotFound(w, r)
		return
	}

	t := template.Must(template.ParseFiles("templates/my.html"))
	if err := t.ExecuteTemplate(w, "my.html", nil); err != nil {
		log.Fatal(err)
	}
}

```


https://github.com/spre55/firebase-auth-sample/tree/feature/20191203_page_only

GAEにデプロイして、動作するようならオッケー。

### Firebase を使える状態にする
Firebaseを使えるようにするため、まずはクライアントSDKをCDNで呼ぶ。そのためにFirebaseコンソールからコードをコピってきてアプリケーション内に埋め込みたい。

まずFirebaseコンソールプロジェクトの設定をみる。

<img width="442" alt="スクリーンショット 2019-12-03 3.24.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/e419380e-d27e-298e-f891-01c7627d7cf8.png">

Settingページへ遷移するので、「アプリを追加」ボタンを押す。

<img width="1418" alt="スクリーンショット 2019-12-03 3.25.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/e3b8d150-beb4-7238-0f2a-e06b9253fe6f.png">

今回はWebアプリなのでWebを選択。

<img width="1440" alt="スクリーンショット 2019-12-03 3.25.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/586cfb87-9456-e579-87dc-a05663347c74.png">

適当にアプリの名前を決めると、以下のような画面になるので、ここで表示されたコードを、自分のアプリケーションに埋め込む。(今回はFirebase Hostingは使わないので未チェックでOK)

<img width="1440" alt="スクリーンショット 2019-12-03 3.25.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/e95ffe56-6277-9b46-194e-fe1cc9a87d16.png">

### アプリケーションに上記のコードを埋め込んで動作させる

上記コードはGithubで公開とかしないならベタ書きでもいいのですが、あんまり見られたくないので、`secret.yaml`に環境変数として記述し、それを`app.yaml`で読み込むようにします。
また、`secret.yaml`をGithubに公開したら意味ないので、`.gitignore`に`secret.yaml`を記載して、git管理対象から除外します。
複数人で共有する場合は、`secret.yaml`を暗号化するか、別の仕組みを使うかする必要がありますが、今回のアプリケーションではそこまではやりません。

最終的なディレクトリ構造はこんな感じ。

```
├── app.yaml
├── go.mod
├── main.go
├── secret.yaml
├── .gitignore
└── templates
    ├── my.html
    └── signup.html
```

`secret.yaml`はこんな感じで書いておいて、

```secret.yaml
env_variables:
  API_KEY: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  AUTH_DOMAIN: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.firebaseapp.com"
  DATABASE_URL: "https://XXXXXXXXXXXXXXXXXXXXXXX.firebaseio.com"
  PROJECT_ID: "XXXXXXXXXXXXXXXXXXXXXXX"
  STORAGE_BUCKET: "XXXXXXXXXXXXXXXXXXXXXXX.appspot.com"
  MESSAGING_SENDER_ID: "XXXXXXXXXXXXXXXX"
  APP_ID: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
```

`app.yaml`でそれを読み込みます。

```app.yaml
env_variables:
runtime: go113

includes:
- secret.yaml
```

`secret.yaml`は`.gitignore`に。

```secret.yaml
secret.yaml
```

次に`main.go`内にて`os.Getenv`で環境変数を読み込み,`signup.html`と`my.html`に渡したいので、

```main.go
 ... (略)
	if err := t.ExecuteTemplate(w, "signup.html", getEnvVars()); err != nil {
 ... (略)

func getEnvVars() map[string]string {
	return map[string]string{
		"apiKey":            os.Getenv("API_KEY"),
		"authDomain":        os.Getenv("AUTH_DOMAIN"),
		"databaseURL":       os.Getenv("DATABASE_URL"),
		"projectId":         os.Getenv("PROJECT_ID"),
		"storageBucket":     os.Getenv("STORAGE_BUCKET"),
		"messagingSenderId": os.Getenv("MESSAGING_SENDER_ID"),
		"appId":             os.Getenv("APP_ID"),
	}
}

```

```signup.html
...(略)
    <script>
        const config = {
            apiKey: "{{.apiKey}}",
            authDomain: "{{.authDomain}}",
            databaseURL: "{{.databaseURL}}",
            projectId: "{{.projectId}}",
            storageBucket: "{{.storageBucket}}",
            messagingSenderId: "{{.messagingSenderId}}",
            appId: "{{.appId}}",
        };

        firebase.initializeApp(config);
...(略)
```

こんな感じ。

### 各種機能の実装

ここまでで、各プロバイダとFirebaseを使う準備はできました。
あとは機能の実装です。

今回実装するのは以下の機能でした。

 - メール/パスワードサインアップ・ログイン
 - Twitterログイン
 - Googleログイン
 - Facebookログイン
 - Githubログイン
 - ログアウト

[firebaseui](https://github.com/firebase/firebaseui-web)を使って実装してみたいと思います。

`signup.html` にて、`uiConfig.signInOptions`に各種`PROVIDER_ID`を指定してあげれば、ソッコーで
ログインボタンのUIが出来上がります。

```signup.html
...(略)
        const uiConfig = {
            callbacks: {
                signInSuccessWithAuthResult: function (authResult, redirectUrl) {
                    return true;
                },
                uiShown: function () {
                    document.getElementById('loader').style.display = 'none';
                }
            },
            signInFlow: 'popup',
            signInSuccessUrl: 'my',
            signInOptions: [
                firebase.auth.GoogleAuthProvider.PROVIDER_ID, // Google
                firebase.auth.FacebookAuthProvider.PROVIDER_ID, // Facebook
                firebase.auth.TwitterAuthProvider.PROVIDER_ID, // Twitter
                firebase.auth.GithubAuthProvider.PROVIDER_ID, // Github
                firebase.auth.EmailAuthProvider.PROVIDER_ID, // Email/Password
            ],
        };
        // サインアップ/サインインフォームのUI
        const ui = new firebaseui.auth.AuthUI(firebase.auth());
        ui.start('#firebaseui-auth-container', uiConfig);
...(略)
```

`firebaseui`を使用すると、自分でCSSとかをいろいろいじったりする必要はありません。
<img width="474" alt="スクリーンショット 2019-12-03 4.08.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32691/7d2b9d0e-282b-694c-7de9-2a4f3259aa3b.png">


サインイン状態は以下のように、`firebase.auth().onAuthStateChanged()`を使用してとってこれます。
ログアウトも、`firebase.auth().signOut()`を呼ぶだけですね。

```my.html
... (略)
        // ユーザのサインイン状態の判定
        firebase.auth().onAuthStateChanged(function (user) {
            if (user) {
                // サインインできてるとき
                console.log('sign in');
            } else {
                // No user is signed in.
                console.log('sign out');
                location.href = "/";
            }
        });

        // ログアウト
        function signOut() {
            firebase.auth().signOut().then(function () {
                console.log('success sign out.');
            }).catch(function (error) {
                console.log('error: ', error);
            });
        }
... (略)

```

ログインページができました。
まあ、検証用では十分ですね。

## まとめ

GoogleAppEngineとFirebaseAuthenticationを使うと簡単にソーシャルログイン機能が作れます。
レンタルサーバーとか借りようと思ってる人、ためしに使ってみては？

## 困ったら
信頼性の高い公式ドキュメントを見ましょう。
ただし翻訳のタイムロスが無い分、ドキュメントは日本語のものより英語のもののほうが情報が新しかったりするので注意してください。