我正在制作一个 Java 应用程序,尝试为需要 PKCE(代码交换证明密钥)的 API (Etsy.com) 实现 OAuth2 授权流程。我已经尝试了一段时间,但在交换 OAuth 令牌的访问代码时,我一直遇到以下错误:
{"error":"invalid_grant","error_description":"code_verifier is invalid"}
这是生成传递 sha256 编码验证器的初始 URL 的方法:
@Override
public String getOAuth2AuthorizationUrl() {
String sha256hex = null;
try {
if(_currentVerifier == null)
_currentVerifier = PkceUtil.generateCodeVerifier();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(
_currentVerifier.getBytes(StandardCharsets.UTF_8));
sha256hex = new String(encodedhash);
} catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
UnicodeEscaper basicEscaper = new PercentEscaper("-", false);
System.out.println("sha256="+sha256hex);
System.out.println("veri="+_currentVerifier);
String oAuth2AuthorizationUrl = super.getOAuth2AuthorizationUrl();
var authUrl = oAuth2AuthorizationUrl
+"&state=asdf&code_challenge_method=S256&code_challenge='"
+ basicEscaper.escape(sha256hex)+"'";
return authUrl;
}
然后我发送请求以交换令牌代码作为 application/x-www-form-urlencoded POST-request:
UnicodeEscaper basicEscaper = new PercentEscaper("-", false);
var params = "grant_type=authorization_code&client_id="
+token.getOwner().clientId+"&redirect_uri="
+basicEscaper.escape("https://localhost")
+"&code="+accessCode+"&code_verifier="+basicEscaper.escape(_currentVerifier)+"";
我尝试了很多变体,也尝试将值包装在 '' 等中,但没有任何效果。我是否误解了这个过程?我最初发送 sha256 字符串,当请求 OAuth2 令牌时,我发送编码的值。 有什么想法吗?
更新: 我尝试使用 Google 库来生成哈希,但仍然遇到相同的错误。
Hasher hasher = Hashing.sha256().newHasher();
hasher.putString(CODE_VERIFIER, Charsets.UTF_8);
HashCode sha256 = hasher.hash();
System.out.println("veri="+AppUtils.encodeBase64(AppUtils.toHexString(sha256.asBytes())));
String oAuth2AuthorizationUrl = super.getOAuth2AuthorizationUrl();
var authUrl = oAuth2AuthorizationUrl
+"&state=asdf&code_challenge_method=S256&code_challenge="+AppUtils.encodeBase64(AppUtils.toHexString(sha256.asBytes()));
return authUrl;
示例中的 code_challenge 参数的格式看起来也不同。
您的挑战生成不正确。验证者的哈希值必须是 base64 url 编码。
标准说
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
所以这是错误的:
sha256hex = new String(encodedhash);
应该是:
sha256hex = Base64.getUrlEncoder().withoutPadding().encodeToString(encodedhash);
另外,不要在 URL 中的挑战周围加引号。
旁注:没有必要逃避挑战和验证器,因为它们是以 url 安全的方式编码的
我知道这个问题已经很老了,但我希望它能拯救像我这样的人。
code_verifier is invalid
实际上与 PKCE verification failed
不同(One keycloak 给我使用了错误的验证器)。
标准规定您的密钥长度应至少为 43 个字符,最多不超过 128 个字符。您还应该加密已经基本编码的版本。这段代码对我有用:
public class Main {
public static void main(String[] args) throws NoSuchAlgorithmException {
Base64.Encoder encoder = Base64.getEncoder().withoutPadding();
String inp = encoder.encodeToString(Utils.generateString(43).getBytes());
MessageDigest digest = MessageDigest.getInstance("SHA-256");
String res = encoder.encodeToString(digest.digest(inp.getBytes()));
System.out.println("Challenge: " + StringEscapeUtils.escapeHtml3(res) + "\nVerifier: " + inp);
}
}
public class Utils {
public static Random random = new Random();
public static String[] characters;
static {
String charS = "abcdefghijklmnopqrstuvwxyz";
charS+=charS.toUpperCase() + "01234567890";
characters = charS.split("");
}
public static String generateString(int length) {
StringBuilder result = new StringBuilder();
for(int now = 0; now<length; now++) {
result.append(characters[random.nextInt(characters.length)]);
}
return result.toString();
}
}
我遇到了很多麻烦,因为即使我阅读并遵循 Etsy 在 grantflow 授权中的文档(我认为是不折不扣的),我所尝试的方法也不起作用。
您应该仔细阅读快速入门指南的这一部分,其中 Etsy 开发人员展示了他们如何执行 PKCE 的代码示例:https://developers.etsy.com/documentation/tutorials/quickstart/#generate-the-pkce-code-挑战
我在最初的程序中所做的(不起作用)是在规范范围内生成随机字符串,并使用标准的 Base64 编码器。您可以在 Etsy 的代码片段(复制如下)中看到,他们修改了常规的 base64 以删除两个不能很好地编码到 URL 中的字符。此外,它们使用 JS 函数生成 32 个随机字节,这可能与连接 32 个字符串字符不同。不管怎样,你应该尝试将这段代码从 JS 翻译成 Java,看看它是如何工作的。
我认为字节编码很重要的另一个原因是,在 Etsy 的 API 中,他们引用了此 Web 规范 https://datatracker.ietf.org/doc/html/rfc7636#appendix-B 并且它使用哈希的八位字节值虽然您在调用 base64 编码函数时可能使用不同的基数。
const crypto = require("crypto");
// The next two functions help us generate the code challenge
// required by Etsy’s OAuth implementation.
const base64URLEncode = (str) =>
str
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
const sha256 = (buffer) => crypto.createHash("sha256").update(buffer).digest();
// We’ll use the verifier to generate the challenge.
// The verifier needs to be saved for a future step in the OAuth flow.
const codeVerifier = base64URLEncode(crypto.randomBytes(32));
// With these functions, we can generate
// the values needed for our OAuth authorization grant.
const codeChallenge = base64URLEncode(sha256(codeVerifier));
const state = Math.random().toString(36).substring(7);
console.log(`State: ${state}`);
console.log(`Code challenge: ${codeChallenge}`);
console.log(`Code verifier: ${codeVerifier}`);
console.log(`Full URL: https://www.etsy.com/oauth/connect?response_type=code&redirect_uri=http://localhost:3003/oauth/redirect&scope=email_r&client_id=1aa2bb33c44d55eeeeee6fff&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`)
今天我花了几个小时试图解决这个问题,终于找到了可行的解决方案。在寻找答案的过程中,我发现了这个问题,我想留下我所做的事情,以防其他人遇到这个问题。