All of our MSAL samples are for either Web, mobile client or console applications in c#. This blog post will show how you can also use MSAL in vb.net in a Winforms desktop application.
When creating a winforms application, the thing to remember is that code in your form will run under the UI thread, which, for the most part is ok. However, when MSAL prompts for credentials, it will get thread-locked by the UI or visa versa if you just run that under the UI thread. You must make the authentication and token requests on a separate thread. In this sample, I show you one way you can handle that complex issue with a task and a delegate method and retrieve the tokens from the call.
This sample project can be found on my gitHub here. You will need to create an app registration in your tenant for this project and then update the Form1.vb variables with the client_id, client_secret and tenant_id. Please refer to the readme file in the gitHub for specifics about the reply URI.
The sample will demonstrate how to:
- log in a user interactively, prompting for credentials and getting the id_token and an access_token for the signed in user
- log in as an application using the client credentials grant flow and getting only an access token as you cannot get an id token using this flow since you’re not logging in as a user.
For the interactive flow, there is a method called “LoginInteractively” that will prompt the user for credentials. For the client credentials grant flow, there is a method called “LoginClientCredentials”. In either case, on the UI thread, when you need to sign-in using one of those methods, you simply need to create a new Task(AddressOf {method}). Like so:
Dim task = New Task(AddressOf LoginClientCredentials) task.Start() task.Wait()
You also need a way of capturing errors to display back on the main thread and I am doing this by simply pushing errors to a stack of strings and the poping them back off after the task completes to build an error message to display to the user.
If errors.Count > 0 Then Dim error_messages As New StringBuilder() Do Until errors.Count <= 0 If error_messages.Length > 0 Then error_messages.Append(ControlChars.NewLine) End If error_messages.Append(errors.Pop()) Loop MessageBox.Show($"Errors encountered: {error_messages.ToString()}") End If
In each method, to populate the text boxes on the form with the tokens, there is a delegate sub used since one thread cannot update ui elements on another thread. This is accomplished like so:
Private Async Sub LoginClientCredentials() Dim authResult As AuthenticationResult = Nothing Try Dim app As IConfidentialClientApplication = ConfidentialClientApplicationBuilder.Create(client_id).WithClientSecret(client_secret).WithTenantId(tenant_id).WithAuthority(AadAuthorityAudience.AzureAdMyOrg).Build() authResult = Await app.AcquireTokenForClient(scopes).ExecuteAsync() Catch ex As Exception errors.Push(ex.Message) Exit Sub End Try accessToken = authResult.AccessToken idToken = "No id token given for this auth flow." 'Since this thread runs under the ui thread, we need a delegate method to update the text boxes txtBoxAccessToken.BeginInvoke(New InvokeDelegate(AddressOf InvokeMethod)) End Sub Private Delegate Sub InvokeDelegate() Private Sub InvokeMethod() txtBoxAccessToken.Text = accessToken txtboxIDToken.Text = idToken End Sub
Here is the full code for Form1
Imports System.Text Imports Microsoft.Identity.Client Public Class Form1 Private accessToken As String = String.Empty Private idToken As String = String.Empty Private client_id As String = "{enter_client_id_here}" Private client_secret As String = "{enter_client_secret_here}" Private tenant_id As String = "{enter_tenant_id_here}" Private redirect_uri As String = "http://localhost" Private scopes() As String = New String() {"openid offline_access profile "} Private errors As New Stack(Of String) Private isLoggedIn As Boolean = False Private Sub btnSignIn_Click(sender As Object, e As EventArgs) Handles btnSignIn.Click txtboxIDToken.Text = String.Empty txtBoxAccessToken.Text = String.Empty idToken = String.Empty accessToken = String.Empty 'we need a task to get MSAL to log us in If (txtBoxScopes.Text.Length > 0) Then Try Dim _scopes() As String = txtBoxScopes.Text.Split(" ") scopes = _scopes Catch ex As Exception MessageBox.Show("Invalid scopes parameter... resetting to openid offline_access profile") txtBoxScopes.Text = "openid offline_access profile" txtBoxScopes.Focus() txtBoxScopes.SelectAll() Exit Sub End Try End If Dim task = New Task(AddressOf LoginInteractively) task.Start() task.Wait() If errors.Count > 0 Then Dim error_messages As New StringBuilder() Do Until errors.Count <= 0 If error_messages.Length > 0 Then error_messages.Append(ControlChars.NewLine) End If error_messages.Append(errors.Pop()) Loop MessageBox.Show($"Errors encountered: {error_messages.ToString()}") End If End Sub Private Sub btnClientCredentials_Click(sender As Object, e As EventArgs) Handles btnClientCredentials.Click txtboxIDToken.Text = String.Empty txtBoxAccessToken.Text = String.Empty idToken = String.Empty accessToken = String.Empty 'we need a task to get MSAL to log us in If (txtBoxScopes.Text.Length > 0) Then Try Dim _scopes() As String = txtBoxScopes.Text.Split(" ") scopes = _scopes Catch ex As Exception MessageBox.Show("Invalid scopes parameter... resetting to https://graph.microsoft.com/.default") txtBoxScopes.Text = "https://graph.microsoft.com/.default" txtBoxScopes.Focus() txtBoxScopes.SelectAll() Exit Sub End Try End If Dim task = New Task(AddressOf LoginClientCredentials) task.Start() task.Wait() If errors.Count > 0 Then Dim error_messages As New StringBuilder() Do Until errors.Count <= 0 If error_messages.Length > 0 Then error_messages.Append(ControlChars.NewLine) End If error_messages.Append(errors.Pop()) Loop MessageBox.Show($"Errors encountered: {error_messages.ToString()}") End If End Sub Private Async Sub LoginInteractively() Try Dim app As IPublicClientApplication = PublicClientApplicationBuilder.Create(client_id).WithRedirectUri(redirect_uri).WithTenantId(tenant_id).WithAuthority(AadAuthorityAudience.AzureAdMyOrg).Build() Dim authResult As AuthenticationResult = Nothing Dim accounts As IEnumerable(Of IAccount) = Await app.GetAccountsAsync() Dim performInterativeFlow As Boolean = False Try authResult = Await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync() Catch ex As MsalUiRequiredException performInterativeFlow = True Catch ex As Exception errors.Push(ex.Message) End Try If performInterativeFlow Then authResult = Await app.AcquireTokenInteractive(scopes).ExecuteAsync() End If If authResult.AccessToken <> String.Empty Then accessToken = authResult.AccessToken idToken = authResult.IdToken End If Catch ex As Exception errors.Push(ex.Message) Exit Sub End Try 'Since this thread runs under the ui thread, we need a delegate method to update the text boxes txtBoxAccessToken.BeginInvoke(New InvokeDelegate(AddressOf InvokeMethod)) Return End Sub Private Async Sub LoginClientCredentials() Dim authResult As AuthenticationResult = Nothing Try Dim app As IConfidentialClientApplication = ConfidentialClientApplicationBuilder.Create(client_id).WithClientSecret(client_secret).WithTenantId(tenant_id).WithAuthority(AadAuthorityAudience.AzureAdMyOrg).Build() authResult = Await app.AcquireTokenForClient(scopes).ExecuteAsync() Catch ex As Exception errors.Push(ex.Message) Exit Sub End Try accessToken = authResult.AccessToken idToken = "No id token given for this auth flow." 'Since this thread runs under the ui thread, we need a delegate method to update the text boxes txtBoxAccessToken.BeginInvoke(New InvokeDelegate(AddressOf InvokeMethod)) End Sub Private Delegate Sub InvokeDelegate() Private Sub InvokeMethod() txtBoxAccessToken.Text = accessToken txtboxIDToken.Text = idToken End Sub End Class
Running the project, you will get the main form displayed. You can click on the “Sign In Interactive” to be prompted for credentials:
and, once you’re signed in, the text boxes will populate with the id_token and access_token like so:
Clicking the “Sign in Client Credentials” will authenticate and get an access token using the scopes defined in the Scopes text box above it and then get an access token only:
Our MSAL.Net GitHub is located here and our official documentation is here.
My other VB.Net posts:
[…] URL: https://blogs.aaddevsup.xyz/2021/04/using-msal-in-a-vb-net-winforms-application/ […]
Thank you for this as it is helping me get up to speed with changing from basic to modern auth. Is the project missing a .resx file? I have downloaded the sample to see it in action so I can test form there but it fails with many errors from not having “My Project\Resources.resc” file in the project. Can you update it on GitHub to include this file?