Search code examples
typescriptoauth-2.0pkce

Oauth2 and PKCE: Code verifier is invalid


I am making a client to authorize via OAuth2 but I keep on getting an "Code verifier is invalid" error.

async function encode(input: string): Promise<string> {
  return btoa(string).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}

async function digestMessage(message) {
  const msgUint8 = new TextEncoder().encode(message);                           // encode as (utf-8) Uint8Array
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);           // hash the message
  const hashArray = Array.from(new Uint8Array(hashBuffer));                     // convert buffer to byte array
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
  return hashHex;
}

function generateRandomString (len?: number) {
  var arr = new Uint8Array((len || 40) / 2)
  window.crypto.getRandomValues(arr)
  return Array.from(arr, dec2hex).join('')
}

async function pkceChallengeFromVerifier(v: string) {
  let hash = await digestMessage(v)
  let challenge = await encode(hash)
  return challenge
}

No matter what verifier I put in, it keeps on giving me the error.


Solution

  • Do not convert it into a string! That changes the base64 output value entirely! Instead, base64 encode the raw hash buffer from crypto.subtle.digest.

    Using this solution for getting the base64 of an array buffer:

    async function hashSHA256(str: string): Promise<ArrayBuffer> {
      const utf8 = new TextEncoder().encode(str);
      const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
      return hashBuffer;
    }
    
    async function base64_arraybuffer(data: ArrayBuffer): Promise<string> {
      // Use a FileReader to generate a base64 data URI
      const base64url: string = await new Promise((r) => {
          const reader = new FileReader()
          reader.onload = () => r(reader.result as string)
          reader.readAsDataURL(new Blob([data]))
      })
    
      /*
      The result looks like
      "data:application/octet-stream;base64,<your base64 data>",
      so we split off the beginning:
      */
      return base64url.split(",", 2)[1]
    }
    
    
    async function encode(input: ArrayBuffer): Promise<string> {
      return (await base64_arraybuffer(input)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
    }
    
    async function pkceChallengeFromVerifier(v: string) {
      let hash = await hashSHA256(v)
      let challenge = encode(hash)
      return challenge
    }
    

    Also make sure that the input verifier string is within the length range of 43 and 128 characters long.