如何使用 PKCE for Spotify 实现授权代码
Posted
技术标签:
【中文标题】如何使用 PKCE for Spotify 实现授权代码【英文标题】:How to implement Authorization Code with PKCE for Spotify 【发布时间】:2021-03-18 01:35:26 【问题描述】:编辑:澄清一下,获取授权码按预期工作。这纯粹是用授权码交换失败的令牌的步骤。
我正在尝试使用 PKCE 流程实现授权代码,以使用 spotify API 进行身份验证。我知道那里有图书馆,但我真的很想自己实现它。我说的流程是这样的: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce 我能够制作链接以将用户重定向到同意页面并获取授权码。但是,当我尝试将此代码交换为令牌时,我收到 400 错误请求,并显示消息“invalid client_secret”。这让我相信 Spotify 假设我正在尝试使用常规授权代码流,因为客户端密码根本不是 PKCE 流的一部分。我怀疑我对 code_verifier 或 code_challenge 的编码错误。 我在 SO (How to calculate PCKE's code_verifier?) 上找到了这个答案并将其翻译成 C#,得到了与 Base64 编码哈希相同的结果,但它仍然不起作用。
下面是我生成 code_verifier 和 code_challenge 的代码,以及请求交换代码的代码。
代码验证器:
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);
代码挑战:
private string GenerateCodeChallenge(string codeVerifier)
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
return Convert.ToBase64String(hash).Replace("+/", "-_").Replace("=", "");
兑换代币:
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);
【问题讨论】:
嗨,我想你错过了一步。在调用 /api/token 之前,您应该调用 /authorize 以获取与访问令牌交换的代码。 @Michaelsoft 抱歉,如果不清楚 - 我检索授权代码的部分工作正常,我也在请求内容中传递它,如“交换令牌”下方的代码所示" 帖子中的标题。 好的,对不起,我误解了那部分。您是否尝试过类似的东西而不是 PostAsync? var client = new HttpClient(); var req = new HttpRequestMessage(HttpMethod.Post, url) Content = new FormUrlEncodedContent(parameters) ; var res = await client.SendAsync(req); 也许使用字典而不是 List我复制了代码并且能够使它工作。 这是 github 上的一个工作项目:https://github.com/michaeldisaro/TestSpotifyPkce。
我所做的更改:
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;
我在重定向到 /authorize 之前调用 Init,在我拥有的重定向 url 上:
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;
替换正确的正则表达式就可以了。看来问题是“=”,只有最后一个必须替换。
功能不完整,我只是看了内容变量,里面有token。拿走它,做任何你喜欢的事情。
【讨论】:
老兄,非常感谢您抽出宝贵时间来做这件事。事实上,它不是 code_verifier 或 code_challenge(或至少不仅如此),但在我改变了构建 Spotify url 的方式后,我将用户重定向到了你在 repo 中的方式,它起作用了。谢谢! 这肯定是一个“组合”,没有 Regex.Replace 我也有 400 个响应。 是的,我认为你是对的。再次感谢您的帮助:)【参考方案2】:这里是GenerateNonce
(现在是GenerateCodeVerifier
)和GenerateCodeChallenge
的重构,它符合rfc-7636 标准,集成到一个可以被实例化或用于其静态方法的类中。
/// <summary>
/// Provides a randomly generating PKCE code verifier and it's corresponding code challenge.
/// </summary>
public class Pkce
/// <summary>
/// The randomly generating PKCE code verifier.
/// </summary>
public string CodeVerifier;
/// <summary>
/// Corresponding PKCE code challenge.
/// </summary>
public string CodeChallenge;
/// <summary>
/// Initializes a new instance of the Pkce class.
/// </summary>
/// <param name="size">The size of the code verifier (43 - 128 charters).</param>
public Pkce(uint size = 128)
CodeVerifier = GenerateCodeVerifier(size);
CodeChallenge = GenerateCodeChallenge(CodeVerifier);
/// <summary>
/// Generates a code_verifier based on rfc-7636.
/// </summary>
/// <param name="size">The size of the code verifier (43 - 128 charters).</param>
/// <returns>A code verifier.</returns>
/// <remarks>
/// code_verifier = high-entropy cryptographic random STRING using the
/// unreserved characters[A - Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
/// from Section 2.3 of[RFC3986], with a minimum length of 43 characters
/// and a maximum length of 128 characters.
///
/// ABNF for "code_verifier" is as follows.
///
/// code-verifier = 43*128unreserved
/// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
/// ALPHA = %x41-5A / %x61-7A
/// DIGIT = % x30 - 39
///
/// Reference: rfc-7636 https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
///</remarks>
public static string GenerateCodeVerifier(uint size = 128)
if (size < 43 || size > 128)
size = 128;
const string unreservedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789-._~";
Random random = new Random();
char[] highEntropyCryptograph = new char[size];
for (int i = 0; i < highEntropyCryptograph.Length; i++)
highEntropyCryptograph[i] = unreservedCharacters[random.Next(unreservedCharacters.Length)];
return new string(highEntropyCryptograph);
/// <summary>
/// Generates a code_challenge based on rfc-7636.
/// </summary>
/// <param name="codeVerifier">The code verifier.</param>
/// <returns>A code challenge.</returns>
/// <remarks>
/// plain
/// code_challenge = code_verifier
///
/// S256
/// code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
///
/// If the client is capable of using "S256", it MUST use "S256", as
/// "S256" is Mandatory To Implement(MTI) on the server.Clients are
/// permitted to use "plain" only if they cannot support "S256" for some
/// technical reason and know via out-of-band configuration that the
/// server supports "plain".
///
/// The plain transformation is for compatibility with existing
/// deployments and for constrained environments that can't use the S256
/// transformation.
///
/// ABNF for "code_challenge" is as follows.
///
/// code-challenge = 43 * 128unreserved
/// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
/// ALPHA = % x41 - 5A / %x61-7A
/// DIGIT = % x30 - 39
///
/// Reference: rfc-7636 https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
/// </remarks>
public static string GenerateCodeChallenge(string codeVerifier)
using (var sha256 = SHA256.Create())
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
return Base64UrlEncoder.Encode(challengeBytes);
对于那些从事单元测试的人。
/// <summary>
/// Pkce unit test.
/// </summary>
/// <remarks>
/// MethodName_StateUnderTest_ExpectedBehavior
/// Arrange, Act, Assert
/// </remarks>
[TestFixture]
public class PkceUnitTests
[Test]
public void GenerateCodeVerifier_DefaultSize_Returns128CharacterLengthString()
string codeVerifier = Pkce.GenerateCodeVerifier();
Assert.That(codeVerifier.Length, Is.EqualTo(128));
[Test]
public void GenerateCodeVerifier_Size45_Returns45CharacterLengthString()
string codeVerifier = Pkce.GenerateCodeVerifier(45);
Assert.That(codeVerifier.Length, Is.EqualTo(45));
[Test]
public void GenerateCodeVerifier_SizeLessThan43_ReturnsDefault128CharacterLengthString()
string codeVerifier = Pkce.GenerateCodeVerifier(42);
Assert.That(codeVerifier.Length, Is.EqualTo(128));
[Test]
public void GenerateCodeVerifier_SizeGreaterThan128_ReturnsDefault128CharacterLengthString()
string codeVerifier = Pkce.GenerateCodeVerifier(42);
Assert.That(codeVerifier.Length, Is.EqualTo(128));
[Test]
public void GenerateCodeVerifier_DefaultSize_ReturnsLegalCharacterLengthString()
const string unreservedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789-._~";
for (int x = 0; x < 1000; x++)
string codeVerifier = Pkce.GenerateCodeVerifier();
for (int i = 0; i < codeVerifier.Length; i++)
Assert.That(unreservedCharacters.IndexOf(codeVerifier[i]), Is.GreaterThan(-1));
[Test]
public void GenerateCodeChallenge_GivenCodeVerifier_ReturnsCorrectCodeChallenge()
string codeChallenge = Pkce.GenerateCodeChallenge("0t4Rep04AxvISWM3rMxGnyla2ceDT71oMzIK0iGEDgOt5.isAGW6~2WdGBUxaPYXA6R8vbSBcgSI-jeK_1yZgVfEXoFa1Ec3gPn~Anqwo4BgeXVppo.fjtU7y2cwq_wL");
Assert.That(codeChallenge, Is.EqualTo("czx06cKMDaHQdro9ITfrQ4tR5JGv9Jbj7eRG63BKHlU"));
[Test]
public void InstantiateClass_WithDefaultSize_Returns128CharacterLengthCodeVerifier()
Pkce pkce = new Pkce();
Assert.That(pkce.CodeVerifier.Length, Is.EqualTo(128));
[Test]
public void InstantiateClass_WithSize57_Returns57CharacterLengthCodeVerifier()
Pkce pkce = new Pkce(57);
Assert.That(pkce.CodeVerifier.Length, Is.EqualTo(57));
【讨论】:
以上是关于如何使用 PKCE for Spotify 实现授权代码的主要内容,如果未能解决你的问题,请参考以下文章
Spotify PKCE 授权流程返回“code_verifier 不正确”
在 ReactJS 中的 Spotify API 上为 PKCE 身份验证创建代码验证器和质询
如何使用 PKCE 登录 Google.Apis.Drive.v3 for .NET?