Google App Engine + Go + Angular + Firebase Authentication でログイン画面の実装


# はじめに

* Go と Angular で実装したクライアント・サーバーアプリケーションで Firebase Authentication を使ってログイン機能を実装します。
* また、実装したアプリを Google App Engine 環境へデプロイします。


# 手順概要

1. Firebase Authentication の設定
1. クライアントサイドのアプリケーションを作成
1. サーバーサイドのアプリケーションを作成
1. ローカル環境で実行して動作を確認
1. Google App Engine 環境へデプロイして動作を確認


# Firebase Authentication の設定

* Firebase Authentication を利用できるようにするため、Firebase のプロジェクトを作成し、Authentication の設定を行います

## 手順

1. Firebase Console にログイン
1. プロジェクトを作成
1. `開発 > Authentication` に移動し `ログイン方法 > Google` で Google ログインを有効化
1. `アプリを追加 > ウェブ` でウェブアプリを作成
1. ウェブアプリ作成完了画面で表示される `apiKey` などの認証情報をメモしておく
1. `Authentication > ログイン方法` と移動し `Google` のログインを有効にする


# クライアントサイドのアプリケーションを作成

* Angular CLI でアプリケーションの雛形を作成し、Firebase Admin SDK を使った認証と、認証情報を使ったサーバーアプリケーションへのアクセスする機能を実装します。

## アプリケーションの雛形の作成と SDK のインストール

```
# アプリケーションの作成
ng new gae-firebase-auth-sample-client

# SDK インストール
npm install firebase @angular/fire --save
```

## Firebase 認証情報の設定

* Firebase のウェブアプリ作成時に表示された認証情報を設定ファイルに記述します

```ts:src/environments/environment.ts
export const environment = {
  production: false,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>'
  }
};
```

## Firebase Admin SDK の初期化

* Firebase Admin SDK を初期化し、Authentication や Http リクエスト用のモジュールを DI できるように読み込みます。

```ts:src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import {AngularFireModule} from '@angular/fire';
import { AngularFireAuthModule } from '@angular/fire/auth';
import {environment} from '../environments/environment';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireAuthModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

## ログイン画面の実装

* ログイン画面とサーバーアプリケーションにアクセスする機能を実装します。
* ログイン処理は DI で初期化される AppComponent.afAuth によって管理され、未ログインならログインボタンを表示し、ログイン済みならログアウトボタンとサーバーへアクセスするボタンが表示されます。
* サーバーへのアクセス時には afAuth から取得できる ID トークンをヘッダに付与して認証を行います。

```ts:src/app/app.component.ts
import { Component } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { auth } from 'firebase/app';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {environment} from '../environments/environment';

@Component({
  selector: 'app-root',
  template: `
    <div *ngIf="afAuth.user | async as user; else showLogin">
      <h1>Hello {{ user.displayName }}!</h1>
      <button (click)="logout()">Logout</button>
      <button (click)="accessServer()">Access Server</button>
    </div>
    <ng-template #showLogin>
      <p>Please login.</p>
      <button (click)="login()">Login with Google</button>
    </ng-template>
  `,
})
export class AppComponent {
  constructor(public afAuth: AngularFireAuth, private http: HttpClient) {
  }
  // Firebase Authentication のログイン実行
  login() {
    this.afAuth.auth.signInWithPopup(new auth.GoogleAuthProvider());
  }
  // Firebase Authentication のログアウト実行
  logout() {
    this.afAuth.auth.signOut();
  }
  // Firebase Authentication の ID Token をヘッダに付与してサーバーアクセスする
  accessServer() {
    this.afAuth.auth.currentUser.getIdToken().then((idToken: string) => {
      const header = new HttpHeaders().set('Authorization', 'Bearer ' + idToken);
      this.http.get(environment.serverUrl, {headers: header}).subscribe((res: string) => {
        console.log(res);
      });
    });
  }
}
```


# サーバーサイドのアプリケーションを作成

* Go 1.12 で Firebase Authentication の ID トークンを受け取って検証を行うだけのサーバーアプリケーションを作成します
* 依存パッケージ管理は Go Modules を使用します

## メインファイルの作成

* net/http を使って ID トークンを検証するだけの HTTP サーバーを実装します
* ローカル環境での実行時にはサーバーアプリケーションとクライアントアプリケーションが別ドメインになるため、環境変数で CORS を有効にできるように用意しておきます

```go:app.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"

	firebase "firebase.google.com/go"
	"firebase.google.com/go/auth"
	"github.com/rs/cors"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", top)

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

	if os.Getenv("ENABLE_CORS") != "" {
		c := cors.New(cors.Options{
			AllowedOrigins: []string{"http://localhost:4200"},
			AllowedHeaders: []string{"Authorization"},
			Debug:          true,
		})
		log.Fatal(http.ListenAndServe(addr, c.Handler(mux)))
	} else {
		log.Fatal(http.ListenAndServe(addr, mux))
	}
}

func top(w http.ResponseWriter, r *http.Request) {
	// Firebase Auth クライアントの作成
	ctx := r.Context()
	client, err := newClient(ctx)
	if err != nil {
		log.Fatalf("error initializing app: %v\n", err)
	}

	// ヘッダから ID Token の取得
	idToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")

	// ID Token の検証
	token, err := client.VerifyIDToken(ctx, idToken)
	if err != nil {
		log.Fatalf("error verifying ID token: %v\n", err)
	}

	fmt.Fprintf(w, `{"result":"verified id token. %+v"}`, token)
}

func newClient(ctx context.Context) (*auth.Client, error) {
	app, err := firebase.NewApp(ctx, nil)
	if err != nil {
		return nil, err
	}
	client, err := app.Auth(ctx)
	if err != nil {
		return nil, err
	}
	return client, err
}
```

## 依存パッケージの管理

* Go Modules を使って依存パッケージをダウンロードします

```
go mod init
go mod download
```



# ローカル環境で実行して動作を確認

* クライアントアプリケーションとサーバーアプリケーションをそれぞれローカルで実行し、ログイン及びサーバーアクセスの動作確認を行います。

## サーバーアプリケーションの実行

* サーバーアプリケーションをローカルで実行する際には Firebase へアクセスするためにサービスアカウントの秘密鍵を用意する必要があります

### Firebase のサービスアカウントの秘密鍵のダウンロード

* Firebase Console にログイン
* `設定 > プロジェクトの設定` に移動し `サービスアカウント > Firebase Admin SDK` を選択
* `新しい秘密鍵の作成` ボタン押下で JSON ファイルをダウンロード

### 実行

```
GOOGLE_APPLICATION_CREDENTIALS=${秘密鍵ファイルのパス} \
ENABLE_CORS=1 \
go run app.go
```

## クライアントアプリケーションの実行

```
ng serve
```

## 動作確認

* http://localhost:4200 へアクセスし `Login with Google` ボタンを押下すると Google 認証画面へリダイレクトされて認証が行われます
* ログイン状態では `Logout` ボタンでログアウト処理が `Access Server` でサーバーアクセス処理が実行されます。サーバーアクセスの結果は Console で確認できます。
* Firebase Authentication による認証ができていると Firebase コンソールから作成済みユーザーの一覧も確認できます。


# Google App Engine 環境へデプロイして動作を確認

## クライアントアプリケーションのビルド

* クライアントアプリケーションをビルドしてサーバーアプリケーションはいかに出力し Google App Engine で配信できるようにする

```
ng build --prod --output-path=${PATH_TO_SERVER_APP}/client/ --base-href=/client/
```

## サーバーアプリケーションの Google App Engine 設定

* Google App Engine 用の設定ファイル app.yaml を作成する
* `/client/` へのリクエストをクライアントアプリケーションのファイルで処理できるように `handlers` 設定を行います

```app.yaml
runtime: go112

handlers:
  - url: /client/(.*\.(gif|png|jpeg|jpg|css|js|ico))$
    static_files: client/\1
    upload: client/(.*)
  - url: /client/(.*)
    static_files: client/index.html
    upload: client/index.html
  - url: /.*
    script: auto
```

## Firebase Authentication 設定更新

* Firebase Authentication で Google App Engine のドメインから Google ログインができるように許可ドメインを追加します。

### 手順

* Firebase コンソールにログイン
* `Authentication > ログイン設定` に移動し
* `承認済みドメイン` に `プロジェクト名.appspot.com` を追加

## デプロイ

```
gcloud app deploy
```

## 動作確認

* https://{プロジェクト名}.appsopt.com/client/ にアクセスしてローカル環境と同様に動作確認を行います


# 参考サイト

* https://github.com/angular/angularfire
* https://github.com/angular/angularfire/blob/master/docs/install-and-setup.md
* https://firebase.google.com/docs/admin/setup
* https://firebase.google.com/docs/auth/admin/verify-id-tokens

# ソース

* https://github.com/nirasan/gae-firebase-auth-sample-server
* https://github.com/nirasan/gae-firebase-auth-sample-client