‘Update your browser’ message when using apps that leverage ADAL/MSAL

You might see the following message…

Update your browser
Your browser is not supported or up-to-date. Try updating it, or else download and install the latest version of Microsoft Edge.
You could also try to access https://aka.ms/mysecurityinfo from another device.

As a quick solution for the user.  Have the user register for MFA ahead of time before using the app. Simply open a supported Browser like Edge or Chrome and have the user navigate to…

https://myapps.microsoft.com

Overview

You might see this when your using an App that integrate or have underlying components that depend on ADAL or MSAL. The solution will depend on the developers implementation. Entra ID Multifactor and self-service password reset registration wizard does not support older versions of Internet Explorer.

ADAL and MSAL when using embedded browser in lower version of .NET uses WinForms which is based on IE 7 components. The recommended solution would be to migrate to MSAL and use the Broker.

Developer or application vendor will need to…

First and foremost, upgrade to the latest version of MSAL.

There is no solution for ADAL

Preferably, enable Web Account Manager (WAM) support with MSAL…
https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-wam

var pca = PublicClientApplicationBuilder.Create("client_id").WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows))

If for some reason WAM can’t be used (for example on Windows Server), then you can use WebView2 (based on Edge). To do this, the application must target the framework .net6+ Windows (this is configured on the project file)…

<TargetFramework>net6.0-windows10.0.22621.0</TargetFramework>

If your unable to use WAM or can’t target .net 6+ Windows, then use the system browser…
https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/using-web-browsers#how-to-use-the-default-system-browser

var result = await pca.AcquireTokenInteractive(s_scopes).WithUseEmbeddedWebView(false)

Tutorial: How to call a protected web API with an application permission token in Azure AD B2C

Introduction

This post covers an end to end scenario where a front end console application authenticates to Azure AD B2C using client credentials OAuth2 grant flow and calls a .Net backend web API. The samples in this post are built on .Net 6 framework.

App Registrations

There are 2 App Registrations required in this tutorial: a front-end console app and a back-end web API. The samples also require either a user flow or a custom policy. I use a “sign-up or sign-in” user flow for both applications.

Back-end web API:

Follow steps 2.1 and 2.2 from this documentation to register and expose an application scope (permission) for the web API. For my web API sample, I have the following app roles defined:

Take note of the Application ID URI of the Web API in the “Expose an API” section since this is used in the console application.

Front-end console application:

Follow step 2 from this documentation to register a client application, create an application secret, and configure its API permission to add application permissions from the web API. Grant Admin Consent to those permissions.

Note: Azure AD B2C provides the application permissions in the access token’s ‘scp’ claim.

Application Code

Front-end Console application

Complete sample for the console app is here. The application uses MSAL.Net to authenticate to Azure AD B2C using client credentials grant flow and calls the web API

Below is the Program.cs file. Fill out line 6 to 11 with information from your console application registration. For the scopes in line 13, this is the web API scope in this format: “<web API Application ID URI>/.default”. The webApiUrl variable on line 14 is the web API’s protected endpoint, which can be called with a Bearer token in the Authorization header. The code has logging functionality commented out. You can enable logging to gain insight if you encounter any problems with AAD Authentication.

// See https://aka.ms/new-console-template for more information
using Microsoft.Identity.Client;
using System.Net.Http.Headers;
using File = System.IO.File;

var clientId = "<Client App ID>";
var clientSecret = "<Client App Secret>";
var tenantName = "<tenant>.onmicrosoft.com";  //for example "contosob2c.onmicrosoft.com"
var PolicySignUpSignIn = "<sign-up or sign-in userflow>"; //for example "b2c_1_susi"
var AuthorityBase = $"https://<tenant>.b2clogin.com/tfp/{tenantName}/";  // for example "https://contosob2c.b2clogin.com/tfp/contosob2c.onmicrosoft.com/"
var Authority = $"{AuthorityBase}{PolicySignUpSignIn}";

string[] scopes = { $"<web API URI>/.default" };
string webApiUrl = "https://localhost:7137/WeatherForecast";
string accessToken = "";

// for logging purpose
/*
void Log(LogLevel level, string message, bool containsPii)
{
    string logs = $"{level} {message}{Environment.NewLine}";
    File.AppendAllText(System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalLogs.txt", logs);
}
*/

IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(clientId)
               .WithClientSecret(clientSecret)
               .WithB2CAuthority(Authority)
               // for logging purpose
               // .WithLogging(Log, LogLevel.Verbose, true)
               .Build();

try
{
    var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
    accessToken = result.AccessToken;
}
catch (Exception ex)
{
    Console.WriteLine (ex.Message);
    Console.WriteLine(ex.StackTrace);

}


var httpClient = new HttpClient();
HttpResponseMessage response;
try
{
    var request = new HttpRequestMessage(HttpMethod.Get, webApiUrl);
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    response = await httpClient.SendAsync(request);
    var content = await response.Content.ReadAsStringAsync();
    Console.WriteLine("Http Response Code: " + response.StatusCode.ToString());
    Console.WriteLine("Http Respone: " + content);
}
catch (Exception ex)
{
   Console.WriteLine(ex.ToString());
}

Web API

Use the following steps to create a .Net 6 web API with Azure AD B2C authentication. Documentation for dotnet utility is here

  1. from the command prompt run ‘dotnet new webapi -au IndividualB2C -f net6.0 -o “c:\temp\b2cwebapi”‘ to create a web API project
  2. In the appsettings.json file change the section named from “AzureAd” to “AzureAdB2C”
  3. Fill out the AzureAdB2C section with information from the web API app registration. Fill out the scopes section with any permissions exposed by the web API. These are in the token’s ‘scp’ claim.

The complete web API project is here.

The project has a protected controller with the following attributes:

[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes")]
public class WeatherForecastController : ControllerBase

Here is the Program.cs file:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"));

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

How to bundle consent

You have a custom client and a custom API. There is an application registration in Azure AD for each of these apps, one for the custom client, and one for the custom API. You want your users to be able to bundle the consent for these apps.

You might see one of the following errors…

  • AADSTS70000: The request was denied because one or more scopes requested are unauthorized or expired. The user must first sign in and grant the client application access to the requested scope
  • AADSTS650052: The app needs access to a service (\”{name}\”) that your organization \”{organization}\” has not subscribed to or enabled. Contact your IT Admin to review the configuration of your service subscriptions.
    • This is the old error replaced by the new error below
  • AADSTS650052: The app is trying to access a service\”{app_id}\”(\”app_name\”) that your organization %\”{organization}\” lacks a service principal for. Contact your IT Admin to review the configuration of your service subscriptions or consent to the application in order to create the required service principal

Step 1: Configure knownClientApplications for the API app registration

First, you will need to add the custom client app ID to the custom APIs app registration knownClientApplications property…

https://learn.microsoft.com/en-us/azure/active-directory/develop/reference-app-manifest#knownclientapplications-attribute

Step 2: Configure API permissions

Second, make sure all API permissions are configured correctly on the custom client and custom API app registrations. Also make sure that all of the custom API app registration API permissions are also added to the custom client app registration.

Step 3: The sign-in request

Third, your authentication request must use the .default scope. For Microsoft accounts, the scope must be for the custom API. For example. This will also work for school and work accounts.

https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?response_type=code
&Client_id=72333f42-5078-4212-abb2-e4f9521ec76a
&redirect_uri=https://localhost
&scope=openid profile offline_access app_uri_id1/.default
&prompt=consent

However, the client will not be listed as having a permission for the API. This is ok because the client will be listed as knownClientApplications.

If you’re not concerned about supporting Microsoft Accounts, and will only be supporting work and school accounts, then use the following recommended approach. For example…

https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?response_type=code
&Client_id=72333f42-5078-4212-abb2-e4f9521ec76a
&redirect_uri=https://localhost
&scope=openid profile offline_access User.Read https://graph.microsoft.com/.default
&prompt=consent 

The Implementation

If using MSAL.Net for example…

String[] consentScope = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/.default" };
var loginResult = await clientApp.AcquireTokenInteractive(consentScope)
    .WithAccount(account)
	 .WithPrompt(Prompt.Consent)
      .ExecuteAsync();

Keep in mind that consent propagation of new servicePrincipals and permissions may take a little time. Your applications must be able to handle this.

Acquiring tokens for more than one resource

If your custom client will also acquire tokens for another resource like Microsoft Graph, you will need to build logic as attempting to acquire these tokens immediately after the consent may fail.

  • Use the default scope
  • Track the acquired scopes until the required scope returns
  • Add a delay if the result still does not have the required scope.

Note: Currently, if acquireTokenSilent fails, MSAL will force you to perform a successful Interaction before it will allow you to use AcquireTokenSilent again, even if you have a valid refresh token to use.

Here is some sample code to handle this …

        public static async Task<AuthenticationResult> GetTokenAfterConsentAsync(string[] resourceScopes)
        {
            AuthenticationResult result = null;
            int retryCount = 0;

            int index = resourceScopes[0].LastIndexOf("/");

            string resource = String.Empty;

            // Determine resource of scope
            if (index < 0)
            {
                resource = "https://graph.microsoft.com";
            }
            else
            {
                resource = resourceScopes[0].Substring(0, index);
            }

            string[] defaultScope = { $"{resource}/.default" };

            string[] acquiredScopes = { "" };
            string[] scopes = defaultScope;
            
            while (!acquiredScopes.Contains(resourceScopes[0]) && retryCount <= 15)
            {
                try
                {
                    result = await clientApp.AcquireTokenSilent(scopes, CurrentAccount).WithForceRefresh(true).ExecuteAsync();
                    acquiredScopes = result.Scopes.ToArray();
                    if (acquiredScopes.Contains(resourceScopes[0])) continue;
                }
                catch (Exception e)
                { }

                // Switch scopes to pass to MSAL on next loop. This tricks MSAL to force AcquireTokenSilent after failure. This also resolves intermittent cachine issue in ESTS
                scopes = scopes == resourceScopes ? defaultScope : resourceScopes;
                retryCount++;

                // Obvisouly something went wrong
                if(retryCount==15)
                {
                    throw new Exception();
                }

                // MSA tokens do not return scope in expected format when .default is used
                int i = 0;
                foreach(var acquiredScope in acquiredScopes)
                {
                    if(acquiredScope.IndexOf('/')==0) acquiredScopes[i].Replace("/", $"{resource}/");
                    i++;
                }

                Thread.Sleep(2000);
            }

            return result;
        }

On the custom API using the On-behalf-of flow

Similarity, the custom API when trying to acquire tokens for another resource might fail immediately after consent.

while (result == null && retryCount >= 6)
            {
                UserAssertion assertion = new UserAssertion(accessToken);
                try
                {
                    result = await apiMsalClient.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();
                    
                }
                catch { }

                retryCount++;

                if (result == null)
                {
                    Thread.Sleep(1000 * retryCount * 2);
                }
            }

If (result==null) return new HttpStatusCodeResult(HttpStatusCode.Forbidden, "Need Consent");

If this continues to fail and you run out of retries, its probably a good time to throw an error to the client and have it perform a full consent.

Example of client code assuming your API throws a 403…

HttpResponseMessage apiResult = null;
apiResult = await MockApiCall(result.AccessToken);

if(apiResult.StatusCode==HttpStatusCode.Forbidden)
{
  var authResult = await clientApp.AcquireTokenInteractive(apiDefaultScope)
    .WithAccount(account)
    .WithPrompt(Prompt.Consent)
    .ExecuteAsync();
  CurrentAccount = authResult.Account;

  // Retry API call
  apiResult = await MockApiCall(result.AccessToken); 
}          

Recommendations and expected behavior

Building an app for handling bundled consent is not as straight forward. Preferably you have a separate process you can walk your users through to perform this bundled consent, provision your app and API within their tenant or on their Microsoft Account and only get the consent experience once. (Separate from actually signing into the app.) If you don’t have this process and trying to build it into your app and your sign in experience, it gets messy and your users will have multiple consent prompts. I would recommend that you build a experience within your app that warns users they may get prompted to consent (multiple times).

For Microsoft Accounts, I would expect minimum of two consent prompts. One for the application, and one for the API.

For work and school accounts, I would expect only one consent prompt. Azure AD handles bundled consent much better than Microsoft Accounts.

Here is a end to end example sample of code. This has a pretty good user experience considering trying to support all account types and only prompting consent if required. Its not perfect as perfect is virtually non-existent.

string[] msGraphScopes = { "User.Read", "Mail.Send", "Calendar.Read" }
String[] apiScopes = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/access_as_user" };
String[] msGraphDefaultScope = { "https://graph.microsoft.com/.default" };
String[] apiDefaultScope = { "api://ae5a0bbe-d6b3-4a20-867b-c8d9fd442160/.default" };

var accounts = await clientApp.GetAccountsAsync();
IAccount account = accounts.FirstOrDefault();

AuthenticationResult msGraphTokenResult = null;
AuthenticationResult apiTokenResult = null;

try
{
	msGraphTokenResult = await clientApp.AcquireTokenSilent(msGraphScopes, account).ExecuteAsync();
	apiTokenResult = await clientApp.AcquireTokenSilent(apiScopes, account).ExecuteAsync();
}
catch (Exception e1)
{
	
	string catch1Message = e1.Message;
	string catch2Message = String.Empty;

	try
	{
        // First possible consent experience
		var result = await clientApp.AcquireTokenInteractive(apiScopes)
		  .WithExtraScopesToConsent(msGraphScopes)
		  .WithAccount(account)
		  .ExecuteAsync();
		CurrentAccount = result.Account;
		msGraphTokenResult = await clientApp.AcquireTokenSilent(msGraphScopes, CurrentAccount).ExecuteAsync();
		apiTokenResult = await clientApp.AcquireTokenSilent(apiScopes, CurrentAccount).ExecuteAsync();
	}
	catch(Exception e2)
	{
		catch2Message = e2.Message;
	};

	if(catch1Message.Contains("AADSTS650052") || catch2Message.Contains("AADSTS650052") || catch1Message.Contains("AADSTS70000") || catch2Message.Contains("AADSTS70000"))
	{
        // Second possible consent experience
		var result = await clientApp.AcquireTokenInteractive(apiDefaultScope)
			.WithAccount(account)
			.WithPrompt(Prompt.Consent)
			.ExecuteAsync();
		CurrentAccount = result.Account;
		msGraphTokenResult = await GetTokenAfterConsentAsync(msGraphScopes);
		apiTokenResult = await GetTokenAfterConsentAsync(apiScopes);
	}
}

// Call API

apiResult = await MockApiCall(apiTokenResult.AccessToken);
var contentMessage = await apiResult.Content.ReadAsStringAsync();

if(apiResult.StatusCode==HttpStatusCode.Forbidden)
{
	var result = await clientApp.AcquireTokenInteractive(apiDefaultScope)
		.WithAccount(account)
		.WithPrompt(Prompt.Consent)
		.ExecuteAsync();
	CurrentAccount = result.Account;

	// Retry API call
	apiResult = await MockApiCall(result.AccessToken);
}

If anyone has better ideas or solutions, please comment on this post.

Using MSAL for Python to perform interactive sign in from a local script

This blog shows how to use MSAL for Python to perform an interactive sign in to Azure AD from running a local python script. The sample also demonstrates how to enable MSAL logging along with how to capture Python SSL web traffic using Fiddler Classic

App Registration:

You will need to have an Azure AD App Registration with “http://localhost” reply URL configured in the ‘Mobile and desktop applications’ platform

The Code:

If you don’t have msal and requests module already installed you may need to run both “pip install msal” and “pip install requests” commands to get these installed first.

Logging:

logging is enabled with the following code to set logging level to DEBUG

logging.basicConfig(level=logging.DEBUG)  # Enable DEBUG log for entire script
logging.getLogger("msal").setLevel(logging.DEBUG)  # Optionally disable MSAL DEBUG logs

Fiddler HTTPS capture

Capturing HTTPS traffic from Python application using Fiddler can be very challenging. The application uses MSAL library to authenticate with Azure AD and then uses the requests module to send request to MS Graph endpoint. The following code is used to help with Fiddler capture (assuming Fiddler is listening on port 8888)

app = msal.PublicClientApplication(client_id = appId, authority = "https://login.microsoftonline.com/"+ tenantId,verify = False)
graph_response = requests.get(  # Use token to call downstream service
        graphurl,
        headers={'Authorization': 'Bearer ' + result['access_token']},
        proxies={"http": "http://127.0.0.1:8888","https":"http://127.0.0.1:8888"},verify=False)

Below is the complete python script you can run locally on the machine. Make sure to provide your tenant ID and Application ID

import sys
import json, logging, msal, requests

# comment out the following 2 lines if you dont't want to enable MSAL logging
logging.basicConfig(level=logging.DEBUG)  # Enable DEBUG log for entire script
logging.getLogger("msal").setLevel(logging.DEBUG)  # Optionally disable MSAL DEBUG logs

tenantId = "<Tenant ID>"
appId = "<Application ID>"
scope = ["User.Read"]
graphurl = "https://graph.microsoft.com/v1.0/me"

app = msal.PublicClientApplication(client_id = appId, authority = "https://login.microsoftonline.com/"+ tenantId)

# use the following instead for Fiddler capture
# the verify = False flag is to tell the program to ignore SSL certificate verification.  We will need this since we are using Fiddler SSL certificate for HTTPS capture
# app = msal.PublicClientApplication(client_id = appId, authority = "https://login.microsoftonline.com/"+ tenantId,verify = False)

result = None

# accounts = app.get_accounts(username = "bhadmin@bachoang99live.onmicrosoft.com")
accounts = app.get_accounts()
if accounts:
    logging.info("Account(s) exists in cache, probably with token too. Let's try.")
    print("Account(s) already signed in:")
    for a in accounts:
        print(a["username"])
    chosen = accounts[0]  # Assuming the end user chose this one to proceed
    print("Proceed with account: %s" % chosen["username"])
    # Now let's try to find a token in cache for this account
    result = app.acquire_token_silent(scope, account=chosen)

if not result:
    logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
    print("A local browser window will be open for you to sign in. CTRL+C to cancel.")
    result = app.acquire_token_interactive(  # Only works if your app is registered with redirect_uri as http://localhost
        scopes = scope,
        #parent_window_handle=...,  # If broker is enabled, you will be guided to provide a window handle
        # login_hint=config.get("username"),  # Optional.
            # If you know the username ahead of time, this parameter can pre-fill
            # the username (or email address) field of the sign-in page for the user,
            # Often, apps use this parameter during reauthentication,
            # after already extracting the username from an earlier sign-in
            # by using the preferred_username claim from returned id_token_claims.

        # prompt=msal.Prompt.SELECT_ACCOUNT,  # Or simply "select_account". Optional. It forces to show account selector page
        #prompt=msal.Prompt.CREATE,  # Or simply "create". Optional. It brings user to a self-service sign-up flow.
            # Prerequisite: https://docs.microsoft.com/en-us/azure/active-directory/external-identities/self-service-sign-up-user-flow
        )

if "access_token" in result:
    # Calling graph using the access token

    # use the following code if you want to capture SSL traffic in Fiddler
    # graph_response = requests.get(  # Use token to call downstream service
    #    graphurl,
    #    headers={'Authorization': 'Bearer ' + result['access_token']},
    #    proxies={"http": # "http://127.0.0.1:8888","https":"http://127.0.0.1:8888"},verify=False)

    graph_response = requests.get(  # Use token to call downstream service
        graphurl,
        headers={'Authorization': 'Bearer ' + result['access_token']})
    print("Graph API call result: %s ..." % graph_response.text[:100])
else:
    print(result.get("error"))
    print(result.get("error_description"))
    print(result.get("correlation_id"))  # You may need this when reporting a bug 

References:

msal.application.ClientApplication class | Microsoft Learn

microsoft-authentication-library-for-python/interactive_sample.py at dev · AzureAD/microsoft-authentication-library-for-python · GitHub

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 resolve “No account or login hint was passed to the AcquireTokenSilent” with a Web App and no persistent token cache

You have implemented Microsoft Authentication Library or Microsoft Identity Web and now you are seeing the following error message:

No account or login hint was passed to the AcquireTokenSilent

The root cause is because the Token Cache is empty when you are trying to acquire a token silently when account was attempted to be pulled from MSAL.

So on Web Applications like Asp.Net or Asp.Net Core, this is generally when the user is still passing a authentication cookie to the web app but the web app has restarted or their is high memory usage where the memory gets cleared out therefore the MSAL token cache is empty. Also keep in mind, MSAL does have a default cache size where MSAL will start clearing old entries from its cache.

There are a few solutions to this. We would recommend implementing persistent token cache…

Either implement a persistent cache using something like SQL server or file based storage…
https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens#:~:text=MSAL%20caches%20a%20token%20after%20it%27s%20been%20acquired.,the%20session%20cookie%20that%27s%20in%20the%20browser%2C%20however.

However in this article we will focus on how to reject the authentication cookie.

Asp.Net

You can implement a Cookie Authentication event to check if the currently signed in user is in the MSAL token cache and if they are not, reject the cookie and force them to sign in again.

The following implementation does depend you have the following helpers:

https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect/blob/master/WebApp/Utils/MsalAppBuilder.cs

https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect/blob/master/WebApp/Utils/AuthenticationConfig.cs

And the Microsoft.Identity.Web.TokenCache nuget package installed.

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    Provider = new CookieAuthenticationProvider()
    {
        OnValidateIdentity = async context =>
        {
            // To Resolve: No account or login hint was passed to the AcquireTokenSilent call.
            IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();

            var signedInUserIdentity = new ClaimsPrincipal(context.Identity);
            if (await clientApp.GetAccountAsync(signedInUserIdentity.GetAccountId()) == null)
            {
                context.RejectIdentity();
            }
        }
    }
});

Asp.Net Core

First, create the custom Cookie Authentication event with the logic to check if the user can acquire a token successfully…

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using System.Threading.Tasks;
using System.Security.Claims;

namespace SampleApp.Services
{
    internal class RejectSessionCookieWhenAccountNotInCacheEvents : CookieAuthenticationEvents
    {
       
     
        public async override Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {

            // Get the registered service that implements MSAL assuming you have one
            var msalInstance = context.HttpContext.RequestServices.GetRequiredService<IMsalService>();

            // Get the MSAL IConfidentialClientApplication object
            IConfidentialClientApplication msalClient = msalInstance.GetClient();

            var accounts = await msalClient.GetAccountsAsync();

            // Get the signed in user account from MSAL if there is one
            var account = await msalClient.GetAccountAsync(accounts.FirstOrDefault());

            // If no account then reject the authentication cookie
            if (account == null)
            {
                context.RejectPrincipal();
            }

            await base.OnValidatePrincipal(context);
        }
    }
}

Finally, register your Custom Cookie Authentication event…

// Register the Custom Cookie Authentication event
Services.Configure<CookieAuthenticationOptions>(cookieScheme, options=>options.Events=new RejectSessionCookieWhenAccountNotInCacheEvents());

Microsoft Identity Web


A good solution is already implemented in Microsoft Identity Web and you simply need to implement the guidance here…
https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access

If the above article does not work, you can manually clear the authentication cookie.

First, create the custom Cookie Authentication event with the logic to check if the user can acquire a token successfully…

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace aad_ms_id_web_mvc_app.Services
{
    internal class RejectSessionCookieWhenAccountNotInCacheEvents : CookieAuthenticationEvents
    {
        public async override Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {
            try
            {
                var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
                string token = await tokenAcquisition.GetAccessTokenForUserAsync(
                    scopes: new[] { "profile" },
                    user: context.Principal);
            }
            catch (MicrosoftIdentityWebChallengeUserException ex)
               when (AccountDoesNotExistInTokenCache(ex))
            {
                context.RejectPrincipal();
            }
        }

        /// <summary>
        /// Is the exception thrown because there is no account in the token cache?
        /// </summary>
        /// <param name="ex">Exception thrown by <see cref="ITokenAcquisition"/>.GetTokenForXX methods.</param>
        /// <returns>A boolean telling if the exception was about not having an account in the cache</returns>
        private static bool AccountDoesNotExistInTokenCache(MicrosoftIdentityWebChallengeUserException ex)
        {
            return ex.InnerException is MsalUiRequiredException
                                      && (ex.InnerException as MsalUiRequiredException).ErrorCode == "user_null";
        }
    }
}

Finally, register the Custom Cookie Authentication event…

// Add Microsoft Identity Web
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                           .AddMicrosoftGraph(Configuration.GetSection("GraphBeta"))
                           .AddInMemoryTokenCaches();

// Register the Custom Cookie Authentication event
Services.Configure<CookieAuthenticationOptions>(cookieScheme, options=>options.Events=new RejectSessionCookieWhenAccountNotInCacheEvents());

How to Resolve IDX10501 Errors in a B2C Microsoft.Identity.Web Application

Consider the situation where you are developing an ASP.NET Core application that needs to support Azure B2C. Following the official Microsoft Document, you implement the Microsoft.Identity.Web library and three built-in User Flows. All is working well, but you also need to implement a Custom Policy. You find the following documentation which shows how to make a request with the B2C policy specified:

https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application-options#pass-the-azure-ad-b2c-policy-id

You try to implement this guidance. Everything compiles ok, but when you test the app, you encounter an IDX10501 error after the custom policy redirects back to the application. I might look like this:

IDX10501: Signature validation failed. Unable to match key: kid: '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. Number of keys in TokenValidationParameters: '0'. Number of keys in Configuration: '1'. Exceptions caught: '[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.

This article is precisely about how to deal with this situation.

The Reason For The Error:

First, let’s understand why this error is being thrown when the custom policy redirects back to your app. In ASP.NET Core, whenever a user is authenticated and authorized, and there is a redirect back to the Web App containing an ID Token, the ASP.NET Core middleware will try to validate this ID Token to make sure that the redirect is genuine. In order to validate the ID Token, the Middleware needs the public key of the signing certificate which was used to sign the ID Token. The Middleware gets this public key by querying AAD B2C. Specifically, there is a “metadata” endpoint in AAD B2C used by the Middleware which provides authentication information including any public keys for signing certificates.

You may remember when creating your custom policy that you needed to create or upload a signing certificate. This signing certificate is different from that used for built-in user flows in AAD B2C. This means that the public keys accessible from the metadata endpoint for your AAD B2C will not contain the public key for your custom policy. The custom policy actually has it’s own metadata endpoint.

The endpoint which the Middleware uses is configured by Microsoft.Identity.Web and set at application startup. Since the metadata URL is already set, invoking a custom policy during runtime will result in a scenario where the Middleware is looking at the wrong metadata URL while validating the returning token.

The Solution:

In short, we need to configure the correct metadata endpoint for our additional Custom Policy.

We do this by creating a second authentication scheme to handle the custom policy. With this additional authentication scheme, we can set the correct metadata endpoint at startup. Below is a brief overview of the steps involved in actually carrying this out:

  1. Add an additional redirect URI to your App Registration
  2. Configure an additional B2C authentication scheme in your application
  3. Add an action to the desired controller
  4. Implement the created action in the Layout

Before continuing, please review the following prerequisites.

Note: an example application that demonstrates the results of the following steps can be found at the following link
https://github.com/mbukovich/ExtraB2CPolicyMVC

Prerequisites:

Add an Additional Redirect URI to your App Registration

Using the same App Registration referred to in the Prerequisites, we will need to add another Redirect URI for the custom policy. The reason we cannot use the existing redirect URI for this situation is that doing so will confuse the Web App. We will be setting up two different authentication schemes, but when the B2C policy redirects back to the Web App, the Middleware will not know which authentication scheme to use. Thus, we need a separate redirect URI to clearly distinguish redirects from the existing and new authentication schemes.

Go to the Azure Portal at portal.azure.com, and navigate to your app registration. Once there, click on the “Authentication” blade on the left side of the screen. Under the existing redirect URI, add a new one. Copy and past the existing URI into the new one, and add a ‘-‘ to the existing URL and then whatever string you would like to define the callback endpoint for your application. In my example, my new redirect URI is “https://localhost:44321/signin-oidc-editemail”. Don’t forget to click “Save”!

Important Note:

You will need a separate redirect URI for each Authentication Scheme. This means if you are adding 2 Custom Policies (or more), then you will need to add 2 (or more) Authentication Schemes (one for each Policy), which means 2 (or more) redirect URIs.

Configure an Additional B2C Authentication Scheme

This process hinges around adding an action to your controller that will issue a challenge to the user. However, before we create this action, we need to properly configure the app with an additional authentication scheme. This means configuring the appsettings.json file as well as the Startup.cs file.

appsettings.json

Add the following JSON to your appsettings.json file:

"<name-of-your-configuration>": {
    "Instance": "https://<B2C-tenant-name>.b2clogin.com",
    "ClientId": "<client-id-of-your-app-registration>",
    "CallbackPath": "/<endpoint-of-your-new-redirect-uri>",
    "SignedOutCallbackPath": "/signout/<built-in-sign-in-sign-up-policy>",
    "Domain": "<B2C-tenant-name>.onmicrosoft.com",
    "SignUpSignInPolicyId": "<built-in-sign-in-sign-up-policy>"
},

This is how the appsettings.json file looks in my example application:

{
  "AzureADB2C": {
    "Instance": "https://markstestorganization1.b2clogin.com",
    "ClientId": "09717d12-ca7f-4388-8393-dafe42c0c3a5",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath": "/signout/B2C_1_signupsignin1",
    "Domain": "markstestorganization1.onmicrosoft.com",
    "SignUpSignInPolicyId": "B2C_1_signupsignin1",
    "ResetPasswordPolicyId": "B2C_1_PasswordReset1",
    "EditProfilePolicyId": "B2C_1_editProfileTest1"
  },
  "AzureADB2CEditEmail": {
    "Instance": "https://markstestorganization1.b2clogin.com",
    "ClientId": "09717d12-ca7f-4388-8393-dafe42c0c3a5",
    "CallbackPath": "/signin-oidc-editemail",
    "SignedOutCallbackPath": "/signout/B2C_1_signupsignin1",
    "Domain": "markstestorganization1.onmicrosoft.com",
    "SignUpSignInPolicyId": "B2C_1_signupsignin1"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Important Notes:

  • The name of the second B2C configuration is arbitrary; you can name it whatever you want. We will using this extra configuration for a single custom policy, and if you want to add more custom policies, you will need to add an additional B2C configuration in the AppSettings.json file. For this reason, I recommend giving the JSON object a name associated with the Custom Policy.
  • CallbackPath takes the String from the end of your Redirect URI from the previous step. Since my redirect URI is “https://localhost:44321/signin-oidc-editemail”, then my CallbackPath is “/signin-oidc-editemail”.
  • You need to include your standard built-in sign-up-sign-in user flow in the Authentication Scheme in case the user tries to use your Custom Policy without being signed-in.

Startup.cs

Now we need to configure an additional authentication scheme in the startup.cs file. Within the “ConfigureServices” function, add the following code (added to line 52 in my example app):

// Create another authentication scheme to handle extra custom policy
services.AddAuthentication()
       .AddMicrosoftIdentityWebApp(Configuration.GetSection("<name-of-json-configuration>"), "<Arbitrary-name-for-Auth-Scheme>", "<Arbitrary-name-of-Cookie-Scheme>");

services.Configure<OpenIdConnectOptions>("<Arbitrary-name-for-Auth-Scheme>", options =>
    {
        options.MetadataAddress = "<Metadata-Address-for-Custom-Policy>";
    });

You will need to choose an arbitrary name for your authentication scheme, and an arbitrary name for the associated cookie scheme. Microsoft.Identity.Web will create the new authentication scheme and cookie scheme with the names you specified. You will also need to replace the text, “<name-of-json-configuration>,” with the name of the json configuration from the previous step. In my case, this would be “AzureADB2CEditEmail.”

It is also very important to get the meta data address. This will be used by the middleware to get the information necessary to validate ID Tokens returned by the Custom Policy. The meta data address can be found in the Azure B2C Portal. On the left side of the screen, under “Policies,” click on “Identity Experience Framework” as indicated in the image below.

You will see your custom policies listed at the bottom of the page; click on the custom policy you are using for this guide. In my case it is “B2C_1A_DEMO_CHANGESIGNINNAME.”

In the dialog that opened up on the right side of the screen, the meta data address is the URL listed under “OpenId Connect Discovery Endpoint.” Copy this URL and paste it in for the value of the MetadataAddress variable.

Add Action to the Controller

Now we add an action to the desired controller to challenge the user with the Custom B2C Policy. In my example, I add the action to my Home Controller for the sake of simplicity. Add the following code to your controller and adjust the values and action name to meet your scenario (this code snippet can be found on line 40 of the “HomeController.cs” file in the “Controllers” folder in my example project):

[Authorize]
public IActionResult EditEmail()
{
    var redirectUrl = Url.Content("~/");
    var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
    properties.Items["policy"] = "B2C_1A_DEMO_CHANGESIGNINNAME";
    return Challenge(properties, "B2CEditEmail");
}

Important Notes:

  • Please rename the function to something that describes the action in your scenario.
  • Change “B2C_1A_DEMO_CHANGESIGNINNAME” to the name of your custom policy.
  • Change “B2CEditEmail” to whatever you named your custom policy authentication scheme. This is how the middleware will know which scheme and meta data address to use.

Implement the Action in the Layout

Now we need to implement the action in the layout so that the user can actually invoke the custom policy. In my example, I added a button alongside existing B2C buttons based on the tutorial mentioned in the prerequisites section; however, you can handle this in whatever way suits your layout and business requirements best.

If you want to follow the same idea that I used, then you can use the following snippet as an example of another list item added to the partial layout created in Step 4 of the tutorial linked in the 5th Prerequisite for this guide. (link: https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application?tabs=visual-studio#step-4-add-the-ui-elements)

<li class="navbar-btn">
    <form method="get" asp-area="" asp-controller="Home" asp-action="EditEmail">
        <button type="submit" class="btn btn-primary" style="margin-right:5px">Edit Email</button>
    </form>
</li>

I added the snippet above to line 13 of the “_LayoutPartial.cshtml” file in the “Views/Shared” folder of my example project. Notice that the “asp-controller” property is set to “Home” in order to reference the Home Controller, and the “asp-action” property is set to “EditEmail” to reference the action created in the Home Controller.

If you have an existing app that doesn’t utilize the partial layout, and you just want a quick link to test the custom policy, you can use the following tag which creates a basic link. Replace the indicated values being sure to reference the correct controller if you didn’t add your action to the Home Controller:

<a asp-area="" asp-controller="Home" asp-action="replace-with-your-controller-action">Replace with text that describes the action</a>

Congratulations! Now you can run the Web App and test the solution.

How to Solve Failed Authentication After Publishing App to Google Play Store

Issue Description

You successfully implemented Azure AD Authentication in your Android app with the Microsoft Authentication Library. The application built and executed perfectly and passed all QA testing with flying colors. And then you published the application on Google Play. And authentication doesn’t work after installing the app.

If you exposed authentication error messages to the user, or had them sent to your team, then you might see an error like this:

“The redirect URI in the configuration file doesn’t match with the one generated with the package name and signature hash. Please verify the uri in the config file and your app registration in Azure portal.”

Another potential behavior that might indicate this problem is this: During development and QA testing, you successfully set up your app to use a supported broker to handle authentication and SSO. However, after deployment through Google Play and installation, the app no longer uses the broker for authentication.

The Cause

When an Android application is built for installation on a device, it is built as an “apk” compressed package and then signed by a certificate. This certificate signing ensures that the person who built the application was the one who owns the private signing key. This prevents potential impersonation attempts where a hacker might modify the application in a harmful way since the hacker will not be able to sign their version of the application with the original private signing key.

In the past, Android developers owned and maintained their own private signing keys. However, now, Google Play Services generate and maintain their own private signing key for an Android Developer. This is actually a good thing, since the key will be securely stored by Google. The developer still maintains an upload key so that Google Play Services can verify the authenticity of an uploaded app bundle, but the actual signing is done by the Google-owned signing certificate when a user installs the app on their device.

How is this relevant? The Microsoft Authentication Library (MSAL) for Android Native and Microsoft Supported Authentication Brokers use the public signature hash of an installed application to identify it during interaction through the Android Operating system necessary during authentication. The public signature hash of an application installed by Google Play will be different from the same application installed before publishing to Google Play. Because of this, Msal will be configured to use the incorrect signature hash.

How To Fix It

Generally, there are three major steps in solving this issue.

  • Find out the new Signature Hash resulting from Google Play Installation
  • Add a new Redirect URI to the App Registration in the Azure Portal with the new signature hash
  • Adapt the Msal Configuration within the application code to use the new Redirect URI and Signature Hash.

Each of these three steps are covered in more detail below:

Step 1:

Find the New Signature Hash

There are two ways to get the new Signature Hash: Use Msal’s “Package Inspector” tool and to get the Signature Hash from the Google Play Console.

For details on how to install and use the Msal Package Inspector, see my article here:
https://blogs.aaddevsup.xyz/2022/03/package-inspector-for-msal-android-native-guide/

Google Play Console

To get the Signature Hash from the Google Play Console, first go to the Console and log in with your Google Developer account.

Once you are in the Google Play Console, select the app you are working on, then look to the left side of the screen. Under the “Release” category, expand “Setup” and click on “App Integrity”.

Now click on the “App signing” tab and you will see the “fingerprint” of the app signing key in three different variations. Copy the one indicated in the screenshot below:

PowerShell Script to Encode the Signature Hash

Copy the “SHA-1” fingerprint and paste it into the following PowerShell script as the value of the $Thumbprint variable. Run the script to obtain the base64 encoded fingerprint that Msal needs.

$Thumbprint = "paste your fingerprint here"
$Thumbprint = $Thumbprint.Replace(":", "")

$Bytes = [byte[]]::new($Thumbprint.Length / 2)

For($i=0; $i -lt $Thumbprint.Length; $i+=2){
    $Bytes[$i/2] = [convert]::ToByte($Thumbprint.Substring($i, 2), 16)
}

$hashedString =[Convert]::ToBase64String($Bytes)

Write-Host $hashedString

Step 2:

Add a new Redirect URI to the App Registration in the Azure Portal with the new signature hash

If you have come this far in developing an Android App using Msal, you likely already understand how to complete this step; however, I will provide basic guidance here.

Note: I highly recommend adding a new redirect URI rather than modifying the existing redirect URI. Your app registration can contain many redirect URIs. Additionally, modifying the existing redirect URI might result in problems with the development version of your app. This could create headaches while troubleshooting, developing updates, etc.

Log in to your Azure Portal at portal.azure.com and navigate to the App registrations portal. This can be done quickly and easily by searching for “App registrations” at the top of the portal screen as indicated in the following screenshot:

Select the app registration for your Android app, click on “Authentication” on the left side of the screen, and then click to “Add a platform”. Select “Android” as indicated in the following screenshot.

Enter the package name of your Android app and the new signature hash in the indicated fields and then click “Configure”. Note that it is fine to have the same package name in multiple Android Redirect URIs as long as the signature hash is different.

Step 3:

Adapt the Msal Configuration within the application code to use the new Redirect URI and Signature hash

As previously mentioned, at this point in the development process, you are likely already familiar with this process. I will cover the basics here.

There are two things you need to change in the application code: The Msal configuration file and the Android Manifest file.

Msal Configuration File:

The only thing to change is the Redirect URI. Copy and paste this directly from the Azure Portal. In the Azure Portal, you will notice that the signature hash portion of the redirect URI is http encoded. It should remain http encoded.

Android Manifest File:

The only thing to change in the Android Manifest File is the android:path property within the tag for the “com.microsoft.identity.client.BrowserTabActivity” activity indicated in the following screenshot. Paste the signature hash as the value for this property.

Important Notes:
-make sure to include the forward slash at the front of the signature hash as seen in the above screenshot
-In contrast to the Redirect URI, the signature hash here is not http encoded

How to enable MSAL for Java (MSAL4J) logging in a Spring Boot application

In this blog, I’ll show how to enable MSAL4J logging using the logback framework in a spring boot web application. I’ll use our Azure AD B2C web sample here. The complete code for this blog is on github. Refer to the MSAL for Java logging documentation for more info. There are 3 main things you need to do for logging to work

1) Include the logback package in the pom.xml file

		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.2.3</version>
		</dependency>

2) Add a file called ‘logback.xml’ with the following content to the resources folder.

This configuration (aka appender) logs to the console. There are other example appenders here you can use as well. You can change the logging level to a different level (error, warning, info, verbose) in your application since debug level is quite chatty.

<?xml version = "1.0" encoding = "UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

3) Set the logging.config property to the location of the logback.xml file before the main method:

@SpringBootApplication
public class MsalB2CWebSampleApplication {

	static { System.setProperty("logging.config", "C:\\Users\\<your path>\\src\\main\\resources\\logback.xml");}
	public static void main(String[] args) {
		// Console.log("main");
		// System.console().printf("hello");
		// System.out.printf("Hello %s!%n", "World");
		System.out.printf("%s%n", "Hello World");
		SpringApplication.run(MsalB2CWebSampleApplication.class, args);
	}
}

HTTPS support

The sample uses https. I follow step 2 from ‘Configure the Web App‘ section to generate a self-signed certificate and place the keystore.p12 file in the resources folder.

App Registration

Make sure you have 2 different app registrations in your Azure AD B2C tenant. One for the web app and one for the web API. Expose the scope in the web API (refer to this documentation if you are not familiar with how to expose web API scope) and configure the web API scope in the ‘API Permission’ blade for the web app. You should also grant admin consent to all the configured permission in the web app. You can also follow this tutorial for app registrations covered in this blog. Below is my set up:

Below is how logging should look like when done correctly:

Package Inspector for MSAL Android Native Guide

Package Inspector for MSAL Android Native Guide

The Microsoft Authentication Library (MSAL) for Android Native contains a tool called Package Inspector. This tool presents a list of packages installed on an Android device and allows the user to view, copy, and paste the signature hash used to sign the application’s package. It can be very useful in troubleshooting and verifying the signature hash for applications installed on an Android device. Below is a guide on the installation, use, and common issues of the Package Inspector.

Some scenarios where the Package Inspector will be useful are:

  • You have successfully developed your application to use MSAL, but after deploying the app to the Google Play Store, the app fails to perform authentication. In this case, the Package Inspector will be useful to discover the new signature hash used by Google to sign the app package.
  • You are implementing MSAL in your Android application, but encounter the following error:
    “The redirect URI in the configuration file doesn’t match with the one generated with the package name and signature hash. Please verify the uri in the config file and your app registration in Azure portal.”
    In this case, you can use the Package Inspector to verify what the package signature hash is and correctly configure both the Azure Portal and application to use the correct signature hash.
  • You are implementing MSAL in your Android application, but encounter the following error:
    “Intent filter for: BrowserTabActivity is missing”
    This error can occur because the signature hash specified in the AndroidManifest.xml file does not match the signature hash actually used to sign the apk file. In this case, the Package Inspector will be useful to verify what the signature hash actually is.

Note: The best place to learn about MSAL for Android Native is the github page at the following link. The readme is a very good start.
https://github.com/AzureAD/microsoft-authentication-library-for-android

Prerequisites

You should have:

Installation

Option 1: Clone the repository directly into Android Studio

  1. Open Android Studio and close any open projects
  2. Click on “Get From Version Control”


  3. Make sure that “Git” is selected at the top of the window, paste in the repository url:
    https://github.com/AzureAD/microsoft-authentication-library-for-android.git
    and click “Clone



Option 2: Download as a zip file and open in Android Studio

  1. Download the repository at:
    https://github.com/AzureAD/microsoft-authentication-library-for-android/archive/refs/heads/dev.zip
  2. Extract the zip file to the directory of your choice.
  3. Open Android Studio and close any projects that are open.
  4. Click on “Open an Existing Project”


  5. Find and select the Root Package for the entire Android MSAL Repository; do not choose the ‘package-inspector’ directory (This small detail is important) and click “OK”. (I renamed the directory to ‘msal-android’, but it will be “microsoft-authentication-library-for-android-dev” by default)


Using the Package Inspector

  1. With the Android MSAL project open in Android Studio, connect the desired Android device. This can be a physical device connected to the computer’s USB port, or an emulator booted from Android Studio’s AVD manager. Make sure that your device appears in the drop-down list at the top of Android Studio and select it.
  2. To the left of the device drop-down, there is another drop-down list. Click this and select “package-inspector”


  3. Click the green “play” button (indicated in the screenshot above with a green circle all the way to the right) to build, install, and run the package inspector on the desired device.
  4. Look through the list of packages in the package-inspector app and click on a package for which you want to see the signature hash. All packages on the device that the package-inspector has permission to access will appear in this list.

Common Issues

Problems loading the tool into Android Studio:
It is very important to make sure that you are loading the root package from the MSAL repository and not individually the package inspector.

Make sure that the project you are loading into Android Studio is not package-inspector, but should be named this:
“microsoft-authentication-library-for-android-dev” or whatever you may have renamed the root repository on your system. See step 4 under Option 2 of the installation section of this blog post.

Not all packages appear in the Package Inspector:
This is a very likely issue to run into. You are able to install and open Package Inspector fine, and a list of packages appear in the app; however, you do not see packages for any of the apps you have installed on the device.

An explanation for the reason behind this can be found in Google’s documentation on a change that was introduced in Android 11 (API 30):
https://developer.android.com/training/package-visibility

How to fix it –
The way to fix the issue is to first open the AndroidManifest.xml file within the ‘package-inspector’ directory found on the left side of Android Studio.


And then add the following permission and query between the <manifest></manifest> tags:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.microsoft.inspector">

    ...

    <permission android:name="android.permission.QUERY_ALL_PACKAGES" />

    <queries>
        <intent>
            <action android:name="android.intent.action.MAIN" />
        </intent>
    </queries>

</manifest>

Rerun the application from Android Studio, and the changes will be installed. Now you should be able to see the packages you have installed.