Microsoft.Identity.Client.MsalClientException: Failed to get user name

You might be using the following method to attempt Integrated Windows Auth while using Microsoft Authentication Library (MSAL)…

result = await app.AcquireTokenByIntegratedWindowsAuth(scopes)

and you are getting one of the following errors…

  • Microsoft.Identity.Client.MsalClientException: Failed to get user name —> System.ComponentModel.Win32Exception: No mapping between account names and security IDs was done
  • Microsoft.Identity.Client.MsalClientException: Failed to get user name —> System.ComponentModel.Win32Exception: Access Denied

Make sure you at least meet these minimum requirements:

  • You are running the app as a local Active Directory user and not a local computer user account.
  • Ensure that the device is joined to the domain

What is actually failing?

MSAL makes a call to GetUserNameEx function from secur32.dll…

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/01ecd12464007fc1988b6a127aa0b1b980bca1ed/src/client/Microsoft.Identity.Client/Platforms/Features/DesktopOS/WindowsNativeMethods.cs#L66

For more information about GetUserNameEx…

https://learn.microsoft.com/en-us/windows/win32/api/secext/nf-secext-getusernameexa

Windows is returning this error message. There is a number of reasons this can fail that is out of scope for this article.

Manually pass the username to AcquireTokenByIntegratedWindowsAuth

If you already know ahead of time what the username is, you can just pass it manually to MSAL… This may be by far the easiest and recommended solution.

result = await app.AcquireTokenByIntegratedWindowsAuth(scopes).WithUsername(“serviceaccount@contoso.com”)

You can try other ways to dynamically get the username….

Use System.Security.Principal.WindowsIdentity.GetCurrent()

Note: If returns a username with no domain, this will not work and return different errors. For proper Azure Active Directory integration, we need to pass username in the format of user principal name.

string username = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
result = await app.AcquireTokenByIntegratedWindowsAuth(scopes).WithUsername(username)

Use PublicClientApplication.OperatingSystemAccount.Username

Note: This attempts to access the Windows Account Broker to get the user signed into the device. This is not going to work if running on IIS or Windows Servers

string username = PublicClientApplication.OperatingSystemAccount.Username;
result = await app.AcquireTokenByIntegratedWindowsAuth(scopes).WithUsername(username)

For more information

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Integrated-Windows-Authentication

How to perform logging for both MSAL.Net and Microsoft Graph SDK

Microsoft Graph SDK has the ability to log out complete HTTP Requests and Reponses as documented here. The way this logging mechanism works is by implementing a custom HttpClient Message handler to intercept every HTTP Request and Response between the client application and the Microsoft Graph Service. Besides hooking into GraphServiceClient’s processing pipeline to do request and response tracing, one can also configure proxy info. See Customize the Microsoft Graph SDK service client – Microsoft Graph | Microsoft Docs for more detail. MSAL.Net is used in the GraphSerivceClient’s Authentication Provider. Consequently, logging in this library can provide valuable insight into authentication failure. In this post I will show how to enable both MSAL.Net logging and MS Graph SDK logging. The sample application I use here is a .Net Core 3.0 console application.

Logging techniques

For MSAL.Net logging, I use the Azure Blob Storage client SDK (v12) to write log entries into an Azure Blob, and for MS Graph SDK logging, I use Serilog library. Serilog has a number of logging providers (they are called “sinks”), which can cater to different logging environment including Application Insight, DocumentDB, Event Hub, etc.. In this blog I use 3 different logging sinks (providers): console logger, file logger, and Azure Blob Storage logger. These different logging providers are configured once at the beginning below. You don’t have to use all 3 and can add new one(s) or comment out the provider(s) you don’t need.

            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                // Serilog console logging
                .WriteTo.Console()
                // Serilog file logging
                .WriteTo.File(LoggingLocalPath, rollingInterval: RollingInterval.Day)
                // Serilog Azure Blob Storage logging
                .WriteTo.AzureBlobStorage(AzureStorageConnection, Serilog.Events.LogEventLevel.Verbose, MSGraphAzureBlobContainerName, MSGraphAzureBlobName)
                .CreateLogger();

Pre-requisites:

You will need the following for this blog:

Application Registration

Have an application registered in Azure AD. Configure ‘http://localhost’ as a redirect URI under ‘Mobile and desktop applications’ platform. This specific redirect URI is required for .Net Core applications.

For API permission, configure these delegated MS Graph permissions: User.Read.All and Application.ReadWrite.All and grant Admin Consent to these permissions since the sample will attempt to use the following MS Graph requests to get the logged in user information and create an App Registation:

GET https://graph.microsoft.com/beta/me and POST https://graph.microsoft.com/beta/applications

Note: the sample attempts to create an application so make sure the logged-in user is in one of the following Administrative roles otherwise the Application creation may fail due to insufficient permission: Application Developer, Application Administrator, Cloud Application Administrator, and Global Administrator. See Azure AD built-in roles – Azure Active Directory | Microsoft Docs for a list of built-in Azure AD roles.

Azure Storage

Create an Azure Storage for storing both MSAL and MS Graph SDK logs as Azure Blobs. For read/write access to Azure Storage you can use the Connection string in either key 1 or key 2 ‘Access keys’ blade:

The Application Code

The complete sample for this can be downloaded from this github repositiory. All the configuration options for this sample is in appsettings.json file below. The sample uses the code snippet from Ray’s blog to read in the configuration settings from the appsettings.json file. The sample uses MS Graph beta endpoint, so I use the Microsoft.Graph.Beta package. For v1 endpoint use Microsoft.Graph package instead of. Refer to Use the Microsoft Graph SDKs with the beta API – Microsoft Graph | Microsoft Docs for more info.

{
  "Azure": {
    "ClientId": "<Application ID>",
    "TenantId": "<Tenant ID>",
    "MSALAzureBlobContainerName": "msallogs", //Blob Container for MSAL log
    "MSALAzureBlobName": "MsalLog.txt", // blob file containing MSAL log
    "MSGraphAzureBlobContainerName": "msgraphlogs",  // Blob Container for MS Graph log
    "MSGraphAzureBlobName": "{yyyy}-{MM}-{dd} - msgraphlog.txt", // blob file containing MS Graph log
    "LoggingLocalPath": "C:/temp/msgraphlog.txt", // path for local file logging
    "AzureStorageConnection": "<Access key connection string for Azure Storage>",
    "Scopes": [ "https://graph.microsoft.com/.default" ]
  }
}

This sample uses both a GET and POST requests to show that you can log out the complete HTTP Requests and Responses including both the Headers and Body. For convenience I use the following 4 Helper Functions to get the information for each part so you can comment out the appropriate one should that info not be needed:

The above helper functions are used in the Logging HttpClient Message Handler shown below. Requests are logged before calling the DelegatingHandler’s SendAsync method and responses are logged after the call.

public class SeriLoggingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken)
        {
            HttpResponseMessage response = null;
            try
            {
                Log.Information("sending Graph Request");
                Log.Debug(GetRequestHeader(httpRequest));
                Log.Debug(GetRequestBody(httpRequest));
                response = await base.SendAsync(httpRequest, cancellationToken);
                Log.Information("Receiving Response:");
                Log.Debug(GetResponseHeader(response));
                Log.Debug(GetResponseBody(response));
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Something went wrong");
                if (response.Content != null)
                {
                   await response.Content.ReadAsByteArrayAsync();// Drain response content to free connections.
                }
            }
            return response;
        }
}

The following code shows how to set up the GraphServcieClient to use the Logging Handler.

            IPublicClientApplication publicClientApplication = PublicClientApplicationBuilder
                .Create(ClientId)
                .WithTenantId(TenantId)
                // Enable MSAL logging
                .WithLogging(MSALlogger, Microsoft.Identity.Client.LogLevel.Verbose, true)
                .WithRedirectUri("http://localhost")
                .Build();

            InteractiveAuthenticationProvider authProvider = new InteractiveAuthenticationProvider(publicClientApplication, Scopes);

            // get the default list of handlers and add the logging handler to the list
            var handlers = GraphClientFactory.CreateDefaultHandlers(authProvider);

            // Remove Compression handler
            var compressionHandler =
                handlers.Where(h => h is CompressionHandler).FirstOrDefault();
            handlers.Remove(compressionHandler);

            // Add SeriLog logger
            handlers.Add(new SeriLoggingHandler());

            InitializeBlobStorageForMSAL().Wait();

            var httpClient = GraphClientFactory.Create(handlers);
            GraphServiceClient graphClient = new GraphServiceClient(httpClient);

This sample will log out verbose sensitive information including PII Info and Access Tokens info so be cautious when sharing the debug logs. The debugging technique here can help troubleshooting problems in environments where an HTTP Fiddler trace may not easily be obtained. I use different logging techniques to write to Azure Blob Storage for both MSAL logging and MS Graph SDK logging since in my testing I have encountered a problem during authentication with the authentication prompt window not appearing when Serilog Azure Blob Storage sink is used for both scenarios.

Running the sample

the MS Graph Requests and Responses should appear in both the console window and the configured Directory location in appsettings.json. The name of the files should contain the date as below:

For Azure Blob Storage logging, the logs are in the following msallogs and msgraphlogs container:

Script errors running MSAL.Net in XBAP application

You may encounter script errors with the background text saying cookies are disabled when running MSAL code snippet similar to the following in a XAML Browser Application (XBAP) from Internet Explorer when performing Azure AD login

string tenantId = "<Tenant ID>";
string clientId = "<Application ID>";
string[] Scopes = new string[] { "User.Read" };
string errorMessage = string.Empty;
try
  {
  using (HttpClient httpClient = new HttpClient())
  {
     IPublicClientApplication publicClientApp = PublicClientApplicationBuilder.Create(clientId)
       .WithDefaultRedirectUri()
       .WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdMyOrg)
       .WithTenantId(tenantId)
       .Build();
        AuthenticationResult authenticationResult = null;
        var t = Task.Run(async () =>
          {
              try
              {
                 authenticationResult = await publicClientApp.AcquireTokenInteractive(Scopes)
                            .WithAccount(null)
                            .WithPrompt(Prompt.ForceLogin)
                            .ExecuteAsync();
                    }
                    catch (Exception ex)
                    {
                        errorMessage = "Error while getting token: " + ex.ToString();
                    }
                });
                t.Wait();

                if (authenticationResult != null)
                {
                    return authenticationResult.AccessToken;
                }
                else
                {
                    return errorMessage;
                }
            }
        }
        catch (Exception ex)
        {
            return ex.Message;
        }

Root Cause

XBAP Applications, although housed in Internet Explorer, runs in its own process space: PresentationHost.exe, which is a very tightly-controlled security container. XBAP Application uses the webBrowser control to host the Azure AD login page. This container host implements many security lockdown features to safe guard attacks which can be induced from the browser surface. These security restrictions include blocking cookies access which is required for the Azure AD login page to work. This security restriction causes the login page to break with the scripting error.

Resolution

Configure MSAL.Net to use the System Browser – Chromium Edge on Windows 10 (default is Embedded Browser in .Net framework) to launch the Azure AD login page. The following changes are what’s needed to use the System Browser

  1. Register ‘http://localhost’ (required) as a redirect URL under ‘Mobile and desktop applications’ platform
  2. Make the following highlighted change:
            try
            {
                using (HttpClient httpClient = new HttpClient())
                {
                    IPublicClientApplication publicClientApp = PublicClientApplicationBuilder.Create(clientId)
                                .WithRedirectUri("http://localhost")
                                .WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdMyOrg)
                                .WithTenantId(tenantId)
                                .Build();
                    AuthenticationResult authenticationResult = null;

                    var t = Task.Run(async () =>
                    {
                        try
                        {
                            authenticationResult = await publicClientApp.AcquireTokenInteractive(Scopes)
                                .WithAccount(null)
                                .WithPrompt(Prompt.ForceLogin)
                                .WithUseEmbeddedWebView(false)
                                .ExecuteAsync();
                        }
                        catch (Exception ex)
                        {
                            errorMessage = "Error while getting token: " + ex.ToString();
                        }
                    });

How to use an access token to connect to an Azure SQL Database

In Azure SQL, an access token can be used to authenticate a user or service principal. In this blog post, I will show you what is required for both and give some methods for acquiring the access token based on the scenario to set the access token parameter in the connection object.

Requirements: You have an Azure tenant and an Azure SQL database already configured and basic understanding of SSMS and creating queries, executing SQL commands. If you do need help creating one, you can follow this tutorial here. We are not going to be using any of the standard Connections strings that you find in the “Connection strings” blade for the server. Instead, we will use something like this ( with your values configured ):

Server=tcp:{sql server name}.database.windows.net, {port – default is 1433};Initial Catalog={database name};Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False

So, if my sql instance is called “RaysSQL” and my database called “RaysTest” and I left the default port at 1433 when creating the instance, my connection string would be this:

Server=RaysSQL.database.windows.net,1433;Initial Catalog=RaysTest;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False

First, lets setup Azure SQL to allow access by a specific user. To begin with, you will need an app registration created ( since we will be connecting from code ) to allow the application to obtain an access token for the user to the database. In the App Registrations pane, create a new app registration, select “Accounts in this organization directory only”, and for the Redirect URI, select “Web” and enter “http://localhost” ( this is the redirect my sample app is using ). Next, take note of the application id ( client id ) as this will be needed for the sample app. Then, go to “Certificates & secrets” and configure a client secret. Take note of that new secret as well as you will need it for the sample. Finally, go to API permissions blade, click on the “Add a permission” button and in the Request API permissions slide out, click on “APIs my organization uses” and search for “Azure Sql Database”.

Click on Delegated permissions and add the permission “user_impersonation”.

For this first scenario, I am going to take an existing Azure user that is in my tenant and add him to the database and give him the db_datareader role ( to allow the user to run select queries ). Make sure that the user is valid and can sign in to the portal properly. Then, in SSMS, connect to the database and run these 2 commands ( where my dev@mytenant.com is my users login that I am going to add to the database ):

CREATE USER [dev@mytenant.com] FROM EXTERNAL PROVIDER;

EXECUTE sp_addrolemember db_datareader, [dev@mytenant.com];

Now that the user has been added, you can obtain an access token for this user. Lets take a look at the sample app code: The application has a class I called Azure_SQL. In this class is where you will configure the settings for your tenant and application.

		private const string TENANT_ID = "{your tenant id or domain name}";
		private const string CLIENT_ID = "{your app id or client id}"; // Azure_SQL
		private const string CLIENT_SECRET = "{the client secret you configured}";
		private const string REDIRECT_URI = "http://localhost";
		private const string CONNECTION_STRING = "Server=tcp:RaysSQL.database.windows.net,1433;Initial Catalog=RaysTest;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False";
		private static readonly string[] scopes = new string[] { "https://database.windows.net/.default" };

I then have a series of methods to get access tokens via different ways but we will first focus on the user scenario and for that, we would be using the method “GetAccessToken_UserInteractive()” This will use MSAL which will prompt the user to sign in using the default browser ( since this is a console application ). Once the sign in is complete, you can close the browser window.

		public static async Task<string> GetAccessToken_UserInteractive()
		{

			IPublicClientApplication app = PublicClientApplicationBuilder
				.Create(CLIENT_ID)
				.WithAuthority(AzureCloudInstance.AzurePublic, TENANT_ID)
				.WithRedirectUri(REDIRECT_URI)
				.Build();			

			string accessToken = string.Empty;

			AuthenticationResult authResult = null;
            IEnumerable<IAccount> accounts = await app.GetAccountsAsync();
						
            try
            {
				authResult = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
				accessToken = authResult.AccessToken;
            } catch (MsalUiRequiredException)
            {
				authResult = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
				accessToken = authResult.AccessToken;
			} catch (Exception ex)
            {
				Console.WriteLine($"Authentication error: {ex.Message}");
            }

			Console.WriteLine($"Access token: {accessToken}\n");

			return accessToken;
		}

I then have a method to make a database call, passing in the access token as a parameter which will retrieve the list of fake users in my sample table. That method is called GetUsernames(string access_token). This is the main muscle of the app. It will take any access token ( that is valid for accessing this database ) and run the query.

		public static Dictionary<Guid,string> GetUsernames(string access_token)
		{
			using (var connection = new SqlConnection(CONNECTION_STRING))
			{
				connection.AccessToken = access_token;

                try
                {
					connection.Open();

                } catch (Exception ex)
                {
					Console.WriteLine($"DB Connection error: {ex.Message}");
                }
				var cmd = connection.CreateCommand();
				cmd.CommandText = "SELECT TOP 10 id, user_name from Users Order by user_name ASC";
				Console.WriteLine($"Executing command: {cmd.CommandText}...\n");

				var reader = cmd.ExecuteReader();
				var users = new Dictionary<Guid, string>();
				
				if (reader.HasRows)
                {
					while (reader.Read())
					{
						users.Add(reader.GetGuid(reader.GetOrdinal("id")), reader.GetString(reader.GetOrdinal("user_name")));
					}
                }

				return users;
			}
		}

As you can see, I am simply making a new SqlConnection using the custom connection string and then on the connection object, we set the access token and the connection will use that token for authentication. In Program.cs, in Main(), I create a variable for the access token, a variable to receive the query results and then set the access_token = Azure_SQL.GetAccessToken_UserInteractive().Result; I set the users variable to the GetUserNames method, passing the access token. I then loop through the users variable to output the data to the console.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Console_Connect_SQL
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!\n");

            string access_token = string.Empty;
            Dictionary<Guid, string> users = new Dictionary<Guid, string>();

            // get the data from the database with a user signin....
            Console.WriteLine("Signing in as a user...\n");
            access_token = Azure_SQL.GetAccessToken_UserInteractive().Result;
            users = Azure_SQL.GetUsernames(access_token);

            foreach(KeyValuePair<Guid,string> user in users)
            {
                Console.WriteLine($"ID: {user.Key.ToString()} USER: {user.Value.ToString()}");
            }
        }
    }
}

Fun tip: the console application will output the access_token. You can copy that token and go to https://jwt.ms , paste it in the top and see what the values are in the token.

Note: Managed SQL instances, as in this blog example, do not support authentication based on an Azure Group: Azure Active Directory authentication – Azure SQL Database | Microsoft Docs

For logging in as a service principal ( app registration – using the client credentials grant flow ) it is pretty much the same process as above. Create an app registration for the service principal, create the client secret ( I am using the same app registration for both so that is why I have the client secret on my app registration ). Then, when running the sql script to create the user, you will just use the app registrations display name as the user. In the code, the main difference is, you will be using the client id and the client secret to sign in instead of prompting the user. So, using the same app registration we have created above, you can use this code to get an access token:

		public static async Task<string> GetAccessToken_ClientCredentials()
		{

			IConfidentialClientApplication app = ConfidentialClientApplicationBuilder
				.Create(CLIENT_ID)
				.WithClientSecret(CLIENT_SECRET)
				.WithAuthority(AzureCloudInstance.AzurePublic, TENANT_ID)
				.WithRedirectUri(REDIRECT_URI)
				.Build();

			string accessToken = string.Empty;

			AuthenticationResult authResult = null;

			try
			{
				authResult = await app.AcquireTokenForClient(scopes).ExecuteAsync();
				accessToken = authResult.AccessToken;
			}
			catch (MsalClientException ex)
			{
				Console.Write($"Error obtaining access token: {ex.Message}");
				
			}

			Console.WriteLine($"Access token: {accessToken}\n");

			return accessToken;
		}

The sample code will execute two times. First, it will prompt for a user sign in to show the user flow and it executes the desired query ( make sure you have a table with some records in it and the query is valid ). Then, it will execute as a service principal and again, run a query and output the data.

My sample data is using the “users” format to make it easy to populate a pre-existing data structure. If you want to create the same data as I have, the GitHub will have a SQL script to both create the table and populate it will some sample users.

Another note, Azure SQL does have a firewall configured so you will need to add your IP address in the portal. If you log in with SSMS with a user in the same tenant, you may be prompted to add your IP address automatically.

That is it. In summary, there isn’t much you need to do on the Azure SQL side except for running the script to either add a user or a service principal to be able to login to the database and then the script to add that user or service principal to a role that would allow the action you need such as the db_datareader role to allow SELECT queries to be ran.

Using MSAL in a VB.Net Winforms application

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:

Using PowerShell to configure a signing certificate for a SAML-based SSO Enterprise Application

In my last blog post I talked about how to use PowerShell to instantiate an MSAL Confidential Client Application to acquire an access token using Client Credentials Grant flow. In this post we will use PowerShell to instantiate an MSAL Public Client Application to perform an Authorization Code Grant flow to obtain a delegated permission Access Token for Microsoft Graph. We will then use that access token to call Microsoft Graph to configure a signing certificate for our SAML Application Service Principal. Just a quick refresher that a certificate is always required when setting up SAML Sigle Sign-On feature for an Enterprise App in Azure AD.

Pre-requisites

To run the script in this blog you should have the following:

1) A SAML-based SSO Enterprise Application you want to configure a signing certificate for. Get the Object ID of this Application in the Properties blade of the Enterprise App – we will need it for the script. The script in this blog performs the same thing as doing the following UI action in the portal:

Azure Active Directory -> Enterprise Application -> Pick the correct App -> Single sign-on -> click ‘Edit’ link in the SAML Signing Certificate section -> Import Certificate


If you are trying to automate a SAML-based SSO Application, take a look at the documentation Automate SAML-based SSO app configuration with Microsoft Graph API. This blog can help with step 4, Configure Signing Certifcate, of that article.

2) An app registration to sign in a user and get an access token for Microsoft Graph. Get the Application (client) ID of this app in the Overview section – we will need it for the script. This application should have the following App Registration configuration:

Supported account typesAccounts in this organizational directory only
Redirect URIshttp://localhost under ‘Mobile and desktop applications’ platform
API permissionsMicrosoft Graph – Delegated permissions: Application.ReadWrite.All and User.Read
(Make sure you grant Admin consent to these permissions)

3) The user who logs in to get the MS Graph Access Token should be one of the following Azure AD Administrative Role – this is required in order to make a change to the Service Principal:

  • Cloud Application Administrator
  • Application Administrator
  • Global Administrator

Signing Certificate

We will need to have a certificate to configure for our application. You can either create a self-signed certificate (using either PowerShell or OpenSSL as shown below) or obtain one from your Trusted Certificate Authority. We will need the following certificate components for our script:

  1. public key (typically a .cer file)
  2. private key in PKCS#12 format (in .pfx file)
  3. password for the private key (pfx file)

Note: It is important to have the private key in PKCS#12 format since Azure AD does not support other format types. Using the wrong format can result in the the error “Invalid certificate: Key value is invalid certificate” when using MS Graph to PATCH the Service Principal with a keyCredentials containing the certificate info

Using PowerShell to create a self-signed certificate

The following PowerShell script can be used to create a self-signed certificate and then export both the private key and public key out to a .pfx and and a .cer files

# fqdn - this is used for the 'issued to' and 'issued by' field of the certificate
# pwd - password for exporting the certificate private key
# location - path to folder where both the pfx and cer file will be written to, for example C:\users\john\Documents

Param(
    [Parameter(Mandatory=$true)]
    [string]$fqdn,
    [Parameter(Mandatory=$true)]
    [string]$pwd,
    [Parameter(Mandatory=$true)]
    [string]$location
) 

if (!$PSBoundParameters.ContainsKey('location'))
{
    $location = "."
} 

$cert = New-SelfSignedCertificate -certstorelocation cert:\currentuser\my -DnsName $fqdn
$pwdSecure = ConvertTo-SecureString -String $pwd -Force -AsPlainText
$path = 'cert:\currentuser\my\' + $cert.Thumbprint
$cerFile = $location + "\\" + $fqdn + ".cer"
$pfxFile = $location + "\\" + $fqdn + ".pfx" 

Export-PfxCertificate -cert $path -FilePath $pfxFile -Password $pwdSecure
Export-Certificate -cert $path -FilePath $cerFile

Using OpenSSL to create a self-signed certificate

If you don’t have OpenSSL installed already, refer to the OpenSSL documentation for building and installation instruction. For Windows users, this StackOverflow discussion has some useful information on how to download OpenSSL for Windows.

1) Run the following openssl command to create a public key file (crt) and private key file (pem) with your info.  See https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm for some openssl reference guide.

openssl req-x509 -sha256 -days 365 -newkey rsa:2048 -keyout "C:\Users\path\privateKey.key" -out "C:\Users\path\certificate.crt" -subj '/C=your country/ST=your state/L=your locality/O=Your Company, Inc./OU=your Organizational Unit/CN=yourdomain.com'

2) convert the pem file to pfx file with your info:

openssl pkcs12 -export -out "C:\Users\path\certificate.pfx" -inkey "C:\Users\path\privateKey.key" -in "C:\Users\path\certificate.crt" -passout pass:your password -passin pass:your password

3) convert the crt file to DER encoded binary X.509 .cer file:

On Windows, double-click on the crt file to launch the certificate wizard. Go to ‘Details’ tab and click on ‘Copy to File…’ button:

Click ‘Next’ on the ‘Welcome to the Certificate Export Wizard’ page. Choose ‘DER encoded binary X.509 (cer)’ option and save to cer file.

Configuring the signing certificate using the MS Graph request

At this point we should have a pfx file, a cer file, and a pfx file password. We will use all of this information to construct the following MS Graph request:

PATCH https://graph.microsoft.com/v1.0/serviceprincipals/<Service Principal Object ID>

Request body:

Note: This is not a complete request body. For brevity, I am only emphasizing the main part related to private and public key.

{
    "keyCredentials":[
        {
	    ...
            "type": "AsymmetricX509Cert",
            "usage": "Sign",
            "key": Base64-encoded private key

        },
        {
	    ...
            "type": "AsymmetricX509Cert",
            "usage": "Verify",
            "key": Base64-encoded public key

        }
    ],
    "passwordCredentials": [
        {
	    ...
            "secretText": "password for the pfx file"
        }
    ]
}

Note: the passwordCredentials is required in the above request. Omitting it may result in the error “The value for the property \”usage\” in one of your credentials is invalid. Acceptable values are Sign, Verify.”

There is also a PowerShell script to generate the above json body from the certificate files located here.

Use the following PowerShell script with your info to send a PATCH request to configure the signing certificate for this SAML Application. The script will attempt to download MSAL.Net module if it does not already exist on the machine and then use MSAL.Net to sign in to Azure AD. You may need to run the following commands first to update both Nuget package and PowerShellGet.

## Update Nuget Package and PowerShellGet Module
Install-PackageProvider NuGet -Force
Install-Module PowerShellGet -Force

Note: this script will replace the current keyCredentials and passwordCredentials configured for this Service Principal. If you need to do an append operation, you will need to get the current selection first and append it to the payload.

$ClientID = "<client ID>"
$loginURL       = "https://login.microsoftonline.com"
$tenantdomain   = "<tenant>.onmicrosoft.com"
$redirectURL = "http://localhost" # this reply URL is needed for PowerShell Core 
[string[]] $Scopes = "https://graph.microsoft.com/.default"
$pfxpath = "C:\Users\path\certificate.pfx" # path to pfx file
$cerpath = "C:\Users\path\certificate.cer" # path to cer file
$SPOID = "<Service Principal Object ID>"
$graphuri = "https://graph.microsoft.com/v1.0/serviceprincipals/$SPOID"
$password = "<password for pfx file>"  # password for the pfx file
$CertDisplayName = "CN = <Your Cert Display Name>" # "CN=contoso"


# choose the correct folder name for MSAL based on PowerShell version 5.1 (.Net) or PowerShell Core (.Net Core)
# script is tested on both PS 5.1 and PS Core 7

if ($PSVersionTable.PSVersion.Major -gt 5)
    { 
        $core = $true
        $foldername =  "netcoreapp2.1"
    }
else 
    { 
        $core = $false
        $foldername = "net45"
    }


# Download MSAL.Net module to a local folder if it does not exist there
if ( ! (Get-ChildItem $HOME/MSAL/lib/Microsoft.Identity.Client.* -erroraction ignore) ) {
    install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.Identity.Client -Destination $HOME/MSAL/lib -force -forcebootstrap | out-null
}
 
# Load the MSAL assembly -- needed once per PowerShell session
[System.Reflection.Assembly]::LoadFrom((Get-ChildItem $HOME/MSAL/lib/Microsoft.Identity.Client.*/lib/$foldername/Microsoft.Identity.Client.dll).fullname) | out-null
 
$global:app = $null
 
$ClientApplicationBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientID)
[void]$ClientApplicationBuilder.WithAuthority($("$loginURL/$tenantdomain"))
[void]$ClientApplicationBuilder.WithRedirectUri($redirectURL)


$global:app = $ClientApplicationBuilder.Build()
 
Function Get-GraphAccessTokenFromMSAL {
    [Microsoft.Identity.Client.AuthenticationResult] $authResult  = $null
    $AquireTokenParameters = $global:app.AcquireTokenInteractive($Scopes)
    [IntPtr] $ParentWindow = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle
    if ($ParentWindow)
    {
        [void]$AquireTokenParameters.WithParentActivityOrWindow($ParentWindow)
    }
    try {
        $authResult = $AquireTokenParameters.ExecuteAsync().GetAwaiter().GetResult()
    }
    catch {
        $ErrorMessage = $_.Exception.Message
        Write-Host $ErrorMessage
    }
    
    return $authResult
}
 
$myvar = Get-GraphAccessTokenFromMSAL
if ($myvar)
{
    $GraphAccessToken = $myvar.AccessToken
    Write-Host "Access Token: " $myvar.AccessToken

    #  this is for PowerShell Core
    $Secure_String_Pwd = ConvertTo-SecureString $password -AsPlainText -Force

    # reading certificate files and creating Certificate Object
    if ($core)
    {
        $pfx_cert = get-content $pfxpath -AsByteStream -Raw
        $cer_cert = get-content $cerpath -AsByteStream -Raw
        $cert = Get-PfxCertificate -FilePath $pfxpath -Password $Secure_String_Pwd
    }
    else 
    {
        $pfx_cert = get-content $pfxpath -Encoding Byte
        $cer_cert = get-content $cerpath -Encoding Byte
        # Write-Host "Enter password for the pfx file..."
        # calling Get-PfxCertificate in PowerShell 5.1 prompts for password
        # $cert = Get-PfxCertificate -FilePath $pfxpath
        $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxpath, $password)
    }

    # base 64 encode the private key and public key
    $base64pfx = [System.Convert]::ToBase64String($pfx_cert)
    $base64cer = [System.Convert]::ToBase64String($cer_cert)

    # getting id for the keyCredential object
    $guid1 = New-Guid
    $guid1 = $guid1.ToString()
    $guid2 = New-Guid
    $guid2 = $guid2.ToString()

    # get the custom key identifier from the certificate thumbprint:
    $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256')
    $hash = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($cert.Thumbprint))
    $customKeyIdentifier = [System.Convert]::ToBase64String($hash)

    # get end date and start date for our keycredentials
    $endDateTime = ($cert.NotAfter).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
    $startDateTime = ($cert.NotBefore).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )

    # building our json payload
    $object = [ordered]@{    
    keyCredentials = @(       
         [ordered]@{            
            customKeyIdentifier = $customKeyIdentifier
            endDateTime = $endDateTime
            keyId = $guid1
            startDateTime = $startDateTime  
            type = "AsymmetricX509Cert" 
            usage = "Sign"
            key = $base64pfx
            displayName = $CertDisplayName  
        },
        [ordered]@{            
            customKeyIdentifier = $customKeyIdentifier
            endDateTime = $endDateTime
            keyId = $guid2
            startDateTime = $startDateTime  
            type = "AsymmetricX509Cert" 
            usage = "Verify"
            key = $base64cer
            displayName = $CertDisplayName    
        }
        )  
    passwordCredentials = @(
        [ordered]@{
            customKeyIdentifier = $customKeyIdentifier
            keyId = $guid1            
            endDateTime = $endDateTime
            startDateTime = $startDateTime
            secretText = $password 
        }
    )
    }

    $json = $object | ConvertTo-Json -Depth 99
    Write-Host "JSON Payload:"
    Write-Output $json

    # Request Header
    $Header = @{}
    $Header.Add("Authorization","Bearer $($GraphAccessToken)")
    $Header.Add("Content-Type","application/json")

    try 
    {
        Invoke-RestMethod -Uri $graphuri -Method "PATCH" -Headers $Header -Body $json
    } 
    catch 
    {
        # Dig into the exception to get the Response details.
        # Note that value__ is not a typo.
        Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ 
        Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
    }

    Write-Host "Complete Request"
}
else 
{
    Write-Host "Fail to get Access Token"
}

If the script completes successfully we should see the thumbprint of our certificate showing up for this application:


Last Updated on 8/19/2022

Using MSAL.Net to perform the client credentials flow with a certificate instead of a client secret in a .NetCore console appliction.

The sample files for this post can be found in this GitHub repository: https://github.com/RayGHeld/NetCore_ClientCredentials_withCert

There is also a Powershell script there to create a certificate for this sample.

You can perform the OAuth2 client_credentials grant flow to sign in as an application for your automated type of services. This flow does not require an interactive user to authenticate and should only be run in secure environments. There are 2 methods to handle securing the authentication portion: 1) use a client secret and 2) use a certificate. This blog post will show you how to authenticate using this flow in a .NetCore console application and a certificate.

The flow that is being followed is documented here. To begin the process, you will need a certificate and for this post, I will be using a self signed certificate using Powershell. I borrowed the Powershell example from this blog post and made modifications for this post so that the certificate that is created lines up with code since I am demonstrating multiple ways to get the certificate from the certificate store.

The first step is to create the certificate. You can find the Powershell script in the GitHub link at the start of this post. I have masked my tenant id and client id. Please remember to use your own. Also, note the folder location that the cert is created in ( you can change that value as well ) but you will need that in the .NetCore app.

# Create self-signed certificate and export pfx and cer files.  You will need to run the script in PowerShell Admin window to create this self-signed certificate

# please be aware that running this script will create a new cert each time in your certificate store.  
# If you need to rerun it, I recommend deleting the previous certs first in the store.
# Duplicates are only really an issue if you are using the GetCertByName method

$tenant_id = 'xxxx' # tenant id is used for the cert name / subject name and matches the .netcore code sample that goes with this script
$client_id = 'xxxx' # client id is used for the cert name / subject name and cert password and matches the .netcore code sample that goes with this script
# Change the following file path to your own
$FilePath = 'C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\'
$StoreLocation = 'CurrentUser' # be aware that LocalMachine requires elevated privileges
$expirationYears = 1

$SubjectName = $tenant_id + '.' + $client_id
$cert_password = $client_id

$pfxFileName = $SubjectName + '.pfx'
$cerFileName = $SubjectName + '.cer'

$PfxFilePath = $FilePath + $pfxFileName
$CerFilePath = $FilePath + $cerFileName

$CertBeginDate = Get-Date
$CertExpiryDate = $CertBeginDate.AddYears($expirationYears)
$SecStringPw = ConvertTo-SecureString -String $cert_password -Force -AsPlainText 
$Cert = New-SelfSignedCertificate -DnsName $SubjectName -CertStoreLocation "cert:\$StoreLocation\My" -NotBefore $CertBeginDate -NotAfter $CertExpiryDate -KeySpec Signature
Export-PfxCertificate -cert $Cert -FilePath $PFXFilePath -Password $SecStringPw 
Export-Certificate -cert $Cert -FilePath $CerFilePath 

Once the certificate is created, you can find it in your certificate store on the computer the script was ran from. To get to the certificate store on the computer, I simply did a search in the tool bar search for “Certificate” and then used the Manage user certificates link that appeared.

Once the certificate is created, you can upload that to an app registration in the Azure portal.

For the .netcore application, you will need to make similar changes to the client id, tenant id and also the folder path. Also, you can enter the thumbprint here as well since there is a method to search for the certificate by thumbprint ( which is actually the best way of doing this ).

        static String accessToken = string.Empty;
        static String client_id = "xxxx";
        static String cert_password = client_id;
        static String tenant_id = "xxxx";
        static String authority = $"https://login.microsoftonline.com/{tenant_id}";
        static List<String> scopes = new List<String>() { $"https://graph.microsoft.com/.default" };
        static String thumbprint = "xxxxxx";

There are 3 methods that will search for the certificate. I have 2 of them commented out ( you can only use one method here so choose one, comment the other 2 out when testing ) and they are part of the confidential client builder class — see the method called GetToken.

            IConfidentialClientApplication ica = ConfidentialClientApplicationBuilder.Create(client_id)
                .WithCertificate(GetCert(@"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\"))
                //.WithCertificate(GetCertByThumbprint( StoreLocation.CurrentUser, thumbprint ) )
                //.WithCertificate( GetCertByName( StoreLocation.CurrentUser, $"{tenant_id}.{client_id}" ) )
                .WithTenantId(tenant_id)
                .WithAuthority(authority)
                .Build();

And, that is it. Your application will perform the client credentials flow using the certificate you created. This demo app does not make any api calls. It is only getting a access token. You can copy that access token and decode it at http://jwt.ms to see what it contains.

The full module with the functions being called to get a certificate:

using Microsoft.Identity.Client;

using System;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace MSAL_ClientCred_Cert
{

    /*
     * Reference: https://blogs.aaddevsup.xyz/2019/01/console_keyvault_clientcredentials/ -- for instructions on how to create a certificate
     */
    class Program
    {
        static String accessToken = string.Empty;
        static String client_id = "xxx";
        static String cert_password = client_id;
        static String tenant_id = "xxx.com";
        static String authority = $"https://login.microsoftonline.com/{tenant_id}";
        static List<String> scopes = new List<String>() { $"https://graph.microsoft.com/.default" };
        static String thumbprint = "xxx";

        static void Main(string[] args)
        {

            Console.WriteLine("Hello World!");
            GetToken();
            Console.ReadKey();
        }

        static async void GetToken()
        {
            accessToken = string.Empty;

            IConfidentialClientApplication ica = ConfidentialClientApplicationBuilder.Create(client_id)
                .WithCertificate(GetCert(@"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\"))
                //.WithCertificate(GetCertByThumbprint( StoreLocation.CurrentUser, thumbprint ) )
                //.WithCertificate( GetCertByName( StoreLocation.CurrentUser, $"{tenant_id}.{client_id}" ) )
                .WithTenantId(tenant_id)
                .WithAuthority(authority)
                .Build();

            AuthenticationResult authResult = null;

            try
            {
                authResult = await ica.AcquireTokenForClient(scopes).ExecuteAsync();
                accessToken = authResult.AccessToken;

                Console.WriteLine($"Access Token: {accessToken}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"MSAL Error: {ex.Message}");
            }


        }

        /// <summary>
        /// Gets a certificate by the .pfx name from the file system
        /// </summary>
        /// <param name="basePath">The folder where the cert is stored -- filename is derived from the settings in this application</param>
        /// <returns></returns>
        static X509Certificate2 GetCert(String basePath)
        {
            String certPath = $"{basePath}{tenant_id}.{client_id}.pfx";
            X509Certificate2 cer = new X509Certificate2(certPath, cert_password, X509KeyStorageFlags.EphemeralKeySet);

            return cer;
        }

        /// <summary>
        /// Gets a certificate based on the name -- please note, there is an issue using this method
        /// if there are multiple certs with the same name.
        /// </summary>
        /// <param name="storeLoc"></param>
        /// <param name="subjectName"></param>
        /// <returns></returns>
        static X509Certificate2 GetCertByName(StoreLocation storeLoc, String subjectName)
        {

            //String certPath = @"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\MSAL_ClientCred_Cert.cer";
            //X509Certificate2 cer = new X509Certificate2( certPath, client_id, X509KeyStorageFlags.EphemeralKeySet );
            //store.Certificates.Add( cer ); -- use this to import the certificate to the store

            // read certificates already installed
            X509Store store = new X509Store(storeLoc);
            X509Certificate2 cer = null;

            store.Open(OpenFlags.ReadOnly);

            // look for the specific cert by subject name -- returns the first one found, if any.  Also, for self signed, set the valid parameter to 'false'
            X509Certificate2Collection cers = store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false);
            if (cers.Count > 0)
            {
                cer = cers[0];
            };
            store.Close();

            return cer;

        }

        /// <summary>
        /// Gets a certificate by Thumbprint -- this is the preferred method
        /// </summary>
        /// <param name="storeLoc"></param>
        /// <param name="thumbprint"></param>
        /// <returns></returns>
        static X509Certificate2 GetCertByThumbprint(StoreLocation storeLoc, String thumbprint)
        {
            X509Store store = new X509Store(storeLoc);
            X509Certificate2 cer = null;

            store.Open(OpenFlags.ReadOnly);

            // look for the specific cert by thumbprint -- Also, for self signed, set the valid parameter to 'false'
            X509Certificate2Collection cers = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
            if (cers.Count > 0)
            {
                cer = cers[0];
            };
            store.Close();

            return cer;
        }
    }
}

Using PowerShell to get Azure AD audit logs

In my previous blog, I talked about how to use PowerShell with Microsoft Graph Reporting API. In that blog, I used the Client Credentials grant flow to acquire an access token for Microsoft Graph against the V1 endpoint. Also the code sample in that blog only works if all the reporting data result set is small. In this blog I’ll discuss how to get a Microsoft Graph access token using Client Credentials grant flow against the V2 endpoint and also add paging support to handle large result set which can span multiple pages.

As a pre-requisite, you will have to create an Application Registration in Azure Active Directory and configure the application to have Microsoft Graph Application Permission “AuditLog.Read.All” as laid out in my last blog. Remember to grant admin consent to the Microsoft Graph permissions as well.

Getting an Access Token

There are a couple of ways you can get an access token.

  1. Build a raw HTTP POST request for the v2 token endpoint (code below)
$ClientID = "<client id>"
$ClientSecret = "<client secret>"
$loginURL       = "https://login.microsoftonline.com"
$tenantdomain   = "<tenant name>.onmicrosoft.com" 
$scope1 = "https://graph.microsoft.com/.default"

$body = @{grant_type="client_credentials";scope=$scope1;client_id=$ClientID;client_secret=$ClientSecret}
$oauth = Invoke-RestMethod -Method Post -Uri $("$loginURL/$tenantdomain/oauth2/v2.0/token") -Body $body
Write-Host "Access Token: " $oauth.access_token
  1. Use MSAL.Net to acquire a token

I actually like this approach better than the first approach mainly due to MSAL’s capability of always getting a valid access token. By default, an access token is only valid for 1 hour. This can be problematic (especially with the first approach) if an application takes longer than 1 hour to page through a large result set with the same token. Once the token expires, MS Graph will return an error with regards to expired token. To be able to handle this scenario more effectively the application will have to take care of checking for token validity and sending out new request to get a fresh access token should the current one becomes expired. With MSAL, you don’t have to worry about token expiration. MSAL maintains its own token cache. With every AcquireTokenxxx API call, MSAL returns the token from its cache only if the token is still valid. If the existing cached token is about to expire or has expired, MSAL will automatically send out a new request to get a fresh token and return that new token to the client. Below is a sample PowerShell snippet using MSAL to acquire an access token for Microsoft Graph and then use the token for getting user sign-ins report.

Note: This PowerShell script has been tested with MSAL.Net v4.11.0 and Windows PowerShell v5.1. It might not be compatible with PowerShell Core.

$ClientID = "<client id>"
$ClientSecret = "<client secret>"
$loginURL       = "https://login.microsoftonline.com"
$tenantdomain   = "<tenant name>.onmicrosoft.com"
[string[]] $Scopes = "https://graph.microsoft.com/.default"

# Download MSAL.Net module to a local folder if it does not exist there
if ( ! (Get-ChildItem $Env:USERPROFILE/MSAL/lib/Microsoft.Identity.Client.* -erroraction ignore) ) {
    install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.Identity.Client -Destination $Env:USERPROFILE/MSAL/lib -force -forcebootstrap | out-null
}

# Load the MSAL assembly -- needed once per PowerShell session
[System.Reflection.Assembly]::LoadFrom((Get-ChildItem $Env:USERPROFILE/MSAL/lib/Microsoft.Identity.Client.*/lib/net45/Microsoft.Identity.Client.dll).fullname) | out-null

$global:app = $null

$ClientApplicationBuilder = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($ClientID)
[void]$ClientApplicationBuilder.WithAuthority($("$loginURL/$tenantdomain"))
[void]$ClientApplicationBuilder.WithClientSecret($ClientSecret)
$global:app = $ClientApplicationBuilder.Build()

Function Get-GraphAccessTokenFromMSAL {
    [Microsoft.Identity.Client.AuthenticationResult] $authResult  = $null
    $AquireTokenParameters = $global:app.AcquireTokenForClient($Scopes)
    try {
        $authResult = $AquireTokenParameters.ExecuteAsync().GetAwaiter().GetResult()
    }
    catch {
        $ErrorMessage = $_.Exception.Message
        Write-Host $ErrorMessage
    }
   
    return $authResult
}

$myvar = Get-GraphAccessTokenFromMSAL
Write-Host "Access Token: " $myvar.AccessToken
$global:headerParams = @{}

Function Set-AuthHeader {
$global:headerParams = @{
    "Authorization" = "Bearer $((Get-GraphAccessTokenFromMSAL).AccessToken)"
    }
}

$queryUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns"
$counter = 1
$d=(get-date -Uformat %Y%m%d).ToString()
$ReportPath = "$Env:USERPROFILE\$($d)_Report.csv"

do 
{
    
    Set-AuthHeader
    Write-Host "Getting Page " $counter
    $result = Invoke-RestMethod -Method Get -Uri $queryUrl -Headers $global:headerParams
    $content = $result.value
    # Write out content to csv file
    $content | Export-Csv -Path $ReportPath -Append -NoTypeInformation
    # Get the next page
    $queryUrl = $result."@odata.nextLink"
    $counter++

} 
until (!($queryUrl))

Write-Host "The report can be found at " $ReportPath
Write-Host "end"

Different Types of Directory Reports

You can change the Microsoft Graph query above to get other types of Directory reports. Below are all the different report types available under the auditLogs path.

User Sign-ins reports

https://docs.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-beta&tabs=http

The query used in the above PowerShell code is for getting a list of all the user sign-ins report. Below are some other example queries using $filter:


List all sign-ins where appDisplayname starts with ‘Postman

https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter=startswith(appDisplayName,'Postman')

List all user sign-ins between 2 different times

https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter=createdDateTime ge 2020-04-06T11:54:00Z and createdDateTime le 2020-04-08T11:54:00Z


Note: The ‘$’ character in PowerShell has a special meaning to denote PowerShell variables. To use the above queries in PowerShell you need to escape the ‘$’ character using an Grave Accent or tick mark (`) character. An example of this is below:

$url = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=startswith(appDisplayName,'Postman')"

Directory Audit reports

https://docs.microsoft.com/en-us/graph/api/directoryaudit-list?view=graph-rest-1.0&tabs=http

Use the query below to get a list of audit logs generated by Azure Active Directory.

https://graph.microsoft.com/v1.0/auditLogs/directoryAudits

Provisioning reports

https://docs.microsoft.com/en-us/graph/api/provisioningobjectsummary-list?view=graph-rest-beta&tabs=http

Use the query below to get all the provisioning events in your tenant

https://graph.microsoft.com/beta/auditLogs/provisioning

Query Parameters

As of the time of this writing the Reporting API only support these query clauses: $filter, $top, and $skiptoken. Depending on object types, only certain attributes support $filter query. Check the documentation on each audit log type for more info. The $select query is not supported either so if you want to limit the number of fields saved out to the Excel file you can modify line 59 in the above PowerShell snippet as followed

# only save out these fields to csv file:  appDisplayName, appId, and ipAddress
$content | select appDisplayName, appId, ipAddress | Export-Csv -Path $Env:USERPROFILE\Report.csv -Append -NoTypeInformation

Other Report Types

There are other types of reports existing under the “reports” path. These include Office 365 usage, ADFS application activity, Application sign-in summary, and Registration and usage. These endpoints will require different API Permission set. Check out the Reports documentation for more info.

References

Microsoft-Authentication-with-PowerShell-and-MSAL
Pull Azure AD Audit Report- Updated
Fetch Data from Microsoft Graph with PowerShell (Paging Support)
Adam Edwards’s PowerShellGraphDemo

VB.NET – Use MSAL.Net in a Console Application to Authenticate to Azure

At one point in recent history, VB.Net was one of the most popular development languages around. In fact, there are many legacy applications written in VB.Net and even still, VB.Net is still in the top 10 languages ( source: https://www.techworm.net/2018/02/popular-programming-languages-2018-according-tiobe-pypl.html ).  I will show a simple console application using VB.Net to authenticate using MSAL.Net

Of course, the first step, as in everything Azure, starts with setting up an app registration to use for Authentication. For this demonstration, I created a single tenant app registration with the following Redirect URI configured ( urn:ietf:wg:oauth:2.0:oob ):

There is nothing else special about this app registration.

Now, for the code. To start with, you must install the Microsoft.Identity.Client from the Nuget package manager. I will present to you sample VB.Net code that performs authentication synchronously, then asynchronously.

Imports Microsoft.Identity.Client

Module Module1

    Private _accessToken As String = String.Empty
    Private Const client_id As String = "{client_id}" '<-- enter the client_id guid here
    Private Const tenant_id As String = "{tenant_id}" '<-- enter either your tenant id here
    Private authority As String = $"https://login.microsoftonline.com/{tenant_id}"
    Private scopes As New List(Of String)

    Sub Main()

        scopes.Add($"{client_id}/.default")

        Console.WriteLine("Starting Synchronous Sample...")

        SyncSample()

        Console.WriteLine($"{Environment.NewLine}End Synchronous Sample...{Environment.NewLine}Start Asynchronous Sample...")

        AsyncSample()

        Console.WriteLine($"{Environment.NewLine}End Asynchronous Sample.{Environment.NewLine}Press any key to close...")
        Console.ReadKey()

    End Sub

#Region "Synchronous Code"


    Private Sub SyncSample()
        If Login() Then
            Console.WriteLine(_accessToken)
        Else
            Console.WriteLine("Not Authorized")
        End If
    End Sub

    Private Function Login() As Boolean

        Dim publicClientApp As IPublicClientApplication
        publicClientApp = PublicClientApplicationBuilder.Create(client_id).WithAuthority(authority).Build()

        Dim accounts As IEnumerable(Of IAccount) = publicClientApp.GetAccountsAsync().Result()
        Dim firstAccount As IAccount = accounts.FirstOrDefault()
        Dim authResult As AuthenticationResult

        Try
            authResult = publicClientApp.AcquireTokenSilent(scopes, firstAccount).ExecuteAsync().Result()
        Catch e As MsalUiRequiredException
            Try
                authResult = publicClientApp.AcquireTokenInteractive(scopes).ExecuteAsync().Result()
            Catch ex As Exception
                'user cancelled
                Return False
            End Try
        Catch ex As Exception
            Console.WriteLine($"Auth Exception: {ex.Message}")
            Return False
        End Try

        _accessToken = authResult.AccessToken

        Return True

    End Function

#End Region

#Region "Asynchronous Code"

    Private Sub AsyncSample()

        Dim task As Task(Of Boolean) = LoginTask()

        If task.Result() Then
            Console.WriteLine(_accessToken)
        Else
            Console.WriteLine("Not Authorized")
        End If
    End Sub

    Private Async Function LoginTask() As Task(Of Boolean)
        Dim publicClientApp As IPublicClientApplication
        publicClientApp = PublicClientApplicationBuilder.Create(client_id).WithAuthority(authority).Build()

        Dim accounts As IEnumerable(Of IAccount) = Await publicClientApp.GetAccountsAsync()
        Dim firstAccount As IAccount = accounts.FirstOrDefault()
        Dim authResult As AuthenticationResult

        Dim tryInteractive As Boolean = False

        Try
            authResult = Await publicClientApp.AcquireTokenSilent(scopes, firstAccount).ExecuteAsync()
            _accessToken = authResult.AccessToken
        Catch e As MsalUiRequiredException
            tryInteractive = True
        End Try

        If tryInteractive Then
            Try
                authResult = Await publicClientApp.AcquireTokenInteractive(scopes).ExecuteAsync()
                _accessToken = authResult.AccessToken
            Catch ex As Exception
                Return False
            End Try
        End If

        Return _accessToken <> String.Empty

    End Function

#End Region

End Module

 

Although there are quite a few differences from our C# samples, we can still leverage MSAL in our VB.Net code as well with some minor changes.

UPDATE: 06/18/2020
To clear up some confusion about the redirect URI, please try selecting the default MSAL redirect from the portal that is created when you create the app registration and after selecting that by clicking the checkbox, click the save button:

Then, modify your code to add the .WithRedirectUri parameter like so:
publicClientApp = PublicClientApplicationBuilder.Create(client_id).WithAuthority(authority).WithRedirectUri(“msal28a00d08-ba05-4015-a269-9d0546e850b9://auth”).Build()

The redirect URI I had in this post, I obtained to see what the default was sent during authentication by running fiddler, in an attempt to make this more universal for everyone, which apparently isn’t the case 🙂