Angular+Firebase Authenticationで認証機能を導入する

> この記事は[Angular+Firebaseでチャットアプリを作る](http://qiita.com/Yamamoto0525/items/a76ea4b3924eeb82b0f9)のエントリーです。
> 前記事：[AngularのRxJSを使ってデータの受け渡しをする](https://qiita.com/Yamamoto0525/items/efc0e5617c7427fea86a)
> 次記事：[Angularのルーティング設定（応用編）](https://qiita.com/Yamamoto0525/items/bebb507416b562b14b77)

# この記事で行うこと

[前回の記事](https://qiita.com/Yamamoto0525/items/efc0e5617c7427fea86a)ではRxJSの使い方を学習しました。
前回作成したsessionサービスを、**Firebase Authentication**を使ってサーバーサイドのユーザー管理システムと連携させます。

- [Firebase Authenticationの設定をする](https://qiita.com/Yamamoto0525/items/054b28e8b34135399237#firebase-authentication%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%92%E3%81%99%E3%82%8B)
- [AngularFireAuthをsessionサービスに組み込む](https://qiita.com/Yamamoto0525/items/054b28e8b34135399237#angularfireauth%E3%82%92session%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AB%E7%B5%84%E3%81%BF%E8%BE%BC%E3%82%80)
- [ログイン画面に認証機能を実装する](https://qiita.com/Yamamoto0525/items/054b28e8b34135399237#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E7%94%BB%E9%9D%A2%E3%81%AB%E8%AA%8D%E8%A8%BC%E6%A9%9F%E8%83%BD%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B)

### Firebase Authenticationとは

**Firebase Authentication**は、Firebaseが提供するマルチプラットフォームログイン機能です。
自分で実装するとかなり面倒なユーザー管理機能ですが、Firebase Authenticationはその機能を無料で利用することができ、かつユーザーIDのデバイス間共有も容易にしてくれる優れものです。

認証はある程度類型化された機能ではありますが、少しでも注意を怠るとセキュリティ障害を引き起こします。スタートアップや小さいプロダクトの場合、できるだけその商品のMVP（Minimum Viable Product）に焦点を絞って開発したいのですが、認証機能は手を抜くわけにはいかないので、そこに時間と資金が取られてしまいます。

Firebase Authenticationは、メール認証やOAuth認証といった現代のニーズに即した認証機能を保有し、かつサーバーサイドの安全性を担保してくれるので、プロダクトマネジメントの視点からも採用を考慮すべきサービスだと思います。

### Firebase Authenticationでできること

|機能 |概要|本記事での使用|
|:--|:--|:-:|
|FirebaseUI Auth|Firebase謹製のUIパッケージ。必要な機能を全部のせできる。|×|
|メール&パスワード認証|メールアドレスとパスワードによる認証|○|
|フェデレーション ID プロバイダとの統合|OAuth認証方式で、Facebookやtwitterなどの外部サービスとのID連携を利用できる|×|
|電話番号認証|スマートフォンにSMSを送信してユーザーを認証する|×|
|カスタム認証|アプリの既存のログインシステムと連携して、RTDBなどのFirebase製品を利用する|×|
|匿名認証|ゲスト用認証|×|

#### 参考

[公式ドキュメント](https://firebase.google.com/docs/auth/?authuser=1)

*****
（追記：2020/6）現時点（2020年6月）での最新の内容に書き換えています。
*****

# 実装内容

## Firebase Authenticationの設定をする

### ログイン方法を設定する

開発に取り掛かる前に、FirebaseのコンソールからFirebase Authenticationを有効化しておきます。

![screenshot-console.firebase.google.com-2020.06.26-00_30_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112566/3060c807-ec09-a3bc-7b23-178588b7f03e.png)


本アプリケーションでは「メール&パスワード認証」を利用します。
「メール / パスワード」を有効にして保存してください。

![screenshot-console.firebase.google.com-2020.06.26-00_32_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112566/f0515418-e8a8-2246-5d2a-163ca73e14f2.png)


### テンプレートを設定する

次に、メールアドレスのテンプレートを変更します。
タブの中にある「テンプレート」を開き、画面左下にある「テンプレート言語」から「Japanese」を選択します。

![NgChat – Authentication – Firebase console (1).png](https://qiita-image-store.s3.amazonaws.com/0/112566/3970e164-d2a8-8b77-0bcf-0ca518a1edf9.png)

「パスワードの再設定」については文言の変更ができますが、「メールアドレスの確認」「メールアドレスの変更」は文言変更ができません。

もし返信用のドメインを変更したい場合は、鉛筆マークを押した先にある編集画面から「ドメインをカスタマイズ」を選択してください。
※変更したドメインは、すべてのテンプレートに反映されます。

これでコンソール側の設定は完了です。
次はアプリケーション側の実装に入っていきます。

## AngularFireAuthをsessionサービスに組み込む

まず、前回作成したsessionサービスに、*AngularFireAuth*をDIします。

```ts:src/service/session.service.ts
import { AngularFireAuth } from '@angular/fire/auth'; // 追加
import * as firebase from 'firebase/app'; // 追加

// ~~~ 省略 ~~~

  constructor(private router: Router,
              private afAuth: AngularFireAuth) { // 追加
  }
```
*AngularFireAuth*はルートモジュールですでにimportしているので、コンポーネント内の記述だけで問題ありません。

参考：　[AngularのNgModuleを使って、アプリの構成を管理する](https://qiita.com/Yamamoto0525/items/c647f03a5ab56883e09e)

この*AngularFireAuth*のメソッドである`createUserWithEmailAndPassword()`と、その返り値にある`firebase.User`が持つメソッド`sendEmailVerification()`を使って、アカウント作成用のメソッドを作成します。

その際、メールアドレスとパスワードを引数に渡す必要があるので、新しく`Password`クラスを作成します。

```ts:src/class/chat.ts
export class Password { // 追加
  email: string;
  password: string;
  passwordConfirmation: string;

  constructor() {
    this.email = '';
    this.password = '';
    this.passwordConfirmation = '';
  }

  reset(): void {
    this.email = '';
    this.password = '';
    this.passwordConfirmation = '';    
  }
}
```

```ts:src/service/session.service.ts
import { Password, Session } from '../class/chat'; // 更新

// ~~~ 省略 ~~~

  // アカウント作成
  signup(account: Password): void { // 追加
    this.afAuth
      .createUserWithEmailAndPassword(account.email, account.password) // アカウント作成
      .then(auth => auth.user.sendEmailVerification()) // メールアドレス確認
      .then(() => alert('メールアドレス確認メールを送信しました。'))
      .catch(err => {
        console.log(err);
        alert('アカウントの作成に失敗しました。\n' + err);
      });
  }
```

> (2020/06追記)これまで `this.afAuth.auth.createUserWithEmailAndPassword`と記述していましたが、angularfireの変更で`auth`が不要になりました。
> 参考: https://github.com/angular/angularfire/issues/2409

`createUserWithEmailAndPassword()`でアカウントを作成することはできますが、それだけではアカウントを有効にすることができません。`sendEmailVerification()`を設定することで、前述したテンプレートの内容を登録したメールアドレス宛に送信し、メールアドレスの実在を確認することができます。

## ログイン画面に認証機能を実装する

### サインアップ画面の実装

前項で作成したsessionサービスのメソッドをビューに設置していきます。
サインアップ画面の実装にかかる前に、今回は*FormModule*を使用するため、`account.module.ts`にSharedモジュールをimportしておきます。

```ts:src/account/account.module.ts
// CommonModuleを削除
import { SharedModule } from '../shared/shared.module'; // 追加

import { AccountRoutingModule } from './account-routing.module';

// ~~~ 省略 ~~~

@NgModule({
  imports: [
    // CommonModuleを削除
    SharedModule, // 追加
    AccountRoutingModule,
  ],
```
これで*FormModule*が使えるようになりました。
次に、`sign-up.component.ts`に`SessionService`をDIし、`submitSignUp()`メソッドを追加します。

```ts:src/account/sign-up/sign-up.component.ts
import { Component, OnInit } from '@angular/core';

import { Password }  from '../../class/chat'; // 追加
import { SessionService }  from '../../core/service/session.service'; // 追加

@Component({
  selector: 'app-sign-up',
  templateUrl: './sign-up.component.html',
  styleUrls: ['./sign-up.component.css']
})
export class SignUpComponent implements OnInit {

  public account = new Password(); // 追加

  constructor(private session: SessionService) { } // 追加

  ngOnInit() {
  }

  // アカウント作成
  submitSignUp(e: Event): void { // 追加
    e.preventDefault();
    // パスワード確認
    if (this.account.password !== this.account.passwordConfirmation) {
      alert('パスワードが異なります。');
      return;
    }
    this.session.signup(this.account);
    this.account.reset();
  }

}
```
ここではパスワード確認のバリデーションを`submitSignUp()`内部で行っていますが、カスタムディレクティブを使ってテンプレート側で制御することもできます。
※本記事では取り上げません。機会があれば書きます。

さらに、テンプレート側の実装を行います。

```html:src/account/sign-up/sign-up.component.html
<div class="page">
  <section class="card">
    <form class="form-signup" (ngSubmit)="submitSignUp($event)" #signUpForm="ngForm"><!-- 追加 -->
      <h2 class="form-signup-heading">アカウント作成</h2>
      <label for="inputEmail" class="sr-only">
        Email address
      </label>
      <input type="email" name="email"
             id="inputEmail" class="form-control"
             placeholder="Email address"
             [(ngModel)]="account.email"
             #email="ngModel"
             autocomplete="username email" required autofocus><!-- #email, name, autocompleteを追加 -->
      <label for="inputPassword" class="sr-only">
        Password
      </label>
      <input type="password" name="password"
             id="inputPassword" class="form-control"
             placeholder="Password"
             [(ngModel)]="account.password"
             #password="ngModel"
             autocomplete="new-password" required><!-- #password, name, autocompleteを追加 -->
      <label for="confirmationPassword" class="sr-only">
        Password
      </label>
      <input type="password" name="password_confirmation"
             id="confirmationPassword" class="form-control"
             placeholder="Password Confirmation"
             [(ngModel)]="account.passwordConfirmation"
             #password_confirmation="ngModel"
             autocomplete="new-password" required><!-- #password_confirmation, name, autocompleteを追加 -->
      <button class="btn btn-lg btn-success btn-block"
              type="submit" [disabled]="!signUpForm.form.valid">
        SIGN UP
      </button><!-- clickイベントを削除し、disabledを追加 -->
    </form>
    <a routerLink="/account/login"class="signup-link">
      ログインする
    </a>
  </section>
</div>
```
`ngForm`や`ngModel`は*FormModule*のビルトインディレクティブです。
`#signUpForm`などのテンプレート参照変数を使って、フォーム内の値を参照し、ログインボタンのバリデーションに利用しています。
※`#email`、`#password`、`#password_confirmation`のいづれか１つの値がvalidでない場合、`[disabled]`が有効になってボタンをクリックできなくなります。

では、この状態で`ng serve`を実行し、挙動を確認します。

![スクリーンショット 2018-03-07 14.03.13.png.png](https://qiita-image-store.s3.amazonaws.com/0/112566/a0b5b1d1-7016-148a-3da5-3ad0c3d89979.png)

送信されたメールアドレスから、メールアドレス有効化のリンクをクリックします。

![screenshot-ngchat-f1343.firebaseapp.com-2020.06.26-12_09_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112566/29953aaa-4bba-d9bd-a137-e539055ebf00.png)


この有効化完了画面は、Firebaseによって用意されたテンプレートを使用しています。
もしこの画面を変更したい場合は、firebaseコンソール画面のテンプレート編集から、「アクション URL をカスタマイズ」を選択して任意のURLを設定します。

### ログイン画面の実装

[前回作成](https://qiita.com/Yamamoto0525/items/efc0e5617c7427fea86a)した`login()`メソッドと`logout()`メソッドに*AngularFireAuth*の`signInWithEmailAndPassword()`と`signOut()`を追加します。

```ts:src/service/session.service.ts
  login(account: Password): void { // 変更
    this.afAuth
      .signInWithEmailAndPassword(account.email, account.password)
      .then(auth => {
        // メールアドレス確認が済んでいるかどうか
        if (!auth.user.emailVerified) {
          this.afAuth.signOut();
          return Promise.reject('メールアドレスが確認できていません。');
        } else {
          this.session.login = true;
          this.sessionSubject.next(this.session);
          return this.router.navigate(['/']);
        }
      })
      .then(() => alert('ログインしました。'))
      .catch( err => {
        console.log(err);
        alert('ログインに失敗しました。\n' + err);
      });
  }

  logout(): void {// 変更
    this.afAuth
      .signOut()
      .then(() => {
        this.sessionSubject.next(this.session.reset());
        return this.router.navigate(['/account/login']);
      }).then(() => alert('ログアウトしました。'))
      .catch( err => {
        console.log(err);
        alert('ログアウトに失敗しました。\n' + err);
      });
  }
```
ここで使用した*AngularFireAuth*のメソッドをまとめておきます。

|メソッド名|機能|戻り値|
|:--|:--|:-:|
|`createUserWithEmailAndPassword(email, password)`|メールアドレスとパスワードでアカウントを作成する|`firebase.auth.UserCredential`|
|`sendEmailVerification(actionCodeSettings)`|メールアドレス有効化メールを送信する|`void`|
|`signInWithEmailAndPassword(email, password)`|メールアドレスとパスワードでログインする|`firebase.auth.UserCredential`|
|`signOut()`|ログアウトする|`void`|

### 参考

[公式ドキュメント](https://firebase.google.com/docs/reference/js/firebase.auth?authuser=1)

サインアップ画面同様、ログイン画面の実装を行います。

```ts:src/account/login/login.component.ts
import { Component, OnInit } from '@angular/core';

import { SessionService } from '../../core/service/session.service';
import { Password } from '../../class/chat'; // 追加

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  public account = new Password(); // 追加

  constructor(private sessionService: SessionService) { }

  ngOnInit() {
  }

  submitLogin(e: Event) {
    e.preventDefault();
    this.sessionService.login(this.account); // 変更
  }

}
```

```src/account/login/login.component.html
<div class="page">
  <section class="card">
  	<form class="form-login" (ngSubmit)="submitLogin($event)" #loginForm="ngForm"><!-- 追加 -->
  		<h2 class="form-login-heading">
  			ログイン
  		</h2>
	    <label for="inputEmail" class="sr-only">
	    	Email address
	    </label>
	    <input type="email" name="email" 
	    	id="inputEmail" class="form-control" 
	    	placeholder="Email address" 
	    	[(ngModel)]="account.email" 
	    	#email="ngModel"
	    	autocomplete="username email" required autofocus><!-- #email, name, autocompleteを追加 -->
	    <label for="inputPassword" class="sr-only">
	    	Password
	    </label>
	    <input type="password" name="password" 
	    	id="inputPassword" class="form-control" 
	    	placeholder="Password" 
	    	[(ngModel)]="account.password" 
	    	#password="ngModel"	    	
	    	autocomplete="new-password" required><!-- #password, name, autocompleteを追加 -->
	    <button class="btn btn-lg btn-info btn-block" type="submit" 
	    	[disabled]="!loginForm.form.valid">
	    	LOGIN
	    </button><!-- clickイベントを削除し、disabledを追加 -->
	</form>
	<a routerLink="/account/sign-up" class="login-link">
		アカウントを作る
	</a>
  </section>
 </div>
```
先ほど作成したアカウントを使って、ログインしてみます。

![スクリーンショット 2018-03-07 14.24.38.png.png](https://qiita-image-store.s3.amazonaws.com/0/112566/e5eb4315-4695-fe50-0500-21db59b641d0.png)

正しいアカウントでログインを行うと、「ログインしました。」というアラートが表示され、チャット画面に遷移するようになります。
もしこの時、間違ったアカウントで登録しようとすると、次のように表示されます。

![スクリーンショット 2018-03-07 14.31.53.png.png](https://qiita-image-store.s3.amazonaws.com/0/112566/e72ed46d-96a2-8d9b-c456-69cb20659232.png)

認証先のFirebaseが送信されたemail、パスワードを評価し、その結果を返してくれます。
パスワード認証にかかるエラーレスポンスは次の通りです。

#### サインアップ時のエラーレスポンス

|エラーコード|概要|
|:--|:--|
|auth/email-already-in-use|メールアドレスがすでに使用されている|
|auth/invalid-email|メールアドレスの形式が正しくない|
|auth/operation-not-allowed|メールアドレス/パスワード方式が有効化されていない|
|auth/weak-password|パスワードが弱すぎる|

#### ログイン時のエラーレスポンス

|エラーコード|概要|
|:--|:--|
|auth/invalid-email|メールアドレスの形式が正しくない|
|auth/user-disabled|ユーザーが無効化されている|
|auth/user-not-found|ユーザーが見つからない|
|auth/wrong-password|パスワードが間違っている|

参考：[公式ドキュメント](https://firebase.google.com/docs/reference/js/firebase.auth?authuser=1)

これで認証機能のベースは整いました。
ただ、今の段階ではたとえログインをしていなくても直接URLを入力すると、チャット画面に遷移することができてしまいます。

これでは認証機能として意味がありませんので、次回はルーティングのガード設定を行っていきます。

## ソースコード

この時点での[ソースコード](https://github.com/Yamamoto0525/NgChat/tree/155445f0641a5fb512fe5d486ecac730fde97659)
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。