【JavaScript】code_verifierとcode_challengeを生成する方法!

OAuth 2.0のPKCE(Proof Key for Code Exchange)フローでは、code_verifiercode_challengeを用いたセキュアな認証が必要です。

この記事では、JavaScriptでcode_verifiercode_challengeを生成する方法について、以下の内容をサンプルコードを用いてわかりやすく解説します。

  • PKCEとは
  • code_verifierを生成する方法
    • cryptoを使わずにランダムな文字列を生成する方法
    • crypto.getRandomValues()を使用する方法(ブラウザ向け)
    • crypto.randomBytes()を使用する方法(Node.js向け)
  • code_challengeを生成する方法
    • crypto.subtle.digest()を使用する方法(ブラウザ向け)
    • crypto.createHash()を使用する方法(Node.js向け)

PKCEとは

PKCE(ピクシー、Proof Key for Code Exchange)は、OAuth 2.0の認可コードフローにおいて、クライアントアプリのセキュリティを向上させるための仕組みです。

あわせて読みたい

PKCE』については下記の記事で詳しく説明しています。興味のある方は下記のリンクからぜひチェックをしてみてください。

通常の認可コードフローでは、認可コード(authorization code)が悪意のある第三者に盗まれると、不正にアクセストークンを取得されるリスクがあります。しかし、PKCEを利用することで、このリスクを軽減できます。PKCEでは以下の2つの値を使用します。

  • code_verifier
    • 文字数:43~128文字
    • 使用可能な文字:半角英数字(azAZ09)と記号(-_~)からなるランダムな文字列
  • code_challenge
    • code_verifierをSHA-256でハッシュ化し、Base64URLエンコードしたもの

ではこれから、code_verifiercode_challengeを生成する方法を順番に解説します。

code_verifierを生成する方法

code_verifierを生成する方法を解説します。

cryptoを使わずにランダムな文字列を生成する方法

cryptoモジュールを使わずに、JavaScriptのMath.random()を使用してcode_verifierを生成するサンプルコードを以下に示します。

function generateCodeVerifier(codeVerifierLength = 43) {
  // codeVerifierの長さは(最低43文字、最大128文字)なので、この範囲にない場合にはエラーをスローする。
  if (codeVerifierLength < 43 || codeVerifierLength > 128) {
    throw new Error('CodeVerifierの文字数は43文字〜128文字です。');
  }

  // 使用可能な文字 (URLセーフでBase64に変換可能な値)
  const codeVerifierCharSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';

  let codeVerifier = '';

  for (let i = 0; i < codeVerifierLength; i++) {
    let randomIndex = Math.floor(Math.random() * codeVerifierCharSet.length);
    codeVerifier += codeVerifierCharSet[randomIndex];
  }

  return codeVerifier;
}

const codeVerifier = generateCodeVerifier();
console.log('codeVerifier: ', codeVerifier);
console.log('Length:', codeVerifier.length);

JavaScriptのMath.random()を使用しているため、ブラウザ・Node.jsのどちらでも動作します。

Math.random()は暗号学的に安全ではないため、高セキュリティが求められる場合には、後ほど説明するcrypto.getRandomValues()crypto.randomBytes()を用いてcode_verifierを生成することを推奨します。

crypto.getRandomValues()を使用する方法(ブラウザ向け)

Web Crypto APIのcrypto.getRandomValues()メソッドを用いることで、より安全にcode_verifierを生成することができます。サンプルコードを以下に示します。

function generateCodeVerifier(byteLength = 32) {
  const byteArray = new Uint8Array(byteLength);
  crypto.getRandomValues(byteArray);
  const base64String = btoa(String.fromCharCode(...byteArray));
  return base64String
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

const codeVerifier = generateCodeVerifier(); // 32バイト(約48文字)
console.log('codeVerifier:', codeVerifier);
console.log('Length:', codeVerifier.length);

Web Crypto APIのcrypto.getRandomValues()メソッドを使用しているので、ブラウザでのみ動作します。Node.jsでは動作しません。crypto.getRandomValues()メソッドを使ってランダムなバイト列を生成しています。

crypto.randomBytes()を使用する方法(Node.js向け)

Node.js環境では、crypto.randomBytes()メソッドを用いることで、より安全にcode_verifierを生成することができます。サンプルコードを以下に示します。

const crypto = require('crypto');

function generateCodeVerifierNode(byteLength = 32) {
  return crypto
    .randomBytes(byteLength)
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

const codeVerifierNode = generateCodeVerifierNode(); // 32バイト(約48文字)
console.log('codeVerifierNode:', codeVerifierNode);
console.log('Length:', codeVerifierNode.length);

crypto.randomBytes()メソッドを用いているため、Node.jsのみで動作します。ブラウザでは動作しません。crypto.randomBytes()メソッドを使ってランダムなバイト列を生成しています。

code_challengeを生成する方法

code_verifierを元にしてcode_challengeを生成する方法を解説します。

crypto.subtle.digest()を使用する方法(ブラウザ向け)

Web Crypto APIのcrypto.subtle.digest()メソッドを用いることで、code_challengeを生成することができます。サンプルコードを以下に示します。

async function generateCodeChallenge(codeVerifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);

  // SHA256でハッシュ化
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);

  // ArrayBufferをUint8Arrayに変換
  const hashArray = Array.from(new Uint8Array(hashBuffer));

  // Uint8ArrayをBase64文字列に変換
  const hashBase64 = btoa(String.fromCharCode(...hashArray));

  // Base64文字列をURL安全なBase64形式に変換
  const base64Url = hashBase64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

  return base64Url;
}
generateCodeChallenge(codeVerifier).then((codeChallenge) =>
  console.log('codeChallenge: ', codeChallenge)
);

Web Crypto APIのcrypto.subtle.digest()メソッドを使用しているので、ブラウザでのみ動作します。Node.jsでは動作しません。crypto.subtle.digest('SHA-256', data)でSHA-256ハッシュを生成してます。

crypto.createHash()を使用する方法(Node.js向け)

Node.js環境では、crypto.createHash()メソッドを用いることで、code_challengeを生成することができます。サンプルコードを以下に示します。

const crypto = require('crypto');

function generateCodeChallengeNode(codeVerifier) {
// SHA256でハッシュ化
  const hash = crypto.createHash('sha256').update(codeVerifier).digest();
  return hash.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

const codeChallengeNode = generateCodeChallengeNode(codeVerifier);

console.log('codeChallengeNode:', codeChallengeNode);

crypto.createHash()メソッドを用いているため、Node.jsのみで動作します。ブラウザでは動作しません。crypto.createHash('sha256')でSHA-256ハッシュを生成してます。

本記事のまとめ

この記事では、JavaScriptでcode_verifiercode_challengeを生成する方法について、以下の内容を説明しました。

  • PKCEとは
  • code_verifierを生成する方法
    • cryptoを使わずにランダムな文字列を生成する方法
    • crypto.getRandomValues()を使用する方法(ブラウザ向け)
    • crypto.randomBytes()を使用する方法(Node.js向け)
  • code_challengeを生成する方法
    • crypto.subtle.digest()を使用する方法(ブラウザ向け)
    • crypto.createHash()を使用する方法(Node.js向け)

お読み頂きありがとうございました。