Chrome Dev Summit 2018 is happening now and streaming live on YouTube. Watch now.

Credential Management API の仕様が変更されました

このポストで説明している変更点のいくつかは Google I/O のセッション Secure and Seamless Sign-In: Keeping Users Engaged でも触れています :

Chrome 57

Chrome 57 における Credential Management API の重要な変更点は以下のとおりです。

クレデンシャルが異なるサブドメイン間で共有できるようになりました

Chrome は Credential Management API を使って、異なるサブドメインに保存されたクレデンシャルを取得できるようになりました。 例えば、パスワードが login.example.com に保存されている場合、www.example.com にあるスクリプトは、アカウントチューザーダイアログで表示するアカウントの一つとして利用することができます。

ユーザーがダイアログをタップしてクレデンシャルを選択した際、現在のオリジンに渡してコピーするためには、パスワードを明示的に navigator.credentials.store() で保存する必要があります。

一度保存してしまえば、以後そのパスワードは同じオリジン www.example.com に保存されたクレデンシャルとして扱われるようになります。

以下のスクリーンショットでは、login.aliexpress.com に保存されたクレデンシャルが m.aliexpress.com から見えており、ユーザーにより選択可能である状態を示しています。

Account chooser showing selected subdomain login details

Chrome 60

Chrome 60 の Credential Management API では重要な変更点がいくつかあります:

機能検知に注意が必要です

新しい Credential Management API が利用可能かどうかを調べるには、preventSilentAccess が存在するかどうかをチェックします。

if (navigator.credentials && navigator.credentials.preventSilentAccess) {
  // 新しい Credential Management API が利用可能
}

PasswordCredential オブジェクトがパスワードを含むようになります

Credential Management API はこれまでパスワードの扱いについて保守的なアプローチを取っていました。JavaScript からパスワードを隠蔽していたため、開発者は若干カスタマイズされた fetch() API を使って、直接 PasswordCredential オブジェクトをサーバーに送って、認証を行う必要がありました。

しかしこのアプローチでは、いくつもの制約がありました。 API を採用できない理由として頂いたフィードバックには下記のようなものがあります:

  • JSON オブジェクトの一部としてパスワードを送らなければならない。
  • サーバーにパスワードのハッシュ値を送らなければならない。

慎重なセキュリティ分析を行った結果、JavaScript からパスワードを隠蔽しても、すべてのアタックベクターに対して期待したほど成果を挙げられないということが判明しました。そして、我々は今回の変更を行うという結論に至ったのです。

新しい Credential Management API では、取得したクレデンシャルオブジェクトに生のパスワードが含まれるため、プレーンテキストとして扱うことができます。そのため開発者は、従来と同様の方法を使ってクレデンシャルをサーバーに送ることができます。

navigator.credentials.get({
  password: true,
  federated: {
    provider: [ 'https://accounts.google.com' ]
  },
  mediation: 'silent'
}).then(c => {
  if (c) {
    let form = new FormData();
    form.append('email', c.id);
    form.append('password', c.password);
    form.append('csrf_token', csrf_token);
    return fetch('/signin', {
      method: 'POST',
      credentials: 'include',
      body: form
    });
  } else {
    // ログインフォームにフォールバック
  }
}).then(res => {
  if (res.status === 200) {
    return res.json();
  } else {
    throw 'Auth failed';
  }
}).then(profile => {
  console.log('Auth succeeded', profile);
});

カスタマイズされた fetch 関数はまもなく使えなくなります

カスタマイズされた fetch() 関数を使っているかを検証するには、PasswordCredential オブジェクト、もしくは FederatedCredential オブジェクトを credentials プロパティの値として使っているかをご確認ください。例えば:

fetch('/signin', {
  method: 'POST',
  credentials: c
})

ひとつ前のサンプルコードのように、通常の fetch() 関数、もしくは XMLHttpRequest を使うことが推奨されます。

navigator.credentials.get() が enum の mediation を受け取るようになります

Chrome 60 まで navigator.credentials.get() はオプショナルな unmediated プロパティとして Boolean を受け取っていました。 例えば:

navigator.credentials.get({
  password: true,
  federated: {
    provider: [ 'https://accounts.google.com' ]
  },
  unmediated: true
}).then(c => {
  // Sign-in
});

unmediated: true とすることで、クレデンシャルを取得する際にブラウザがアカウントチューザーを表示するのを防ぐことができます。

このフラグは mediation に変更され、ユーザーの仲介は下記のような状況で発生するようになります:

  • ユーザーがログインするアカウントを選択する必要がある場合。
  • navigator.credentials.requireUseMediation() を呼び出した後にユーザーが明示的にログインしたい場合。

mediation の値として下記のいずれかを選びます:

mediation unmediated の等値 振る舞い
silent unmediated: true アカウントチューザーを表示せずにクレデンシャルを返す。
optional unmediated: false 前回 preventSilentAccess() が呼ばれている場合、アカウントチューザーを表示する。
required 新しい値 毎回アカウントチューザーを表示する。アカウントの切り替えに便利。

この例では、先程のフラグ unmediated: true の等値を使うことで、アカウントチューザーを表示することなくクレデンシャルを返します:

navigator.credentials.get({
  password: true,
  federated: {
    provider: [ 'https://accounts.google.com' ]
  },
  mediation: 'silent'
}).then(c => {
  // Sign-in
});

requireUserMediation()preventSilentAccess() に変更されます

get() の新しいオプション mediation に合わせ、navigator.credentials.requireUserMediation()navigator.credentials.preventSilentAccess() に変更されます。

このメソッドはアカウントチューザーを表示せずに(「ユーザーの仲介」とも呼ばれます)クレデンシャルを渡すことを防ぎます。 これを使うことで、ユーザーがログアウト後に戻ってきた時、自動ログインしないようにすることができます。

signoutUser();
if (navigator.credentials) {
  navigator.credentials.preventSilentAccess();
}

新しいメソッド navigator.credentials.create() を使って非同期にクレデンシャルオブジェクトを生成できるようになります

新しいメソッド navigator.credentials.create() を使って非同期にクレデンシャルオブジェクトを生成できるようになります。同期・非同期両方のアプローチを比較してみましょう。

PasswordCredential オブジェクトを生成する

同期的アプローチ
let c = new PasswordCredential(form);
非同期的アプローチ (new)
let c = await navigator.credentials.create({
  password: form
});

もしくは:

let c = await navigator.credentials.create({
  password: {
    id: id,
    password: password
  }
});

FederatedCredential オブジェクトを生成する

同期的アプローチ
let c = new FederatedCredential({
  id:       'agektmr',
  name:     'Eiji Kitamura',
  provider: 'https://accounts.google.com',
  iconURL:  'https://*****'
});
非同期的アプローチ (new)
let c = await navigator.credentials.create({
  federated: {
    id:       'agektmr',
    name:     'Eiji Kitamura',
    provider: 'https://accounts.google.com',
    iconURL:  'https://*****'
  }
});

マイグレーションガイド

既存の Credential Management API の実装をお持ちですか?こちらのマイグレーションガイド

を参考にして下さい。新しい API への対応方法をステップ・バイ・ステップでご紹介します。