index.node.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. let crypto;
  2. crypto =
  3. globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
  4. globalThis.crypto ?? // Node.js >18
  5. import("node:crypto").then(m => m.webcrypto); // Node.js <18 Non-REPL
  6. /**
  7. * Creates an array of length `size` of random bytes
  8. * @param size
  9. * @returns Array of random ints (0 to 255)
  10. */
  11. async function getRandomValues(size) {
  12. return (await crypto).getRandomValues(new Uint8Array(size));
  13. }
  14. /** Generate cryptographically strong random string
  15. * @param size The desired length of the string
  16. * @returns The random string
  17. */
  18. async function random(size) {
  19. const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
  20. const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
  21. let result = "";
  22. while (result.length < size) {
  23. const randomBytes = await getRandomValues(size - result.length);
  24. for (const randomByte of randomBytes) {
  25. if (randomByte < evenDistCutoff) {
  26. result += mask[randomByte % mask.length];
  27. }
  28. }
  29. }
  30. return result;
  31. }
  32. /** Generate a PKCE challenge verifier
  33. * @param length Length of the verifier
  34. * @returns A random verifier `length` characters long
  35. */
  36. async function generateVerifier(length) {
  37. return await random(length);
  38. }
  39. /** Generate a PKCE code challenge from a code verifier
  40. * @param code_verifier
  41. * @returns The base64 url encoded code challenge
  42. */
  43. export async function generateChallenge(code_verifier) {
  44. const buffer = await (await crypto).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
  45. // Generate base64url string
  46. // btoa is deprecated in Node.js but is used here for web browser compatibility
  47. // (which has no good replacement yet, see also https://github.com/whatwg/html/issues/6811)
  48. return btoa(String.fromCharCode(...new Uint8Array(buffer)))
  49. .replace(/\//g, '_')
  50. .replace(/\+/g, '-')
  51. .replace(/=/g, '');
  52. }
  53. /** Generate a PKCE challenge pair
  54. * @param length Length of the verifer (between 43-128). Defaults to 43.
  55. * @returns PKCE challenge pair
  56. */
  57. export default async function pkceChallenge(length) {
  58. if (!length)
  59. length = 43;
  60. if (length < 43 || length > 128) {
  61. throw `Expected a length between 43 and 128. Received ${length}.`;
  62. }
  63. const verifier = await generateVerifier(length);
  64. const challenge = await generateChallenge(verifier);
  65. return {
  66. code_verifier: verifier,
  67. code_challenge: challenge,
  68. };
  69. }
  70. /** Verify that a code_verifier produces the expected code challenge
  71. * @param code_verifier
  72. * @param expectedChallenge The code challenge to verify
  73. * @returns True if challenges are equal. False otherwise.
  74. */
  75. export async function verifyChallenge(code_verifier, expectedChallenge) {
  76. const actualChallenge = await generateChallenge(code_verifier);
  77. return actualChallenge === expectedChallenge;
  78. }