このコードラボでは、従来のアーキテクチャで構築されている既存のウェブサイトに Credential Management API を実装し、自動ログインを利用可能にする方法を学びます。

学習内容

用意するもの

Chrome の設定で "Auto Sign-In" がオンになっていることを確認してください。

このチュートリアルの利用方法をお選びください。

通読するのみ 通読し、演習を行う

ウェブアプリ作成の経験に関して、ご自身に当てはまるものをお選びください。

初心者 中級者 上級者

Credential Management API による自動ログインはノートパソコンからリモートで有効にできますが、このコードラボでは、クラウドで実行されるコマンドライン環境の Google Cloud Shell を使用します。Google Cloud Shell は Debian ベースの仮想マシンで、永続的な 5 GB のホーム ディレクトリを備え、ネットワーク パフォーマンスと認証効率を大幅に向上します。このコードラボで必要になるのはブラウザのみです(したがって Chromebook でも利用できます)。

Cloud Console 内にプロジェクトを設定する

Google アカウント(Gmail または Google Apps)をお持ちでない場合は、作成します。Google Cloud Platform Console(console.cloud.google.com)にログインし、新しいプロジェクトを作成します。

プロジェクト ID を控えておきます。プロジェクト ID は、すべての Google Cloud プロジェクトで 1 つしかない名前です。

Google Sign-In の認証情報を設定する

左側のメニュー から API Manager を開き、[Credentials] を選択します。

[OAuth consent screen] タブを選択し、[Product name shown to users] を指定します。任意の名前を指定できますが、今回は「Credential Management Codelab」にしましょう。[Save] を選択して保存します。

今度は、Cloud Shell をアクティベートします。右上のボタンをクリックします(環境へのプロビジョニングと接続に少し時間がかかります)。

プロンプトが表示されたら、[Start Cloud Shell] を選択します。Cloud Shell が開いたら、プレビュー ボタンをクリックして、ブラウザ ウィンドウを開きます。エラー メッセージは無視してかまいません。このエラーは後で修正されます。

このブラウザウィンドウを使用して、アプリをプレビューします。ブラウザに表示された URL は、後で使用するのでメモします。

[Credentials] 画面に戻り、[Credentials] タブを選択し、[Create credentials] をクリックして [OAuth client ID] を選択します。

[Web application] を選択して、フォームに次のように入力します。

これらの項目に入力できたら、[Create] を選択して認証情報を作成します。この認証情報のエントリの右横にある アイコンをクリックして、認証情報が記載されている JSON ファイルをダウンロードします。このファイルは、client_secret_******.apps.googleusercontent.com.json のような名前になります。次のセクションでは、この JSON ファイルのコンテンツを使用します。

リポジトリのクローンを作成する

Cloud Shell に戻ります。いよいよベースコードを作成します。

まず、git clone を使用してレポジトリのクローンを作成します。

$ git clone https://github.com/googlecodelabs/credential-management-api.git

コードのクローンを作成できたら、ディレクトリに移動し、環境を設定します。

$ cd credential-management-api
$ virtualenv env
$ source env/bin/activate
$ npm install

client_secrets.json を作成する

最後に、プロジェクトのルートに client_secrets.json というテキスト ファイルを作成し、先ほどダウンロードした client_secret_******.apps.googleusercontent.com.json という名前の JSON ファイルのコンテンツをコピーして貼り付けます。

次へ進む

これで設定は完了です。おつかれさまでした。

次のコマンドを使用して、ベースコードを実行します。

$ dev_appserver.py working --host=0.0.0.0 --port=8080 --admin_port=8081

プレビュー ボタンをクリックして、ベースコードの実行状態を確認します。

ではウェブサイトを確認して、ウェブサイトの動作と、このチュートリアルの作業内容について説明します。現在のサーバー側のコードを参照するには、working/main.py を確認します。

トップページ: /

このページは、ウェブサイトを初めて開いたときに表示されます。ユーザーはログアウトしている状態であり、登録フォームが表示されます。ユーザーがログインしている場合は、/main にリダイレクトされます。

登録エンドポイント: /register

トップページ(/)でフォームに入力して [Register] ボタンを選択すると、POST リクエストがこのエンドポイントに送られます。処理が成功した場合はメインページ(/main)にリダイレクトされ、失敗した場合はトップページ(/)に戻ります。

ログイン ページ: /signin

このページには、トップページ(/)の右上隅の [Sign In] ボタンをタップしてアクセスします。

ID とパスワードによるログイン エンドポイント: /auth/password

フォームに入力して、ログイン ページ(/signin)の [Sign In] ボタンを選択すると、POST リクエストがこのエンドポイントに送信されます。処理が成功した場合は /main にリダイレクトされ、失敗した場合は /signin に戻ります。

Google Sign-In エンドポイント: /auth/google

トップページまたはログイン ページにある [Sign In with Google] ボタンをタップすると、Google アカウントを使用してユーザーがログインできるウィンドウがポップアップ表示されます。操作を続行すると、POST リクエストがこのエンドポイントに送信され、処理が成功した場合は /main にリダイレクトされ、失敗した場合は / or /signin に戻ります。この POST リクエストには、ユーザーが Google に実際にログインしているかどうかを確認するための id_token が含まれます。

メインページ: /main

ログインすると、このページにアクセスできます。

ログアウト エンドポイント: /signout

メインページで [Sign Out] ボタンをタップすると、GET リクエストがこのエンドポイントに送信されます。ユーザーはログアウトされ、/ にリダイレクトされます。

登録解除エンドポイント: /unregister

メインページで [Unregister] ボタンをタップすると、POST リクエストがこのエンドポイントに送信されます。登録解除するユーザーは、処理が成功した場合は / にリダイレクトされ、失敗した場合は /main に戻ります。

このステップでは、ユーザー登録時に認証情報を保存する機能を実装します。

登録フォームの送信をキャプチャし、認証情報を保存する

元の登録コードは、POST を使用してフォームを /register に送信します。このフォームには、名前、メールアドレス、パスワードが含まれています。

登録成功のレスポンスを受け取ってからこれらの認証情報を保存したいので、このフォーム送信を AJAX 呼び出しに変えましょう。

static/scripts ディレクトリに index.js という名前のファイルを作成します。

登録フォームの送信イベントを取得します。form には regForm の ID が保持されています。デフォルトの動作を阻止し、fetch() を使用してフォームを POST で送信する必要があります。リクエストの一環で Cookie がブラウザから送信されるように、credentials: 'include' は必ず含めてください。

var regForm = document.querySelector('#regForm');
regForm.addEventListener('submit', function(e) {
  e.preventDefault();

  fetch('/register', {
    method: 'POST',
    credentials: 'include',
    body: new FormData(regForm)
  }).then(function(res) {
    // fetch successful
  }, function() {
    app.fire('show-toast', {
      text: 'Registration failed'
    });
  });
});

エラー メッセージは、Polymer を使用してトーストとして表示されます。これは、ベースコードに実装されています。上の例のように app.fire('show-toast') を呼び出します。

サーバーが応答すると、fetch() が解決されます。HTTP ステータス コードが 200 だった場合は、navigator.credentials.store() を呼び出して認証情報を保存します。PasswordCredential オブジェクトは POST に使用した form 要素を使用してインスタンス化できます。認証情報の保存が完了したら、メインページにリダイレクトします。

    if (res.status === 200) {
      if (navigator.credentials) {
        var cred = new PasswordCredential(regForm);
        navigator.credentials.store(cred)
        .then(function() {
          location.href = '/main?quote=You are registered';
        });
      } else {
        location.href = '/main?quote=You are registered';
      }
    } else {
      app.fire('show-toast', {
        text: 'Registration failed'
      });
    }

上記のコードをまとめると、次のようになります。このコードは index.js にコピーできます。

var regForm = document.querySelector('#regForm');
regForm.addEventListener('submit', function(e) {
  e.preventDefault();

  fetch('/register', {
    method: 'POST',
    credentials: 'include',
    body: new FormData(regForm)
  }).then(function(res) {
    if (res.status === 200) {
      if (navigator.credentials) {
        var cred = new PasswordCredential(regForm);
        navigator.credentials.store(cred)
        .then(function() {
          location.href = '/main?quote=You are registered';
        });
      } else {
        location.href = '/main?quote=You are registered';
      }
    } else {
      app.fire('show-toast', {
        text: 'Registration failed'
      });
    }
  }, function() {
    app.fire('show-toast', {
      text: 'Registration failed'
    });
  });
});

autocomplete 属性を追加する

前のセクションでは form 要素を使用して PasswordCredential オブジェクトをインスタンス化しましたが、この方法では ID、パスワード、名前のマッピングが必要です。このマッピングはセマンティクスを使用して処理できます。

templates/index.htmlform 要素内の各 input 要素に autocomplete 属性を追加します。

<input id="csrf_token" type="hidden" name="csrf_token" value="{{csrf_token}}" />
<paper-input id="name" name="name" label="Whatever name" autocomplete="name"
  pattern=".{1,32}" error-message="4-32 letters please" auto-validate required>
  </paper-input>
<paper-input type="email" id="email" name="email" label="Fake email address"
  autocomplete="username" error-message="at least email format, please" auto-validate
  required></paper-input>
<paper-input id="password" type="password" name="password" label="Bogus password"
  autocomplete="new-password" pattern=".{4,32}" error-message="4-32 letters please"
  auto-validate required></paper-input>

HTTP ステータス コードで応答する

サーバーでの AJAX 呼び出しの処理には、リダイレクトではなく HTTP ステータス コードの応答を使用します。main.py を開き、次の行を編集します。

233 行目で、メールまたはパスワードが指定されていなかった場合 400 Bad Request を返します。

return make_response('Bad Request', 400)

254 行目で、登録が成功した場合 200 OK を返します。

return make_response('Registered', 200)

index.js を HTML に追加する

最後に、templates/layout.htmlindex.js を読み込むための script タグを追加します。

その他のスクリプト タグの下に来るように、HTML の最後に次のコードを追加します。

{% if path == '/' %}
<script src="scripts/index.js"></script>
{% endif %}

試してみる

これで、ユーザー登録時の認証情報の保存に必要な処理がすべて実装されました。自分で登録を試し、パスワードの保存ダイアログがポップアップで表示されるかどうかを確かめてみましょう。

期待どおりに動作しない場合は、step01 ディレクトリを確認して、間違いを確認してください。

パスワードの保存を促すプロンプトが表示されない場合は、chrome://settings からユーザー設定が適切に設定されていることを改めて確認します。

このステップでは、ユーザーのログイン時に認証情報を保存する機能を実装します。

ログイン フォームの送信をキャプチャし、認証情報を保存する

static/scripts ディレクトリに signin.js を作成して、コードを実装します。ユーザー登録時の処理とは、次の点が異なります。

var form = document.querySelector('#form');
form.addEventListener('submit', function(e) {
  e.preventDefault();

  fetch('/auth/password', {
    method: 'POST',
    credentials: 'include',
    body: new FormData(form)
  }).then(function(res) {
    if (res.status === 200) {
      if (navigator.credentials) {
        var cred = new PasswordCredential(form);
        navigator.credentials.store(cred)
        .then(function() {
          location.href = '/main?quote=You are signed in';
        });
      } else {
        location.href = '/main?quote=You are signed in';
      }
    } else {
      app.fire('show-toast', {
        text: 'Authentication failed'
      });
    }
  }, function() {
    app.fire('show-toast', {
      text: 'Authentication failed'
    });
  });
});

autocomplete 属性を追加する

templates/signin.html を変更して、autocomplete 属性を追加します。これはログインなので、パスワード マッピングに new-password ではなく current-password を使用しています。

<input id="csrf_token" type="hidden" name="csrf_token" value="{{csrf_token}}" />
<paper-input type="email" id="lemail" name="email" autocomplete="username"
  label="email" error-message="at least email format, please" auto-validate>
  </paper-input>
<paper-input id="lpassword" type="password" name="password"
  autocomplete="current-password" label="password" pattern=".{4,32}" 
  error-message="4-32 letters please" auto-validate required>
  </paper-input>

HTTP ステータス コードで応答する

main.py を変更して、サーバーが HTTP ステータス コードで応答することで AJAX 呼び出しを処理できるようにします。

159 行目で、メールまたはパスワードが指定されていなかった場合に 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

166 行目で、登録されているアカウントが見つからなかった場合に 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

172 行目で、アカウントが空だった場合に 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

176 行目で、パスワードが一致しなかった場合に 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

181 行目で、認証が成功した場合に 200 OK を返します。

return make_response('Authenticated', 200)

signin.js を HTML に追加する

今度は、singin.js を読み込む script タグを templates/layout.html に追加しましょう。

その他のスクリプト タグの下に来るように、HTML の最後に次のコードを追加します。

{% if path == '/signin' %}
<script src="scripts/signin.js"></script>
{% endif %}

試してみる

これで、ユーザー ログイン時に認証情報を保存する機能を実装できました。既に保存済みの認証情報がある場合は、アドレスバーで鍵のアイコンをクリックするか chrome://settings/passwords を指定して、認証情報を削除します。

削除できたら、登録している ID とパスワードの組み合わせを使用して、/signin からのログインを試します。認証が成功した場合のみ、認証情報が保存されます。フォームを基にして認証情報を保存する方法では、このように処理することはできません。

期待どおりに動作しない場合は、step02 ディレクトリを確認して、間違いを確認してください。

Credential Management API を使用する醍醐味は、自動ログインにあります。パスワードの入力なしで、ユーザーはウェブサイトに自動的にログインできます。モバイル端末を操作していてタッチスクリーンでの入力が面倒な場合に、この機能は特に便利です。

autoSignIn() 関数を定義する

自動ログイン機能の中心になるのは navigator.credentials.get() API です。認証情報を取得して、サーバーに POST 送信する関数を定義しましょう。

static/scripts ディレクトリに auto.js を作成し、Promise を返す autoSignIn() を定義します。

var autoSignIn = function() {
  if (navigator.credentials) {
    // auto sign in logic comes here
  } else {
    return Promise.reject();
  }
};

パスワードの認証情報を取得するには、オプションの password: true を指定して navigator.credentials.get() を呼び出します。呼び出すと、ユーザーがアカウントを選択できるように Account Chooser が表示されます。ユーザーがアカウントを選択すると、解決の関数に PasswordCredential インスタンスが渡されます。ユーザーが [Cancel] をタップしてこのステップをスキップすると、undefined が渡されます。

    return navigator.credentials.get({
      password: true
    }).then(function(cred) {
      if (cred) {
        // a PasswordCredential is passed
      } else {
        return Promise.reject();
      }
    });

パスワードの認証情報のオブジェクトは、fetch() のオプションの credentials の値として、サーバーに POST で送信できます。

fetch('/auth/password', {
  method: 'POST',
  credentials: cred
});

以上の注意点を踏まえると、取得のコードは次のようになります。

        var form = new FormData();
        var csrf_token = document.querySelector('#csrf_token').value;
        form.append('csrf_token', csrf_token);

        switch (cred.type) {
          case 'password':
            cred.additionalData = form;
            cred.idName = 'email';
            return fetch('/auth/password', {
              method: 'POST',
              credentials: cred
            });
        }
        return Promise.reject();

fetch() からのステータス コードを確認し、リクエストが成功した場合のみ解決します。

    }).then(function(res) {
      if (res.status === 200) {
        return Promise.resolve();
      } else {
        return Promise.reject();
      }
    });

最後に上記のすべてのコードをまとめましょう。

var autoSignIn = function() {
  if (navigator.credentials) {
    return navigator.credentials.get({
      password: true
    }).then(function(cred) {
      if (cred) {
        var form = new FormData();
        var csrf_token = document.querySelector('#csrf_token').value;
        form.append('csrf_token', csrf_token);

        switch (cred.type) {
          case 'password':
            cred.additionalData = form;
            cred.idName = 'email';
            return fetch('/auth/password', {
              method: 'POST',
              credentials: cred
            });
        }
        return Promise.reject();
      } else {
        return Promise.reject();
      }
    }).then(function(res) {
      if (res.status === 200) {
        return Promise.resolve();
      } else {
        return Promise.reject();
      }
    });
  } else {
    return Promise.reject();
  }
};

これで autoSignIn() を利用できるようになりました。この関数を使用する状況は 2 つあります。

1 つはユーザーが [Sign-In] をタップしたときです。Account Chooser が表示され、ログインに使用するアカウントをユーザーが選択できます。

2 つ目はユーザーがウェブサイトにアクセスしたときです。ブラウザーが対応していれば、ユーザーに明示的なアクションを求めることなく、自動的にログインします。自動ログインができない場合は、暗黙的に終了します。

Sign-In ボタンのタップで autoSignIn() を呼び出す

次の作業の準備として、templates/layout.html を開き、124 行目にある a タグを削除して JavaScript コードを挿入できるようにします。

<paper-button id="signin" raised>Sign In</paper-button>

static/scripts/index.js#signinclick に対するイベントリスナを作成し、autoSignIn() を呼び出します。解決された場合は、ユーザーが Account Chooser を使用したログインに成功したことを示します。ユーザーをメインページ(/main)にリダイレクトします。ログインできなかった場合はログイン ページ(/signin)に移動します。

var signin = document.querySelector('#signin');
signin.addEventListener('click', function() {
  autoSignIn()
  .then(function() {
    location.href = '/main?quote=You are signed in';
  }, function() {
    location.href = '/signin';
  });
});

ページへのアクセス時に autoSignIn() を呼び出す

再び static/scripts/auto.js を開き、autoSignIn() を呼び出すコードを追加します。解決されるとメインページに移動し、解決されなかった場合はログイン ページに移動します。

autoSignIn().then(function() {
  location.href = '/main?quote=You are automatically signed in';
}, function() {
  console.log('auto sign-in skipped');
});

auto.js を HTML に追加する

では templates/layout.htmlauto.js を追加しましょう。今度は条件指定は必要ありません。

<script src="scripts/auto.js"></script>

試してみる

これで自動ログイン機能を実装できました。ログアウトして、動作を確認してみましょう。

適切に処理されると初回のログイン時に Account Chooser 表示されます。表示されたら、ログインに使用するアカウントをクリックします。

しかし、ここでログアウトを試します。「Signing in as ...」というメッセージがポップアップ表示されましたか。

この場合は、強制的にログアウトが阻止されています。この動作は不適切ですが、バグではありません。どうしたらこの不適切な状態を解決できるでしょうか。

step03 ディレクトリで、上記とは異なる不適切な処理がないかどうかを確認してください。

"メディエーション モード" を使用して、予期せず再び自動ログインする動作を防ぎます。メディエーション モードを有効にすると、自動ログインが阻止されます。明示的にユーザーが操作した場合にのみ、ログインされます。

ユーザーのログアウト時にメディエーション モードを有効にする

次の作業の準備として、templates/layout.html を開き、126 行目にある a タグを削除して JavaScript コードを挿入できるようにします。

<paper-button id="signout" raised>Sign Out</paper-button>

static/scripts ディレクトリに main.js を作成し、#signoutclick に対するイベントリスナを定義して、navigator.credentials.requireUserMediation() を呼び出します。これでメディエーション モードが有効になります。解決されると、ユーザーは /signout に転送されます。

var signout = document.querySelector('#signout');
signout.addEventListener('click', function() {
  if (navigator.credentials) {
    navigator.credentials.requireUserMediation()
    .then(function() {
      location.href = '/signout';
    });
  } else {
    location.href = '/signout';
  }
});

ユーザーの登録解除時にメディエーション モードを有効にする

static/scripts/main.js によって、ユーザーが登録解除したときのフォーム送信を検出します。フォーム送信時のデフォルトの動作を阻止し、/unregister に対する fetch() を使用してフォームを POST で送信する必要があります。リクエストの一環で Cookie がブラウザから送信されるように、credentials: 'include' は必ず含めてください。

var unregForm = document.querySelector('#unregForm');
unregForm.addEventListener('submit', function(e) {
  e.preventDefault();

  fetch('/unregister', {
    method: 'POST',
    credentials: 'include',
    body: new FormData(unregForm)
  }).then(function(res) {
    if (res.status === 200) {
      // successful unregistration
    } else {
      app.fire('show-toast', {
        text: 'Unregister failed'
      });
    }
  }, function() {
    app.fire('show-toast', {
      text: 'Unregister failed'
    });
  });
});

登録解除が成功したら、navigator.credentials.requireUserMediation() を呼び出します。

      if (navigator.credentials) {
        navigator.credentials.requireUserMediation()
        .then(function() {
          location.href = '/?quote=You are unregistered';
        });
      } else {
        location.href = '/?quote=You are unregistered';
      }

上記のコードをまとめると次のようになります(これを static/scripts/main.js に追加します)。

var unregForm = document.querySelector('#unregForm');
unregForm.addEventListener('submit', function(e) {
  e.preventDefault();

  fetch('/unregister', {
    method: 'POST',
    credentials: 'include',
    body: new FormData(unregForm)
  }).then(function(res) {
    if (res.status === 200) {
      if (navigator.credentials) {
        navigator.credentials.requireUserMediation()
        .then(function() {
          location.href = '/?quote=You are unregistered';
        });
      } else {
        location.href = '/?quote=You are unregistered';
      }
    } else {
      app.fire('show-toast', {
        text: 'Unregister failed'
      });
    }
  }, function() {
    app.fire('show-toast', {
      text: 'Unregister failed'
    });
  });
});

登録解除用の HTTP ステータス コードで応答する

今度は、サーバーが HTTP ステータス コードを返して登録解除された AJAX 呼び出しを受け付け、リダイレクトしないようにします。main.py を開き、次のパートを編集します。

263 行目で、ID が指定されていなかった場合に 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

266 行目で、登録されているアカウントが見つからなかった場合に 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

271 行目で、検出したアカウントが空だった場合に 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

277 行目で、登録が成功した場合に 200 OK を返します。

return make_response('Unregistered', 200)

自動ログイン時にメディエーション ステータスを反映する

自動ログインに対するメディエーション モードを認識するためのフラグ navigator.credentials.get() があります。オプション unmediated: true を追加することでメディエーション モードに対応し、undefined が適切に解決されます。ユーザーがメディエーション モードであり、次の条件を満たしていない場合、アカウントを選択するようユーザーに求めません。

static/scripts/auto.js を開いて、unmediatedautoSignIn() の引数として、また、navigator.credentials.get() のオプションとして追加します。

var autoSignIn = function(unmediated) {
  if (navigator.credentials) {
    return navigator.credentials.get({
      password: true,
      unmediated: unmediated
    })
    .then(function(cred) {
      if (cred) {

ランディング ページの autoSignIn()true を使用します。

autoSignIn(true).then(function() {
  location.href = '/main?quote=You are automatically signed in';
}, function() {
  console.log('auto sign-in skipped');
});

static/scripts/index.js を開き、autoSignIn() の引数に false を使用します。

var signin = document.querySelector('#signin');
signin.addEventListener('click', function() {
  autoSignIn(false)
  .then(function() {
    location.href = '/main?quote=You are signed in';
  }, function() {
    location.href = '/signin';
  });
});

main.js を HTML に追加する

では、main.jstemplates/layout.html に追加しましょう。

{% if path == '/main' %}
<script src="scripts/main.js"></script>
{% endif %}

試してみる

以上でメディエーション モードを有効にする機能が実装されました。これで、予期せず再び自動ログインすることなく、ログアウトできるようになりました。

期待どおりに動作しない場合は、step04 ディレクトリを確認して、間違いを確認してください。

最後のステップでは、Google Sign-In のフェデレーション サポートを追加します。

ここまでで、ID とパスワードを使用する場合は、問題なくログインとログアウトができるようになりましたが、フェデレーションには対応していません。最後に、Google Sign-In で Credential Management API を使用できるようにして、フェデレーション アカウントを使用した自動ログインを有効にしましょう。

Google Sign-In を使用したログイン時に認証情報を保存する

Google のフェデレーション機能はこのコードラボの範囲外ですが、便宜的に Google Sign-In JavaScript ライブラリを使用します。ユーザーが Google アカウントでのログインを試みると、このライブラリによってウィンドウがポップアップで表示されます。次に、認証と承認を経て、JavaScript によってウィンドウにアカウント情報が設定されて閉じられます。

これらのステップに必要なコードのほとんどは static/scripts/federation.js に定義されているので心配は不要です。gSignIn() を呼び出して、GoogleUser オブジェクトを使用して解決する Promise を受け取ることができます。

static/scripts/app.js を開きます。認証済みの Google アカウントを確認するために、既存のコードでは form 要素が構築され、この要素がサーバーに送信されます。このリクエストには、id_token(および、前述の csrf_token)が含まれます。

ここでは AJAX 呼び出しを使用して id_token を送信します。

Google による認証が正常に完了すると、GoogleUser オブジェクトを使用して Promise が解決されます。このオブジェクト内の id_token を(csrf_token と併せて)サーバーに送信し、こちらでの認証を処理できます。

  gSignIn()
  .then(function(googleUser) {
    // Successful Google Sign-In authentication
  }).then(function() {
    location.href = '/main?quote=You are signed in with Google SignIn';
  }, function() {
    app.fire('show-toast', {
      text: 'Google Sign-In failed'
    });
  });

FormData を使用して POST 送信するデータを作成ます。id_tokencsrf_token を追加します。

    var form = new FormData();
    form.append('id_token', googleUser.getAuthResponse().id_token);
    form.append('csrf_token', document.querySelector('#csrf_token').value);

fetch() を使用して FormData/auth/google に POST で送信します。リクエストの一環で Cookie がブラウザから送信されるように、credentials: 'include' は必ず含めてください。

    return fetch('/auth/google', {
      method: 'POST',
      credentials: 'include',
      body: form
    }).then(function(res) {
      // fetch successful
    });

サーバーが応答すると、fetch() が解決されます。HTTP ステータス コードが 200 だった場合は、navigator.credentials.store() を呼び出して認証情報を保存します。FederatedCredential オブジェクトは個別にパラメータを設定してインスタンス化できます。これらのパラメータを取得するには、GoogleUser オブジェクトを使用します。Promise を返すことで、解決した場合はユーザーをメインページ(/main)にリダイレクトし、解決できなかった場合はエラーを表示できます。

      if (res.status === 200) {
        if (navigator.credentials) {
          var profile = googleUser.getBasicProfile();
          var cred = new FederatedCredential({
            id: profile.getEmail(),
            name: profile.getName(),
            iconURL: profile.getImageUrl(),
            provider: GOOGLE_SIGNIN
          });
          return navigator.credentials.store(cred);
        } else {
          return Promise.resolve();
        }
      } else {
        return Promise.reject();
      }

上記のコードをまとめると、次のようになります。

var gsignin = document.querySelector('#gsignin');
gsignin.addEventListener('click', function() {
  gSignIn()
  .then(function(googleUser) {
    // Now user is successfully authenticated with Google.
    // Send ID Token to the server to authenticate with our server.
    var form = new FormData();
    form.append('id_token', googleUser.getAuthResponse().id_token);
    form.append('csrf_token', document.querySelector('#csrf_token').value);

    return fetch('/auth/google', {
      method: 'POST',
      credentials: 'include',
      body: form
    }).then(function(res) {
      if (res.status === 200) {
        if (navigator.credentials) {
          var profile = googleUser.getBasicProfile();
          var cred = new FederatedCredential({
            id: profile.getEmail(),
            name: profile.getName(),
            iconURL: profile.getImageUrl(),
            provider: GOOGLE_SIGNIN
          });
          return navigator.credentials.store(cred);
        } else {
          return Promise.resolve();
        }
      } else {
        return Promise.reject();
      }
    });
  }).then(function() {
    location.href = '/main?quote=You are signed in with Google SignIn';
  }, function() {
    app.fire('show-toast', {
      text: 'Google Sign-In failed'
    });
  });
});

この場合も、main.py を変更して、サーバーが HTTP ステータス コードで応答することで AJAX 呼び出しを処理できるようにします。次のパートを編集します。

195 行目で、id_token が確認できなかった場合 401 Unauthorized を返します。

return make_response('Authentication failed', 401)

218 行目で、Google アカウントを使用したログインが成功した場合 200 OK を返します。

return make_response('Authenticated', 200)

Google Sign-In が初期化されるまで autoSignIn() の処理を先送りする

これで Google Sign-In を使用した自動サインインの有効化に必要なコードのほとんどが実装されました。ただし、ウェブサイトにアクセスした時点で Google Sign-In が初期化されていないために、autoSignIn() がエラーになる可能性が高くあります。初期化は非同期に行われるため、初期化が完了するまで autoSignIn() は待機する必要があります。

これを解決するため、static/scripts/federation.js を開き、Google Sign-In の初期化によって Promise が返されるようにします。

// Initialize Google Sign-In
var googleAuthReady = (function() {
  return new Promise(function(resolve) {
    gapi.load('auth2', function() {
      gapi.auth2.init().then(function() {
        return resolve();
      });
    });
  });
})();

googleAuthReady 変数を使用して、初期化が完了するまで autoSignIn() を先送りできます。static/scripts/auto.js を開き、autoSignIn() への呼び出しを次のコードで置き換えます。

googleAuthReady.then(function() {
  return autoSignIn(true);
}).then(function() {
  location.href = '/main?quote=You are automatically signed in';
}, function() {
  console.log('auto sign-in skipped');
});

自動ログインのオプションの 1 つとしてフェデレーション アカウントを有効にする

最後に、ユーザーがウェブサイトにアクセスしたときと、[Sign-In] をタップしたときのいずれでも、フェデレーション アカウントをログイン オプションの 1 つとして使用できるようにします。

static/scripts/auto.js を開き、navigator.credentials.get() のオプションとして federated を追加し、これに GOOGLE_SIGNIN を含む配列を渡します。GOOGLE_SIGNINstatic/scripts/federation.js に定義します。

  if (navigator.credentials) {
    return navigator.credentials.get({
      password: true,
      federated: {
        providers: [ GOOGLE_SIGNIN ]
      },
      unmediated: unmediated
    }).then(function(cred) {
      if (cred) {

これでブラウザからユーザーに対して Account Chooser が表示されたときに、フェデレーション アカウントがオプションとして表示できるようになります。ユーザーがフェデレーション アカウントを選択すると、解決する認証情報の種類は federated になります。また、認証オブジェクトの .provider を確認し、プロバイダーによっても切り替えます。

フェデレーションの最初のステップでは、関連ライブラリを呼び出します。たとえばプロバイダが Google であれば、Google Sign-In ライブラリを使用します。

幸い既に gSignIn() はあるので、以前と同様にこれを呼び出して、結果の id_token をサーバーに POST 送信します。

また、前のステップで作成し、既に CSRF トークンが設定されている FormData を再利用することもできます。

        switch (cred.type) {
          case 'password':
            cred.additionalData = form;
            cred.idName = 'email';
            return fetch('/auth/password', {
              method: 'POST',
              credentials: cred
            });
          case 'federated':
            switch (cred.provider) {
              case GOOGLE_SIGNIN:
                return gSignIn(cred.id)
                .then(function(googleUser) {
                  var id_token = googleUser.getAuthResponse().id_token;
                  form.append('id_token', id_token);
                  return fetch('/auth/google', {
                    method: 'POST',
                    credentials: 'include',
                    body: form
                  });
                });
            }
        }

試してみる

これで、Credential Management API を使用する自動ログインを実装した、稼働可能なウェブサイトを作成できました。

期待どおりに動作しない場合は、step05 ディレクトリを確認して、間違いを確認してください。

以上が皆様の開発にお役に立つことを願っています。

これでウェブサイトにユーザーは自動でログインできるようになりました。

これまでの学習内容

次のステップ

さらに詳しく