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…
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.
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)…
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
from the command prompt run ‘dotnet new webapi -au IndividualB2C -f net6.0 -o “c:\temp\b2cwebapi”‘ to create a web API project
In the appsettings.json file change the section named from “AzureAd” to “AzureAdB2C”
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 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();
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…
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.
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…
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…
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.
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
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
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…
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)
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…
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:
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());
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());
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:
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:
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:
Add an additional redirect URI to your App Registration
Configure an additional B2C authentication scheme in your application
Add an action to the desired controller
Implement the created action in the Layout
Before continuing, please review the following 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:
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)
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.
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.
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.
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
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
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.
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:
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.
Option 1: Clone the repository directly into Android Studio
Open Android Studio and close any open projects
Click on “Get From Version Control”
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
Extract the zip file to the directory of your choice.
Open Android Studio and close any projects that are open.
Click on “Open an Existing Project”
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
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.
To the left of the device drop-down, there is another drop-down list. Click this and select “package-inspector”
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.
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.
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: