この記事は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を有効化しておきます。
本アプリケーションでは「メール&パスワード認証」を利用します。
「メール / パスワード」を有効にして保存してください。
テンプレートを設定する
次に、メールアドレスのテンプレートを変更します。
タブの中にある「テンプレート」を開き、画面左下にある「テンプレート言語」から「Japanese」を選択します。
「パスワードの再設定」については文言の変更ができますが、「メールアドレスの確認」「メールアドレスの変更」は文言変更ができません。
もし返信用のドメインを変更したい場合は、鉛筆マークを押した先にある編集画面から「ドメインをカスタマイズ」を選択してください。
※変更したドメインは、すべてのテンプレートに反映されます。
これでコンソール側の設定は完了です。
次はアプリケーション側の実装に入っていきます。
AngularFireAuthをsessionサービスに組み込む
まず、前回作成したsessionサービスに、AngularFireAuthをDIします。
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
クラスを作成します。
export class Password { // 追加
email: string;
password: string;
passwordConfirmation: string;
constructor() {
this.email = '';
this.password = '';
this.passwordConfirmation = '';
}
reset(): void {
this.email = '';
this.password = '';
this.passwordConfirmation = '';
}
}
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しておきます。
// 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()
メソッドを追加します。
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()
内部で行っていますが、カスタムディレクティブを使ってテンプレート側で制御することもできます。
※本記事では取り上げません。機会があれば書きます。
さらに、テンプレート側の実装を行います。
<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
のいづれか1つの値がvalidでない場合、[disabled]
が有効になってボタンをクリックできなくなります。
では、この状態でng serve
を実行し、挙動を確認します。
送信されたメールアドレスから、メールアドレス有効化のリンクをクリックします。
この有効化完了画面は、Firebaseによって用意されたテンプレートを使用しています。
もしこの画面を変更したい場合は、firebaseコンソール画面のテンプレート編集から、「アクション URL をカスタマイズ」を選択して任意のURLを設定します。
ログイン画面の実装
前回作成したlogin()
メソッドとlogout()
メソッドにAngularFireAuthのsignInWithEmailAndPassword()
とsignOut()
を追加します。
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 |
参考
サインアップ画面同様、ログイン画面の実装を行います。
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); // 変更
}
}
<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>
先ほど作成したアカウントを使って、ログインしてみます。
正しいアカウントでログインを行うと、「ログインしました。」というアラートが表示され、チャット画面に遷移するようになります。
もしこの時、間違ったアカウントで登録しようとすると、次のように表示されます。
認証先の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を入れてください。