0
votes

Edit: To clarify, getting the authorization code works as expected. It is purely the step of exchanging the authorization code for tokens that fails.

I am trying to implement the authorization code with PKCE flow for authenticating with the spotify API. I know there are libraries out there for this, but I really want to implement it myself. The flow I am talking about is this: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce I am able to craft the link to redirect the user to the consent page and get a hold of the authorization code. However, when I try to exchange this code for tokens, I get a 400 Bad Request with the message "invalid client_secret". This leads me to believe that Spotify assumes I am trying to use the regular Authorization Code flow, as the client secret is not a part of the PKCE flow at all. I suspect I am encoding the code_verifier or the code_challenge wrong. I found this answer on SO (How to calculate PCKE's code_verifier?) and translated it to C#, yielding identical results for the Base64 encoded hash, but it still doesn't work.

My code for generating the code_verifier and code_challenge is below, as well as the code making the request to exchange the code.

CodeVerifier:

private string GenerateNonce()
{
    const string chars = "abcdefghijklmnopqrstuvwxyz123456789";
    var random = new Random();
    var nonce = new char[100];
    for (int i = 0; i < nonce.Length; i++)
    {
        nonce[i] = chars[random.Next(chars.Length)];
    }
    return new string(nonce);
}

CodeChallenge:

    private string GenerateCodeChallenge(string codeVerifier)
    {
        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
        return Convert.ToBase64String(hash).Replace("+/", "-_").Replace("=", "");
    }

Exchange token:

        var parameters = new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("client_id", ClientId ),
            new KeyValuePair<string, string>("grant_type", "authorization_code"),
            new KeyValuePair<string, string>("code", authCode),
            new KeyValuePair<string, string>("redirect_uri", "http://localhost:5000"),
            new KeyValuePair<string, string>("code_verifier", codeVerifier)
        };

        var content = new FormUrlEncodedContent(parameters );
        var response = await HttpClient.PostAsync($"https://accounts.spotify.com/api/token", content);
1
Hi, I think you're missing a step. Before calling /api/token you should be calling /authorize to get the code to exchange with an access token.Michaelsoft
@Michaelsoft Sorry if that was unclear - the part where I retrieve the authorization code works fine, and I am also passing it along in the request content as can be seen in the code below the "Exchange token" header in the post.sunero4
Ok, sorry, I misunderstood that part. Did you tried something similar instead of PostAsync? var client = new HttpClient(); var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(parameters) }; var res = await client.SendAsync(req);Michaelsoft
Maybe using a dict instead of List<KeyValuePair<string, string>>?Michaelsoft
Thanks for the input - I have tried both things now, but unfortunately neither worked. I am almost 100% sure that it must be something related to how I encode the code_verifier or the code_challenge, but I cannot figure out whatsunero4

1 Answers

2
votes

I reproduced code and was able to make it work. Here is a working project on github: https://github.com/michaeldisaro/TestSpotifyPkce.

The changes I made:

public class Code
{

    public static string CodeVerifier;

    public static string CodeChallenge;

    public static void Init()
    {
        CodeVerifier = GenerateNonce();
        CodeChallenge = GenerateCodeChallenge(CodeVerifier);
    }

    private static string GenerateNonce()
    {
        const string chars = "abcdefghijklmnopqrstuvwxyz123456789";
        var random = new Random();
        var nonce = new char[128];
        for (int i = 0; i < nonce.Length; i++)
        {
            nonce[i] = chars[random.Next(chars.Length)];
        }

        return new string(nonce);
    }

    private static string GenerateCodeChallenge(string codeVerifier)
    {
        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
        var b64Hash = Convert.ToBase64String(hash);
        var code = Regex.Replace(b64Hash, "\\+", "-");
        code = Regex.Replace(code, "\\/", "_");
        code = Regex.Replace(code, "=+$", "");
        return code;
    }

}

I call Init before redirecting to /authorize, the on the redirect url I have:

    public async Task OnGet(string code,
                            string state,
                            string error)
    {
        var httpClient = _httpClientFactory.CreateClient();

        var parameters = new Dictionary<string, string>
        {
            {"client_id", "*****************"},
            {"grant_type", "authorization_code"},
            {"code", code},
            {"redirect_uri", "https://localhost:5001/SpotifyResponse"},
            {"code_verifier", Code.CodeVerifier}
        };

        var urlEncodedParameters = new FormUrlEncodedContent(parameters);
        var req = new HttpRequestMessage(HttpMethod.Post, "https://accounts.spotify.com/api/token") { Content = urlEncodedParameters };
        var response = await httpClient.SendAsync(req);
        var content = response.Content;
    }

Replacing the correct regex does the job. It seems the problem is the "=", only the last ones must be replaced.

The function is not complete, I just watched at content variable and there was the token inside. Take that and do whatevere you prefer.