Quantcast
Viewing all articles
Browse latest Browse all 2388

Connect a .NET Desktop Client to a Custom ASP.NET Core Service (EF Core with pure Web API) — Authorization Code Flow (Part 4)

This post is the next installment in my series titled Connect a .NET Desktop Client to a Custom ASP.NET Core Service (EF Core with pure Web API). The content relates to my previously published post Modern Desktop Apps And Their Complex Architectures and aims to illustrate an application system architecture that includes a data access service in addition to a WinForms application with a DevExpress Data Grid.

In the previous post of this series, I demonstrated how to add a log-in feature to the application, and to make security decisions selectively based on tokens returned by a Keycloak service. I pointed out at the time that the implementation used the Resource Owner Password Credentials (ROPC) flow, and I made that first step because that flow is familiar in the context of traditional client applications. When a user logs in, an app-specific dialog queries username and password, then sends that information to a server for verification.

Table of Contents

We also have related blog series, which may be of interest for you as well: JavaScript — Consume the DevExpress Backend Web API with Svelte (7 parts from data editing to validation, localization, reporting).

From ROPC to Authorization Code Flow

As I also mentioned previously, this ROPC flow is not a mechanism that is considered very secure. The main reason is that the client application needs to have at least temporary knowledge of the credentials entered by the user, which means that the user must trust the application and its developers, and that the developers are fully responsible for the fate of those credentials. Of course we all hope that we never make mistakes, but additional security can be had quite easily if we find a way to authenticate a user without ever providing the client application with the credentials in any reusable form.

One solution to this problem is the Authorization Code Flow. Microsoft’s documentation for this flow is somewhat Azure and Microsoft Entra ID specific, but it includes some useful flow diagrams that describe the steps of the process in some detail. Feel free to read this for background information.

Like the previous post, this example uses Keycloak. If you’d like to try the code for yourself, please find the setup instructions in this section of the previous post. In the sections below, we will walk through the following steps to update the demo application for the Authorization Code Flow :

  • Update the Keycloak configuration to register a client with an updated security configuration
  • Accept redirections from the Keycloak service in the WinForms client by registering a protocol handler and passing on any received details to a running app instance
  • Remove the old login form and modify the authentication process to implement the Authorization Code Flow

An interesting note is that no changes at all are required to the data service. Everything that needs to be changed is on the client side. While the flow that provides the token to the client will be changed completely in this update to the demo, the token itself and the information it contains remain exactly the same.

Update the Keycloak Configuration

For the previous version of the sample application, the Keycloak service was configured with a client registration that supported Direct Access Grants , which was necessary for the ROPC flow. You can leave this client registration in place (though for a real-world setup it would be safer to delete it!) and create a new registration to support the Authorization Code Flow now. However, before you do this, check the Client scopes of your realm to make sure the openid scope is included there.

Since Keycloak has a focus on OAuth, theopenidscope may not be included by default in some versions of Keycloak.

If you don’t see the scope listed, please add it using the Create client scope button. Just enter the name openid, select the protocol OpenID Connect and the type Default , then save the entry.

Now you can register a new client using the Create client button. The client type is OpenID Connect and you can set your own client ID. For the description and the sample code, I’ll use appAuthFlow1 as the client ID.

On the page Capability config there is a section titled Authentication flow. This includes the checkbox Direct access grant , and this item may be checked by default. Make sure to uncheck it now! But leave Standard flow active, since that’s what you need for the Authorization code flow .

Finally, on the Login settings page, you need to define one or more valid redirect URIs. I’ll use winappdemo://* for this sample, but you can make up your own protocol prefix if you like, or specify in more detail what the text following the protocol part should look like. For purposes of this sample I will always use the string default with the protocol prefix, so it would be possible to lock things down further by including it in the URI here.

Once you have added the client, switch to the tab Client scopes— for the client registration, not the main menu item of the same name! — and make sure that the openid scope is included. It should be added automatically if you created it first as described, but you can also add it later in case it shouldn’t be there.

Image may be NSFW.
Clik here to view.
Client scopes include openid

Accept redirections in WinForms using a protocol handler

As the name indicates, the idea of the Authorization Code Flow is that the authentication service returns a code to the client side which indicates that valid user credentials have been provided. Step number one of the flow is, therefore, that the user is prompted to log in by a page provided by the authentication service itself, and not by the client application. This typically works by directing the client to a web page, as you will see in a little while.

Once the user has provided valid credentials and logged into the authentication service, the code that is generated is typically returned to the client by way of an automatic redirection. This means that the user’s browser is redirected to a URL, and this URL is specific to the client application that is waiting for the code. The URL is in fact specified by the client application together with the initial request, but the service can determine whether a given URL is valid for the purpose or not. You already configured your client registration on Keycloak to validate those URLs based on the protocol prefix.

It is possible to handle the redirect in various different ways. For example, you could run your own HTTP service inside your client application and react to incoming requests directly. However, Windows also has a feature that allows you to register your own protocol prefix and then your application will simply be run as a command by Windows and the details that are passed through by the URL are supplied as command line arguments. This is the approach I chose to use for this demo application.

The first step you need to take for this mechanism to work is to register your protocol in the registry. If your application uses an installer, it can handle this at install time. However, you can also easily create the necessary registry entries programatically. Here’s the method RegisterProtocol that will run on startup in the demo app.

static void RegisterProtocol()
{
  string customProtocol = "winappdemo";
  string applicationPath = Application.ExecutablePath;
  var keyPath = $@"Software\Classes\{customProtocol}";
  using (var key = Registry.CurrentUser.CreateSubKey(keyPath, true))
  {
    if (key == null)
    {
      throw new Exception($"Registry key can't be written: {keyPath}");
    }
    key.SetValue(string.Empty, "URL:" + customProtocol);
    key.SetValue("URL Protocol", string.Empty);
    using (var commandKey = key.CreateSubKey(@"shell\open\command"))
    {
      commandKey.SetValue(string.Empty, $"{applicationPath} %1");
    }
  }
}

The main parts of this are the name of the custom protocol, which of course has to correspond to your Valid URI configuration on the Keycloak side, and the command which runs whenever the custom protocol is encountered.

For the demo app I chose to implement a mechanism that uses a named pipe to send any incoming protocol information to an already running instance of the same application. I assume that under normal circumstances there should be a running instance already, since the user login process would have to be triggered from that instance. Of course, it is also possible to use several instances of the application at once — it depends on your requirements whether you would like to accept multiple instances running at once or restrict your application to just one instance at a time.

In startup, if no parameters are detected on the command line, the application attempts to start a listener for the named pipe.

private const string pipeName = "WinAppDemoProtocolMessagePipe";
...
static void StartProtocolMessageListener()
{
  Task.Run(async () =>
  {
    while (true)
    {
      using var server = new NamedPipeServerStream(pipeName, PipeDirection.In);
      server.WaitForConnection();
      using var reader = new StreamReader(server);
      var msg = reader.ReadToEnd();
      await HandleProtocolMessage(msg);
    }
  });
}

static async Task HandleProtocolMessage(string msg)
{
  Console.WriteLine($"Handling protocol message: '{msg}'");
  await DataServiceClient.AcceptProtocolUrl(msg);
}

The method HandleProtocolMessage passes the message on to the class DataServiceClient which you might be familiar with from previous versions of my sample. This is where further handling will occur on the UI level and I’ll get back to it in a moment.

If there is a parameter detected on the command line, the application attempts to send the details to a running instance initially.

static void SendProtocolMessage(string msg)
{
  using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.Out);
  client.Connect(500);
  using var writer = new StreamWriter(client) { AutoFlush = true };
  writer.Write(msg);
}

The Connect call uses a short timeout. If no running instance is found, there is an error in the process.

Implement Authorization Code Flow

A couple of small changes have been applied to the MainForm in the demo app. The LoginForm has been removed from the project because it is not needed anymore, and the login logic is now split between the login method, which simply calls the DataServiceClient, and an event handler that reacts to any login status changes in the DataServiceClient and changes the UI appearance accordingly.

The method DataServiceClient.LogIn is where the Authorization Code Flow begins. The helper Url.Combine from the highly recommended Flurl library is used to construct the required URL with all its parts. To keep things generic in the demo, I use a simple Process.Start call to execute the default browser configured in the system to bring up the login page on the Keycloak server.

var url = Url.Combine(authUrl, "realms", realm, "protocol", "openid-connect", "auth")
  .SetQueryParams(
    new
    {
      response_type = "code",
      client_id = clientId,
      redirect_uri = redirectUri,
      scope = "openid profile email",
    }
);

Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });

The redirectUri is a new entry in App.config, and I set it to winappdemo://default . Note that the scope includes the value openid— this is why you configured the Keycloak service to support that scope. It is required so that the service returns an ID token as well as the access and refresh tokens, which is necessary to log out correctly later.

After the user has logged in successfully with the Keycloak service, the redirect URI is used and Windows runs the second instance of the application, which results in a call to AcceptProtocolUrl on the DataServiceClient— you have seen this above already.

The code in AcceptProtocolUrl is similar to what we used before to retrieve a token from Keycloak. The difference is that now we use the code that was returned after the initial roundtrip, instead of sending username and password details directly. Here’s what this part of the logic looks like now:

public static async Task AcceptProtocolUrl(string protocolUrlString)
{
  var protocolUrl = new Url(protocolUrlString);
  if (
    protocolUrl.QueryParams.TryGetFirst("code", out object codeObject)
    && codeObject is string code
  )
  {
    CheckSettings(["authUrl", "realm", "clientId", "redirectUri"]);

    var content = new FormUrlEncodedContent(
      new Dictionary<string, string>
      {
        { "grant_type", "authorization_code" },
        { "client_id", clientId! },
        { "code", code },
        { "redirect_uri", redirectUri! },
      }
    );
    var url = Url.Combine(
      authUrl,
      "realms",
      realm,
      "protocol",
      "openid-connect",
      "token"
    );

    var response = await bareHttpClient.PostAsync(url, content);

...

If you compare this to the previous version, you will find that the difference is largely in the parameters that are sent to the server, which now include the grant_typeauthorization_code and of course the code itself, redirect URI.

The method GetTokens has been extended to return the id_token in addition to the access_token, refresh_token and the expires_in value. This is relevant to log the user out from the server when required. If you don’t log out, the browser will remember a previously logged in user for a while. Of course this could be a security issue in cases where multiple users have access to the same machine, but for us developers it is also important because we can’t test the login mechanism with the two demo accounts reader and writer as long as the browser does not bring up the login window again because it thinks we’re already logged in!

The LogOut method looks like this:

public static void LogOut()
{
  var url = Url.Combine(authUrl, "realms", realm, "protocol", "openid-connect", "logout")
    .SetQueryParams(
      new { post_logout_redirect_uri = redirectUri, id_token_hint = idToken }
    );

  Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}

This summarizes all the important changes and it is time now to test the new login functionality. After the login process has been triggered in the client application, the user finds themselves in a browser logging into the Keycloak service. As before, the demo accounts are “reader” and “writer”, with the passwords the same as the usernames.

Image may be NSFW.
Clik here to view.
Browser based login

When you click the Sign In button, the browser redirects as instructed, and this process brings up a confirmation dialog, at least the first time it happens — depending on the browser it may be possible to confirm that future attempts should be confirmed automatically.

Image may be NSFW.
Clik here to view.
Redirection confirmation

Once the redirection is handled by the WinForms app, the token is fetched and evaluated as before. You can then access the data as permitted by the user’s roles, log out and back in, and test the functionality in detail.

A final note about the mechanisms used in this demo implementation. Developers often wonder whether it isn’t too much of a hassle to deal with the browser window and the tabs that may stay open afterwards, etc — I don’t believe this is a problem. Users today are quite familiar with similar mechanisms, as they are used by many everyday websites and apps, even when watching TV with your Fire Stick or similar devices.

You should definitely resist the temptation to do everything “internally”. For instance, a frequently asked question is whether we couldn’t use a hosted web browser control in the app instead of a separate browser. Technically we could, but it is an important part of the security strategy here that the browser is a separate environment which is not controlled or observed by the application itself. If you feel that the login mechanism described here is not perfect for your needs, I recommend checking out some other options! For instance, the Device Authorization Grant can be useful, enabling a user to log in to a system using a separate device, perhaps a mobile phone with a QR code.

Your Feedback Matters!

You can also download our GitHub example and play with different configurations on your own. Please send your feedback — thanks to all of you who have already done that! — and any questions or ideas, we will attempt to consider everything!

Image may be NSFW.
Clik here to view.

Viewing all articles
Browse latest Browse all 2388

Trending Articles