Xamarin Essentials 无法交换令牌的 Okta 授权码

Xamarin Essentials Unable to exchange Okta authorization code for token

我正在使用 OpenID,我们必须切换到 Xamarin.Essentials.WebAuthenticator。
我可以使用 WebAuthenticator.AuthenticateAsync().
从 Okta 获取授权码 但是,我尝试将该代码转换为访问令牌的所有内容 returns 400 Bad Request.
Okta 的 API 错误是“E0000021:HTTP 媒体类型不支持的异常”,它继续说,“错误的请求。接受 and/or Content-Type headers 可能不匹配支持价值观。”

我已经尝试尽可能地遵循 https://developer.okta.com/blog/2020/07/31/xamarin-essentials-webauthenticator,但我们并没有像他那样使用混合授权类型。
我们只使用授权码,这意味着我必须进行二次调用,我花了两天时间想弄清楚如何。


private async Task LoginOktaAsync()
{
  try
  {
    var loginUrl = new Uri(BuildAuthenticationUrl());  // that method is down below
    var callbackUrl = new Uri("com.oktapreview.dev-999999:/callback"); // it's not really 999999
    var authenticationResult = await Xamarin.Essentials.WebAuthenticator.AuthenticateAsync(loginUrl, callbackUrl);

    string authCode;                                
    authenticationResult.Properties.TryGetValue("code",out authCode);

    // Everything works fine up to this point. I get the authorization code.

    var url = $"https://dev-999999.oktapreview.com/oauth2/default/v1/token"
         +"?grant_type=authorization_code"
         +$"&code={authCode}&client_id={OktaConfiguration.ClientId}&code_verifier={codeVerifier}";

    var request = new HttpRequestMessage(HttpMethod.Post, url);
    var client = new HttpClient();
    var response = await client.SendAsync(request); // this generates the 400 error.
  }
  catch(Exception e)
  {
    Debug.WriteLine($"Error: {e.Message}");
  }
}

以下是生成登录 url 和其他一些东西的方法:


public string BuildAuthenticationUrl()
{
  var state = CreateCryptoGuid();
  var nonce = CreateCryptoGuid();

  CreateCodeChallenge();

  var url = $"https://dev-999999.oktapreview.com/oauth2/default/v1/authorize?response_type=code"
      + "&response_mode=fragment"
      + "&scope=openid%20profile%20email"
      + "&redirect_uri=com.oktapreview.dev-999999:/callback"
      +$"&client_id={OktaConfiguration.ClientId}"
      +$"&state={state}"
      +$"&code_challenge={codeChallenge}"
      + "&code_challenge_method=S256"
      +$"&nonce={nonce}";
  return url;
}

private string CreateCryptoGuid()
{
  using (var generator = RandomNumberGenerator.Create())
  {
    var bytes = new byte[16];
    generator.GetBytes(bytes);
    return new Guid(bytes).ToString("N");
  }
}

private string CreateCodeChallenge()
{
  codeChallenge = GenerateCodeToVerify();
  codeVerifier = codeChallenge;
  using (var sha256 = SHA256.Create())
  {
    var codeChallengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeChallenge));
    return Convert.ToBase64String(codeChallengeBytes);
  }
}    

private string GenerateCodeToVerify() 
{
  var str = "";
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
  Random rnd = new Random();
  for (var i = 0; i < 100; i++) 
  {
    str += possible.Substring(rnd.Next(0,possible.Length-1),1);
  }
  return str;
}


'''

经过大量在线研究,我发现问题出在我 post 获取令牌的方式上。这就是我让它工作的方式:

public static Dictionary<string, string> JsonDecode(string encodedString)
{
    var inputs = new Dictionary<string, string>();
    var json = JValue.Parse(encodedString) as JObject;

    foreach (KeyValuePair<string, JToken> kv in json)
    {
        if (kv.Value is JValue v)
        {
            if (v.Type != JTokenType.String)
                inputs[kv.Key] = v.ToString();
            else
                inputs[kv.Key] = (string)v;
        }
    }
    return inputs;
}


private async Task<string> ExchangeAuthCodeForToken(string authCode)
{
    string accessToken = string.Empty;
    List<KeyValuePair<string, string>> kvdata = new List<KeyValuePair<string, string>>
    {
        new KeyValuePair<string, string>("grant_type", "authorization_code"),
        new KeyValuePair<string, string>("code", authCode),
        new KeyValuePair<string, string>("redirect_uri", OktaConfiguration.Callback),
        new KeyValuePair<string, string>("client_id", OktaConfiguration.ClientId),
        new KeyValuePair<string, string>("code_verifier", codeVerifier)
    };
    var content = new FormUrlEncodedContent(kvdata);

    var request = new HttpRequestMessage(HttpMethod.Post, OktaConfiguration.TokenUrl)
                {Content = content, Method = HttpMethod.Post};
    HttpClient client = new HttpClient();
    HttpResponseMessage response = await client.SendAsync(request);
    string text = await response.Content.ReadAsStringAsync();
    Dictionary<string, string> data = JsonDecode(text);
    data.TryGetValue("access_token", out accessToken);
    return accessToken;
}