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:

  1. log in a user interactively, prompting for credentials and getting the id_token and an access_token for the signed in user
  2. 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:

2 Thoughts to “Using MSAL in a VB.Net Winforms application”

  1. Savik Vas

    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?

Leave a Comment