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?