2021.04.08

WebAuthn導入編

こんにちは。次世代システム研究室のT.Tです。

2021年3月30日に行われた社内の研究発表会の内容をご紹介します。
現在開発運用に携わっている認証サービスでは生体認証のリリースに向けて開発環境での動作検証中です。研究発表ではその開発過程で得られたノウハウを元に、WebAuthnの基本的な仕様と利用方法に触れ、PHPのWebAuthnライブラリであるwebauthn-libを利用したサーバーサイドの実装例を紹介しました。また、WebAuthnによる生体認証ではパスワード認証と比較してセキュリティもユーザビリティも共に向上すると考えられますが、それでも完全な認証セキュリティを提供することはできません。後半ではWebAuthn導入後に考慮すべき脆弱性対策についてご紹介しています。
研究発表ではWebAuthn APIの実装のイメージのみをお伝えしたので、本ブログでサンプルコードを補足します。



スライドの概要

  1. WebAuthn APIの説明
    1. navigator.credentials.create
    2. navigator.credentials.get
  2. WebAuthnを導入することによるメリット
    1. 認証のセキュリティとユーザビリティの向上
    2. 認証をユーザー責任の世界からセキュリティコントロールの世界へ
    3. IdPでWebAuthnを導入して付加価値向上
  3. WebAuthn導入の注意点
    1. 様々な利用シーンに対応するためにSMS認証やOTP等との連携
    2. WebAuthnとしてのセキュリティ対策
    3. WebAuthnの脆弱性も視野に入れた開発運用

補足資料

navigator.credentials.create

YubiKeyで認証情報ソースを生成するサンプルコードです。

<script>
function arrayToBase64String(a) {
    return btoa(String.fromCharCode(...a));
}

function base64url2base64(input) {
    input = input
        .replace(/=/g, "")
        .replace(/-/g, '+')
        .replace(/_/g, '/');

    const pad = input.length % 4;
    if (pad) {
        if(pad === 1) {
            throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
        }
        input += new Array(5-pad).join('=');
    }

    return input;
}

const publicKey = JSON.parse(
'{ \
  "rp": { \
    "name": "Webauthn Server", \
    "id":"local.gmo.jp"}, \
    "pubKeyCredParams": [{"type":"public-key","alg":-7}], \
    "challenge":"H9Ns1jvc9bbXWPaN-vHUjq2pjZB2HPx-VUkebUM_1wM", \
    "attestation":"none", \
    "user": { \
      "name":"john.doe", \
      "id":"ZWE0ZTdiNTUtZDhkMC00YzdlLWJiZmEtNzhjYTk2ZWM1NzRj", \
      "displayName":"John Doe" \
    }, \
    "authenticatorSelection": { \
      "requireResidentKey":false, \
      "userVerification":"preferred" \
    }, \
  "timeout": 60000 \
}');

publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);});
publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), function(c){return c.charCodeAt(0);});

navigator.credentials.create({ 'publicKey': publicKey })
  .then(function(data){
    const publicKeyCredential = {
      id: data.id,
      type: data.type,
      rawId: arrayToBase64String(new Uint8Array(data.rawId)),
      response: {
        clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
        attestationObject: arrayToBase64String(new Uint8Array(data.response.attestationObject))
      }
    };
    alert(JSON.stringify(publicKeyCredential));
  })
  .catch(function(error){
    console.log(error);
  });
</script>

navigator.credentials.get

createを実行して得られるdata.idが認証情報ソースのIDになっているので、getのオプションのallowCredentialsに指定する認証情報ソースのデスクリプタのidに指定します。

<script>
function arrayToBase64String(a) {
    return btoa(String.fromCharCode(...a));
}

function base64url2base64(input) {
    input = input
        .replace(/=/g, "")
        .replace(/-/g, '+')
        .replace(/_/g, '/');

    const pad = input.length % 4;
    if (pad) {
        if(pad === 1) {
            throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
        }
        input += new Array(5-pad).join('=');
    }

    return input;
}

const publicKey = JSON.parse(
'{ \
  "challenge": "THRb7S2ZhDdchfBydOGixw1AL2ikXxB2vG0m91eAKdU", \
  "rpId": "local.gmo.jp", \
  "userVerification": "preferred", \
  "allowCredentials":[ \
    { \
      "type": "public-key", \
      "id": "X3JbwZfPrqhr_JMDdepBRewznNGETkvS8-bO_ueMNjMl3FK-SCWos-GZe2z6qO1NYcXpRBqWI-xGFtGo1widLQ", \ // createを実行して得られた認証情報ソースのID
      "transports": ["internal","usb"] \
    } \
  ], \
  "timeout":60000 \
}');

publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);});
publicKey.allowCredentials.forEach(function(credential) {
  credential.id = Uint8Array.from(window.atob(base64url2base64(credential.id)), function(c){return c.charCodeAt(0);});
});

navigator.credentials.get({ 'publicKey': publicKey })
  .then(function(data){
    const publicKeyCredential = {
      id: data.id,
      type: data.type,
      rawId: arrayToBase64String(new Uint8Array(data.rawId)),
      response: {
        authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
        clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
        signature: arrayToBase64String(new Uint8Array(data.response.signature)),
        userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null
      }
    };

    alert(JSON.stringify(publicKeyCredential));
  })
  .catch(function(error){
    console.log(error);
  });
</script>

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。

参考リンク

Pocket

関連記事