Help us understand the problem. What is going on with this article?

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

この記事はAngular+Firebaseでチャットアプリを作るのエントリーです。
前記事:AngularのRxJSを使ってデータの受け渡しをする
次記事:Angularのルーティング設定(応用編)

この記事で行うこと

前回の記事ではRxJSの使い方を学習しました。
前回作成したsessionサービスを、Firebase Authenticationを使ってサーバーサイドのユーザー管理システムと連携させます。

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製品を利用する ×
匿名認証 ゲスト用認証 ×

参考

公式ドキュメント


(追記:2020/6)現時点(2020年6月)での最新の内容に書き換えています。


実装内容

Firebase Authenticationの設定をする

ログイン方法を設定する

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

screenshot-console.firebase.google.com-2020.06.26-00_30_52.png

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

screenshot-console.firebase.google.com-2020.06.26-00_32_12.png

テンプレートを設定する

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

NgChat – Authentication – Firebase console (1).png

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

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

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

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

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

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を使って、アプリの構成を管理する

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

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

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 = '';    
  }
}
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しておきます。

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.tsSessionServiceをDIし、submitSignUp()メソッドを追加します。

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()内部で行っていますが、カスタムディレクティブを使ってテンプレート側で制御することもできます。
※本記事では取り上げません。機会があれば書きます。

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

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>

ngFormngModelFormModuleのビルトインディレクティブです。
#signUpFormなどのテンプレート参照変数を使って、フォーム内の値を参照し、ログインボタンのバリデーションに利用しています。
#email#password#password_confirmationのいづれか1つの値がvalidでない場合、[disabled]が有効になってボタンをクリックできなくなります。

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

スクリーンショット 2018-03-07 14.03.13.png.png

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

screenshot-ngchat-f1343.firebaseapp.com-2020.06.26-12_09_33.png

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

ログイン画面の実装

前回作成したlogin()メソッドとlogout()メソッドにAngularFireAuthsignInWithEmailAndPassword()signOut()を追加します。

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

参考

公式ドキュメント

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

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

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

スクリーンショット 2018-03-07 14.31.53.png.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 パスワードが間違っている

参考:公式ドキュメント

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

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

ソースコード

この時点でのソースコード
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。

Yamamoto0525
6年間出版社でWEBディレクターとして経験を積んだのち、フリーランスとして活動していました。2017年に株式会社ShareDanを立ち上げ、自社サービスの開発、WEB制作、WEBコンサルティングを行っています。
https://www.sharedan.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした