flutterrestdartspotifypkce

Why does the Spotify Auth API return a 400 with "code_verifier was incorrect"?


I am working on a Flutter application that authenticates with Spotify, referencing the auth flow documentation from here. When implementing this flow, I am able to get an access code using the auth GET request with the information I've provided on my developer page (including a working redirect URL), but the POST for fetching a token returns a 400 with the error "code_verifier was incorrect". I would assume that the error has to do with the way these PKCE strings are being calculated, as the logging confirms that the one used to generate the challenge is also the one used in the POST request.

Here is the code that generates the challenge and the verifier:

  static void _createCodeVerifierAndChallenge() {
    final random = Random.secure();
    final codeVerifier = List<int>.generate(32, (_) => random.nextInt(256));
    _codeVerifier = base64UrlEncodeNoPadding(codeVerifier);
    _codeChallenge =
        base64UrlEncodeNoPadding((sha256.convert(codeVerifier).bytes));

    print('Code Verifier: $_codeVerifier'); // For troubleshooting
    print('Code Challenge: $_codeChallenge'); // For troubleshooting
  }

The base64UrlEncodeNoPadding function used above is defined as follows:

  static String base64UrlEncodeNoPadding(List<int> input) =>
      base64Url.encode(input).replaceAll('=', '');

Both of these values are stored as late static variables, and the verifier is passed from there into the body of a POST request to get the token. This returns the 400 error mentioned above, stating that the code_verifier was incorrect. I have verified that the value initially calculated is the same as the one passed to the POST request by doing a string comparison in a Dartpad.

For reference, the following are the two URLs I'm attempting:

//GET authorize
Uri.parse(
      'https://accounts.spotify.com/authorize'
      '?client_id=${Secrets.clientId}'
      '&response_type=code'
      '&redirect_uri=${Secrets.redirectUri}'
      '&code_challenge_method=S256'
      '&code_challenge=$_codeChallenge'
      '&scope=${Uri.encodeComponent('user-read-private user-read-email')}',
    )

//POST token
Uri.parse('https://accounts.spotify.com/api/token'),
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: {
        'client_id': Secrets.clientId,
        'grant_type': 'authorization_code',
        'code': authCode, // The authorization code I received
        'redirect_uri': Secrets.redirectUri,
        'code_verifier': _codeVerifier, // The late static variable
      },
)

NOTE: I have referenced this question, but it seems the fix in this case involved manually replacing certain characters in their challenge that base64UrlEncode should already be replacing. Even when only replacing the '=' character after base64UrlEncoding, the result is the same. For what it's worth, I have also tried manually replacing the other characters mentioned in this answer as well, with the same result.


Solution

  • This line is probably the reason. You are calculating the code challenge based on the pre-transformed value codeVerified, whereas you should use the base64-encoded value _codeVerified.

    // Incorrect
    _codeChallenge = base64UrlEncodeNoPadding((sha256.convert(codeVerifier).bytes));
    // Correct
    _codeChallenge = base64UrlEncodeNoPadding((sha256.convert(_codeVerifier).bytes));
    

    Let's rename the variables to make it easier to read

    static void _createCodeVerifierAndChallenge() {
        final random = Random.secure();
        final randomIntegers = List<int>.generate(32, (_) => random.nextInt(256));
        _codeVerifier = base64UrlEncodeNoPadding(randomIntegers);
        _codeChallenge =
            base64UrlEncodeNoPadding((sha256.convert(_codeVerifier).bytes));
    
        print('Code Verifier: $_codeVerifier'); // For troubleshooting
        print('Code Challenge: $_codeChallenge'); // For troubleshooting
    }