11 Mar 2024

Getting a Token with PKCE - Desktop App

pkce 3lo winform

Introduction

In most of our samples and tutorials, we build web applications, where you take advantage of the framework of your choice for the server side and the UI is handled with static files.

That's great, but this workflow doesn't cover multiple scenarios that you might need to address.

You can also build desktop applications and even plugins that take advantage of APS APIs, and to do that, you'll need to take care of the authentication using a token. 

That's what we'll cover in this blog post, using the PKCE method.

Thank you Denis for bringing this case to us.

Why PKCE?

As said by Petr Broz in this blog post:

This option is recommended for scenarios where your application is running natively on a desktop or a mobile device, in other words, for scenarios where you cannot protect your app's credentials. This application type uses Proof Key for Code Exchange (PKCE) for increased security.

Even if you protect your secrets from the client, it means that every time you need to change the client secret of your app, you need to update the app credentials.

Also, let's say you need to read/write data with some user context. That means you'll need a three-legged token.

PKCE method makes it safer as it demands a code generated by the client that started the process, in a way that only the client has this code, so only this client can exchange the credential code with a token.

How it works?

The complete process is summarized in the diagram from the main image of this blog post, also available below:

pkce workflow

 

Where:

A – The user access the Desktop app and the app redirects the user to authorization server with app credential and a code challenge generated from a random string used as a code verifier.

B – After logging in and allowing access, authorization server sends credential code to call-back url and this request is intercepted by the desktop app.

C – Desktop app sends credential code with code verifier to Exchange it for a token

D – Oauth API returns the token (including refresh token) to desktop app

For this sample, we've chosen to use a windows form to start the process and expose the token, as it's also easily applied to a plugin.

We came up with the minimal UI as in the image below:

form ui

Where:

1 – Clicking on generate token triggers the process

2 – You also have the option to refresh a token with the button

3 - Once the process is done, the token will be posted in the texbox

From the form, as soon as the user starts the process to generate a valid token, in the background a codeVerifier (just a random string) and a codeChallenge (from the codeVerifier) are generated, using the snippets below:

public static string RandomString(int length)
{
  const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  return new string(Enumerable.Repeat(chars, length)
    .Select(s => s[random.Next(s.Length)]).ToArray());

  //Note: The use of the Random class makes this unsuitable for anything security related, such as creating passwords or tokens.Use the RNGCryptoServiceProvider class if you need a strong random number generator
}

private static string GenerateCodeChallenge(string codeVerifier)
{
  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;
}

 With that we can build the URL to redirect the user to log in and allow the access. 

* It's important to store the codeVerifier as it will be needed when we receive the credential code.

In this sample it's stored in a global variable.

Once he allows the access and get redirected to the callback URL defined on our app, we need to extract the credential code received.

Extracting the credential code

To extract the credential code in a simple way, we can use HttpListener.

With that, we basically add a listener to requests to specific URLs (in our case, the callback).

With the sniper below, we can intercept this request and extract the credential code

// This example requires the System and System.Net namespaces.
public async Task SimpleListenerExample(string[] prefixes)
{
  if (!HttpListener.IsSupported)
  {
    throw new NotSupportedException("HttpListener is not supported in this context!");
  }
  // URI prefixes are required,
  // for example "http://contoso.com:8080/index/".
  if (prefixes == null || prefixes.Length == 0)
    throw new ArgumentException("prefixes");

  // Create a listener.
  HttpListener listener = new HttpListener();
  // Add the prefixes.
  foreach (string s in prefixes)
  {
    listener.Prefixes.Add(s);
  }
  listener.Start();
  //Console.WriteLine("Listening...");
  // Note: The GetContext method blocks while waiting for a request.
  HttpListenerContext context = listener.GetContext();
  HttpListenerRequest request = context.Request;
  // Obtain a response object.
  HttpListenerResponse response = context.Response;

  try
  {
    string authCode = request.Url.Query.ToString().Split('=')[1];
    await GetPKCEToken(authCode);
  }
  catch (Exception ex)
  {
    lbl_Status.Text = "An error occurred!";
    txt_Result.Text = ex.Message;
  }

  // Construct a response.
  string responseString = "<HTML><BODY> You can move to the form!</BODY></HTML>";
  byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
  // Get a response stream and write the response to it.
  response.ContentLength64 = buffer.Length;
  System.IO.Stream output = response.OutputStream;
  output.Write(buffer, 0, buffer.Length);
  // You must close the output stream.
  output.Close();
  listener.Stop();
}

With the credential code in hands, is time to exchange it with a new token.

Retrieving your token

That's achieved using the GetPKCEToken method below:

private async Task GetPKCEToken(string authCode)
{
  try
  {
    var client = new HttpClient();
    var request = new HttpRequestMessage
    {
      Method = HttpMethod.Post,
      RequestUri = new Uri("https://developer.api.autodesk.com/authentication/v2/token"),
      Content = new FormUrlEncodedContent(new Dictionary<string, string>
      {
        { "client_id", Global.ClientId },
		{ "code_verifier", Global.codeVerifier },
		{ "code", authCode},
		{ "grant_type", "authorization_code" },
		{ "redirect_uri", Global.CallbackURL }
	  }),
	};
	using (var response = await client.SendAsync(request))
	{
	  response.EnsureSuccessStatusCode();
	  string bodystring = await response.Content.ReadAsStringAsync();
	  JObject bodyjson = JObject.Parse(bodystring);
	  lbl_Status.Text = "You can find your token below";
	  txt_Result.Text = bodyjson["access_token"].Value<string>();
	}
  }
  catch (Exception ex)
  {
    lbl_Status.Text = "An error occurred!";
    txt_Result.Text = ex.Message;
  }
}

As long as you have your app provisioned in your hub, this token can be used to view the contents you have access to.

There's also the option to refresh a token, addressed with the snippet below:

private async void btn_Refresh_Click(object sender, EventArgs e)
{
  try
  {
    var client = new HttpClient();
    var request = new HttpRequestMessage
    {
      Method = HttpMethod.Post,
      RequestUri = new Uri("https://developer.api.autodesk.com/authentication/v2/token"),
      Content = new FormUrlEncodedContent(new Dictionary<string, string>
      {
        { "scope", "data:read" },
        { "grant_type", "refresh_token" },
        { "refresh_token", Global.RefreshToken },
        { "client_id", Global.ClientId }
      }),
    };
    using (var response = await client.SendAsync(request))
    {
      response.EnsureSuccessStatusCode();
      string bodystring = await response.Content.ReadAsStringAsync();
      JObject bodyjson = JObject.Parse(bodystring);
      lbl_Status.Text = "You can find your new token below";
      txt_Result.Text = Global.AccessToken = bodyjson["access_token"].Value<string>();
      Global.RefreshToken = bodyjson["refresh_token"].Value<string>();
    }
  }
  catch (Exception ex)
  {
  lbl_Status.Text = "An error occurred!";
  txt_Result.Text = ex.Message;
  }
}

You can find the complete source code below:

 SOURCE

Related Article