Firebaeを始めよう No.1 Googleで認証する

この記事の完全なソースコードは次のリポジトリのpublic/authフォルダにあります。

GitHub - webarata3/begin_Firebase: Firebaseのテストです。
Custom Elementsを使っているため、ChromeとSafariでのみ動作します。Firefoxは次のバージョン(63)から多分動きます。

プロジェクトの準備

今回は、Firebaseを使った認証を紹介します。Firebaseを使った認証はメールアドレス認証や、GoogleやTwitterアカウントを使った認証等多数あります。今回はGoogleアカウントでの認証を紹介します。

プロジェクトの作成

Firebaseを始めるには、Web上でプロジェクトを作成するところから行います。プロジェクトの作成は次のページから行います。

ログイン - Google アカウント

このページの「プロジェクトを追加」を押します。

そうすると、次のような画面になりますので、必要事項を登録します。

今回は、プロジェクト名を「sandbox」としました。「プロジェクトを作成」ボタンを押し、しばらく待つとプロジェクトが作成されます。

Firebaseの設定

Google認証をするためにFirebase consoleから「開発」→「Authentication」→「ログイン方法を設定」を選択します。

表示された画面で、「Google」の横にある編集ボタン(鉛筆のアイコン)を押します。そうすると次のような画面になりますので、必要事項を登録します。

これでGoogle認証の準備は完了です。

開発準備

プロジェクトを作成したら、開発の準備をします。Node.jsやfireabae-toolsが必要ですが、それに関しては次の記事を参考にしてください。

Firebaseとムームードメインで独自ドメインでhttps
最初にFirebaseのホスティングを使用するとWebサイトを無料で始めることができ、ドメインを持っていれば独自ドメインをhttpsで運用することもできます。具体的な価格設定は、次のサイトを参考にしてください。今回は、Firebaseのホス

上記準備ができたら、次にソースコード等を置くための、プロジェクトのフォルダを作成します。場所は問いません。好きなところに作成してください。

フォルダができたら、そのプロジェクトのフォルダで、次のコマンドを入力しログインします。

$ firebase login

ブラウザが起動するので、Googleアカウントでログインしてください。

ログインしたら、次のコマンドでプロジェクトの初期化をします。

$ firebase init 

設定項目を対話型でいろいろ聞かれます。ほとんどデフォルト値で構わないですが、プロジェクトには「sandbox」、どの機能をセットアップするかはとりあえずホスティングを選択してください。

そうすると、公開用のpublicフォルダが作成され、その中にデフォルトのindex.htmlと404.htmlが作成されます。

これで開発の準備は終わりです。

プログラム

実行方法

最初に、Firebaseプロジェクトの基本的な実行方法を確認します。Firebaseの開発をするときはfirebase-toolsに組み込まれているHTTPサーバーを使うのが(色んな意味で)便利です。

HTTPサーバーはプロジェクトフォルダのルートで、次のコマンドを入力し実行します。

$ firebase serve

コマンドを入力するとコンソールにURLが表示されます。そのURLは次のとおりです。

http://localhost:5000

認証

認証をするためにまず画面を作ります。HTMLは次のようにします。

<!DOCTYPE html>
<html lang="ja">
 <head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ログイン認証テスト</title>
 </head>
 <body>
  <auth-google></auth-google>
  <script src="/__/firebase/5.4.2/firebase-app.js"></script>
  <script src="/__/firebase/5.4.2/firebase-auth.js"></script>
  <script src="/__/firebase/init.js"></script>
  <script src="/common/auth-google.js"></script>
 </body>
</html>

今回は、認証自体を他の機能でも使うため、Custom Elementsで認証を実装しています。Custom Elementsのauth-gmailタグで認証画面を表示し、認証まで行います。

さて、HTML中のJavaScriptですが次のように記載しています。

  <script src="/__/firebase/5.4.2/firebase-app.js"></script>
  <script src="/__/firebase/5.4.2/firebase-auth.js"></script>
  <script src="/__/firebase/init.js"></script>
  <script src="/common/auth-google.js"></script>

上から4つ目の「auth_google.js」がCustom Elementsの定義が存在し、自分で作成するファイルです。その他の3つのファイルが最初から準備されているファイルです。最初の2つに関しては、Firebaseを使うためのソースになります。これらは用途に応じて次の6種類があります。

<!-- Firebase App is always required and must be first -->
<script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-app.js"></script>

<!-- Add additional services you want to use -->
<script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-database.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-firestore.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-messaging.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-functions.js"></script>

(英語ですが)コメントにあるように、「firebase-app.js」は必ず読み込む必要があります。その他、auth、database、firestore、messaging、functionsは利用する機能に応じて読み込みます。今回は認証なので、「firebase-auth.js」を使います。

ここで、読み込むsrcのパスが特殊なことに気づくかと思います。最初に挙げた例では、「firebase-app.js」へのパスは次のようになっています。

<script src="/__/firebase/5.4.2/firebase-app.js"></script>

しかし、次の例で挙げたパスは次のようになっています。

<script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-auth.js"></script>

/__/のパスは、Firebaseに予約されているパスで、「firebase serve」で起動したlocalhostへのアクセスの場合と、Firebaseでホスティングした場合にのみ有効になります。/__/には実ファイルを置く必要はありません。自動的にファイルが配備されている状態になります。また、全バージョンの利用ができます。上記条件を満たさない場合には、gstatic.comのソースを利用することになります。

もう一つ、「init.js」ファイルについてです。これもFirebaseでホスティングしている場合のみ有効なファイルになります。ここには、Firebaseを利用するための設定が書かれています。具体的には次のような内容になります。

if (typeof firebase === 'undefined') throw new Error('hosting/init-error: Firebase SDK not detected. You must include it before /__/firebase/init.js');
firebase.initializeApp({
  "apiKey": "<API_KEY>",
  "databaseURL": "https://<DATABASE_NAME>.firebaseio.com",
  "storageBucket": "<BUCKET>.appspot.com",
  "authDomain": "<PROJECT_ID>.firebaseapp.com",
  "messagingSenderId": "<SENDER_ID>",
  "projectId": "<PROJECT_ID>"
});

ここに記載されている<>で囲まれた部分はプロジェクトごとに異なります。この値は、Firebaseのコンソールから、「Project Overview」を押してから、「Webのアイコン」を押すと表示されます。

Firebaseでホスティングしない場合には、自分で「init.js」に変わるものを用意しないといけません。

FirebaseのJavaScriptの基本的な設定はここまでです。次に、具体的な認証ロジックを書く、「auth_google.js」を確認します。

'use strict';

class AuthGoogle extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
<style>
.auth_button {
  background-color: #fff;
  border: 1px solid #007bff;
  border-radius: 5px;
  color: var(--blue);
  padding: 4px 8px;
  transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
  outline: none;
}

.auth_button:hover {
  background-color: #007bff;
  color: #fff;
}

#profileImage {
  width: 32px;
  height: 32px;
  background-size: 32px;
  border-radius: 16px;
}

/*
 * flex-boxを指定していると、hidden属性が効かない
 * そのため、class="hidden"を付けてhidden属性の代わりにする
 * https://stackoverflow.com/questions/23772673/hidden-property-does-not-work-with-flex-box
 */
#loaderArea {
  background-color: rgba(0, 0, 0, 0.75);
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: 100vw;

  display: flex;
  justify-content: center;
  align-items: center;

  z-index: 9999;
}

#loaderArea.hidden {
  display: none;
}

#loaderArea > div {
  text-align: center;
}

/* ぐるぐる https://codelabs.developers.google.com/codelabs/your-first-pwapp-ja/#0 */
.loader #spinner {
  box-sizing: border-box;
  stroke: #fff;
  stroke-width: 3px;
  transform-origin: 50%;
  animation: line 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite, rotate 1.6s linear infinite;
}

#loaderMessage {
  color: #fff;
}

@keyframes rotate {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(450deg);
  }
}

@keyframes line {
  0% {
    stroke-dasharray: 2, 85.964;
    transform: rotate(0);
  }
  50% {
    stroke-dasharray: 65.973, 21.9911;
    stroke-dashoffset: 0;
  }
  100% {
    stroke-dasharray: 2, 85.964;
    stroke-dashoffset: -65.973;
    transform: rotate(90deg);
  }
}

.loginUser {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
  margin: 10px;
}

.loginUser > div:not(:last-child) {
  margin-right: 10px;
}
</style>
  <div id="loaderArea">
   <div>
    <div class="loader">
     <svg viewBox="0 0 32 32" width="32" height="32">
      <circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
     </svg>
    </div>
    <div id="loaderMessage">読み込み中...</div>
   </div>
  </div>
  <div class="loginUser">
   <div id="profileImage"></div>
   <div id="displayName"></div>
   <div>
    <button id="loginButton" class="auth_button">Googleでログイン</button>
    <button id="logoutButton" class="auth_button" hidden>ログアウト</button>
   </div>
  </div>
`;

    const loaderArea = this.shadowRoot.getElementById('loaderArea');
    const loginButton = this.shadowRoot.getElementById('loginButton');
    const logoutButton = this.shadowRoot.getElementById('logoutButton');
    const displayName = this.shadowRoot.getElementById('displayName');
    const profileImage = this.shadowRoot.getElementById('profileImage');

    const provider = new firebase.auth.GoogleAuthProvider();

    loginButton.addEventListener('click', event => {
      event.preventDefault();
      loaderArea.classList.remove('hidden');
      firebase.auth().signInWithRedirect(provider);
    });

    logoutButton.addEventListener('click', event => {
      event.preventDefault();
      loaderArea.classList.remove('hidden');
      firebase.auth().signOut();
    });

    const PROFILE_PLACEHOLDER_IMAGE = '/image/profile_placeholder.png';

    firebase.auth().onAuthStateChanged(user => {
      loaderArea.classList.add('hidden');
      if (user) {
        loginButton.setAttribute('hidden', 'true');
        logoutButton.removeAttribute('hidden');
        displayName.textContent = user.displayName;
        const profileImageUrl = user.photoURL || PROFILE_PLACEHOLDER_IMAGE;
        profileImage.style.backgroundImage = `url(${profileImageUrl})`;
        this.dispatchEvent(new CustomEvent('login', {
          detail: user
        }));
      } else {
        loginButton.removeAttribute('hidden');
        logoutButton.setAttribute('hidden', 'true');
        displayName.textContent = '';
        profileImage.style.backgroundImage = 'none';
        this.dispatchEvent(new Event('logout'));
      }
    });
  }
}

customElements.define('auth-google', AuthGoogle);

Custom Element中のHTMLで重要なのは、ログインボタンとログアウトボタンです。未ログインの場合にはログインボタンを表示し、ログイン中の場合にはログアウトボタンを表示します。また、displayName領域にはGoogleアカウントに登録されている名前を、profileImage領域にはGoogleアカウントに登録されている写真を表示します。

CSSでは、ログイン処理自体が少し重い処理のため、ログイン中はぐるぐるを表示し操作できないようにしています。

次に、JavaScript部分を見ていきます。

最初に認証のためのプロバイダを初期化しておきます。

const provider = new firebase.auth.GoogleAuthProvider();

ログイン処理は次のようになります。

loginButton.addEventListener('click', event => {
  event.preventDefault();
  loaderArea.classList.remove('hidden');
  firebase.auth().signInWithRedirect(provider);
});

処理自体は、firebase.auth().signInWithRedirect(provider);だけです。ログインの画面を出す処理としては、今回の例にあるようにリダイレクトによる方法やポップアップを出す方法があります。モバイル対応させる場合にはリダイレクトのほうがいいです。

次に、ログアウトの処理です。firebase.auth().signOut();でログアウトが行なえます。

logoutButton.addEventListener('click', event => {
  event.preventDefault();
  loaderArea.classList.remove('hidden');
  firebase.auth().signOut();
});

ログイン、ログアウトは非同期で実行されます。状態の変化は、onAuthStateChangedで把握できます。ログインしている場合にはuserに値が入っていますので、それでログインしたのかログアウトしたのかを判別できます。

firebase.auth().onAuthStateChanged(user => {
  loaderArea.classList.add('hidden');
  if (user) {
    loginButton.setAttribute('hidden', 'true');
    logoutButton.removeAttribute('hidden');
    displayName.textContent = user.displayName;
    const profileImageUrl = user.photoURL || PROFILE_PLACEHOLDER_IMAGE;
    profileImage.style.backgroundImage = `url(${profileImageUrl})`;
  } else {
    loginButton.removeAttribute('hidden');
    logoutButton.setAttribute('hidden', 'true');
    displayName.textContent = '';
    profileImage.style.backgroundImage = 'none';
  }
});

Google認証の結果のuserはfirebase.Userとして定義されています。認証後必要なものを使いましょう。

今回はCustom Elementsの中に認証ロジックを閉じ込めているので、そのままだとCustom Elementsの外側で認証情報を使えません。ですので、Custom Elementsの外側でもfirebase.Userを使えるように、ログイン・ログアウト時にloginカスタムイベントを用意し、発火しています。

コメント