Xamarin Essentials Unable to exchange Okta authorization code for token

235 views Asked by At

I was using OpenID and we have to switch to Xamarin.Essentials.WebAuthenticator.
I can get an authorization code from Okta using WebAuthenticator.AuthenticateAsync().
But, everything I try to then translate that code into an access token returns 400 Bad Request.
Okta's API error is "E0000021: HTTP media type not supported exception" and it goes on to say, "Bad request. Accept and/or Content-Type headers likely do not match supported values."

I have tried to follow https://developer.okta.com/blog/2020/07/31/xamarin-essentials-webauthenticator as much as possible, but we are not using the hybrid grant type like he is.
We are using only Authorization Code, which means I have to make a secondary call, and I have spent two days trying to figure out how.


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}");
  }
}

Here are the methods that produce the login url and a couple of other things:


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;
}


'''

1

There are 1 answers

0
jsureke On BEST ANSWER

After much online research, I discovered the issue was with how I was doing my post to get the token. This is how I made it work:

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;
}