Issue
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);
Solution
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.
Answered By - Michaelsoft Answer Checked By - Marilyn (PHPFixing Volunteer)
0 Comments:
Post a Comment
Note: Only a member of this blog may post a comment.