Creating a code verifier and challenge for PKCE auth on Spotify API in ReactJS
Asked Answered
D

4

8

I'm trying to add Spotify auth to my single page react application following the doc from their api.

So far this is how I generate the codes based on solutions I found online:

const generateVerifier = () => {
    return crypto.randomBytes(64).toString('hex');
}

const getChallenge = verifier => {
    return crypto.createHash('sha256')
        .update(verifier)
        .digest('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '')
}

An example of a pair of codes I created using that technique:

  • verifier: e8c3745e93a9c25ce5c2653ee36f5b4fa010b4f4df8dfbad7055f4d88551dd960fb5b7602cdfa61088951eac36429862946e86d20b15250a8f0159f1ad001605
  • challenge: CxF5ZvoXa6Cz6IcX3VyRHxMPRXYbv4PADxko3dwPF-I

An example of an old pair of codes I created:

  • verifier: 1jp6ku6-16xxjfi-1uteidc-9gjfso-1mcc0wn-tju0lh-tr2d8k-1auq4zk
  • challenge: SRvuz5GW2HhXzHs6b3O_wzJq4sWN0W2ma96QBx_Z77s

I then get a response from the API saying "code_verifier was incorrect." What am I doing wrong here?

Divorce answered 7/8, 2020 at 21:32 Comment(3)
Have you set your domain/website in the app setting?Carlyle
Yes @AshishDuklan, I get a response from the API saying "code_verifier was incorrect." Everything else is working fine.Divorce
Based on this tool to check your challenge and verifier it looks fine tonyxu-io.github.io/pkce-generator I suspect it's something elseVesture
P
24

Try following this guide for generating code for generating code challenge and verifier

Here are the important parts:

Generate Code Verifier

// GENERATING CODE VERIFIER
function dec2hex(dec) {
  return ("0" + dec.toString(16)).substr(-2);
}

function generateCodeVerifier() {
  var array = new Uint32Array(56 / 2);
  window.crypto.getRandomValues(array);
  return Array.from(array, dec2hex).join("");
}

Generate code challenge from code verifier

function sha256(plain) {
  // returns promise ArrayBuffer
  const encoder = new TextEncoder();
  const data = encoder.encode(plain);
  return window.crypto.subtle.digest("SHA-256", data);
}

function base64urlencode(a) {
  var str = "";
  var bytes = new Uint8Array(a);
  var len = bytes.byteLength;
  for (var i = 0; i < len; i++) {
    str += String.fromCharCode(bytes[i]);
  }
  return btoa(str)
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

async function generateCodeChallengeFromVerifier(v) {
  var hashed = await sha256(v);
  var base64encoded = base64urlencode(hashed);
  return base64encoded;
}

Here's a working example

You can also check the validity of the codes here

Primogeniture answered 10/8, 2020 at 8:21 Comment(3)
I ended up using the Crypto-JS library but will definitely try this instead, thank you!Divorce
Tested, it works with AAD OAuth 2.0 PKCELoudspeaker
I am geting the generatedCodeVerifier(), but how do you get the challengeFromVerifier(v) === returning as object[promise]. console.log("code_verifier = + generateCodeVerifier()); var v = gernerateCodeVerifier(); console.log("challenge = " + generateCodeChallengeFromVerifier(v)); // object[promise]Pushed
P
5

Fully working and verified example:

const {randomBytes, createHash} = require("node:crypto");
// OR: import {randomBytes, createHash} from "crypto";

function generatePKCEPair() {
    const NUM_OF_BYTES = 22; // Total of 44 characters (1 Bytes = 2 char) (standard states that: 43 chars <= verifier <= 128 chars)
    const HASH_ALG = "sha256";
    const randomVerifier = randomBytes(NUM_OF_BYTES).toString('hex')
    const hash = createHash(HASH_ALG).update(randomVerifier).digest('base64');
    const challenge = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // Clean base64 to make it URL safe
    return {verifier: randomVerifier, challenge}
}

Run example:

generatePKCEPair();
// Result:
{
  verifier: '3e2727957a1bd9f47b11ff347fca362b6060941decb4',
  challenge: '1SF5UEwYplIjmAwHUwcitzp9qz8zv98uYflt-tBmwLc'
}
Prussiate answered 4/12, 2022 at 21:54 Comment(0)
P
3

Here's a web worker compatible version that uses the latest (2024) APIs.

const generateRandomBase64String = async (length = 24) =>
  Buffer.from(crypto.getRandomValues(new Uint8Array(length))).toString(
    'base64url'
  );

const computeCodeChallengeFromVerifier = async (verifier: string) => {
  const hashedValue = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(verifier)
  );
  return Buffer.from(hashedValue).toString('base64url');
};

const isCodeVerifierValid = async (codeVerifier: string, codeChallenge: string) => 
  (await computeCodeChallengeFromVerifier(codeVerifier)) === codeChallenge;

Usage:

const codeVerifier = await generateRandomBase64String();
const codeChallenge = await computeCodeChallengeFromVerifier(codeVerifier);

if(await isCodeVerifierValid(codeVerifier, codeChallenge))
  console.log('verifier matches!')
Porphyry answered 14/1 at 4:15 Comment(0)
D
2

I took this snippet from the passport oauth2 library to generate code verifier and code challenge.

const code_verifier = base64url(crypto.pseudoRandomBytes(32));

const code_challenge = crypto
     .createHash("sha256")
     .update(code_verifier)
     .digest();
Declension answered 10/6, 2021 at 11:3 Comment(2)
This is good, but should source the reference which is here: github.com/jaredhanson/passport-oauth2/blob/master/lib/…Vesture
Why the number 32 for pseudoRandomBytes?Wilford

© 2022 - 2024 — McMap. All rights reserved.