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 are developing an app and see one of the following CORS related errors in the console logs…
Access to XMLHttpRequest at ‘https://login.microsoftonline.com/tenant_id/oauth2/v2.0/authorize?client_id=&redirect_uri=signin-oidc&response_type=id_token&scope=openid%20profile&response_mode=form_post&nonce=6370sdfj&state=sdfsdfds-sdfsdfsdf-sd-sdfsdf-T3qwNWW2jRHM&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0‘ (redirected from ‘xxx’) from origin ‘yyyy’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Access to fetch at ‘https://contosob2c.b2clogin.com/tfp/tenant_id/b2c_1_v2_susi_defaultpage/v2.0/.well-known/openid-configuration‘ from origin ‘http://localhost:4200‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’
Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a “preflight” request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.
Browsers will not allow Cross Origin requests when the resource does not have the supporting headers. Cross Origin requests originate from an JavaScript XMLHttpRequest which is kind of like a direct HTTP call with no user interaction or window. Azure AD/Entra ID does not have CORS enabled while performing an interactive sign-in which means you cannot send a CORS request to Azure AD/Entra ID.
Depending on the root cause, there may be different solutions. It would be best to collect a Fiddler capture to try to determine your scenario and the cause.
Within the Fiddler capture, look for your XMLHttpRequest request (This is usually or AJAX or JQuery call) and notice it’s a 302 redirect to “https://login.microsoftonline.com.com/…“
The HTTP Request and response might look something like this…
REQUEST
GET https://login.microsoftonline.com.com/domain.onmicrosoft.com/oauth2/v2.0/authorize?... HTTP/1.1
Host: login.microsoftonline.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Origin: https://app.domain.com
RESPONSE
HTTP/1.1 200 OK
Cache-Control: no-store, no-cache
Pragma: no-cache
Content-Type: text/html; charset=utf-8
Expires: -1
Vary: Accept-Encoding
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-DNS-Prefetch-Control: on
P3P: CP="DSP CUR OTPi IND OTRi ONL FIN"
Set-Cookie: ...
Referrer-Policy: strict-origin-when-cross-origin
Date: Tue, 24 Nov 2020 19:08:05 GMT
Content-Length: 194559
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html dir="ltr" class="" lang="en">
<head>
Notice there is a Origin header in the request and no ‘Access-Control-Allow-Origin’ header in the response.
We often see this when you try to pass a access token (or Authentication cookie) to the XMLHttpRequest endpoint, the security token whether it’s an access token or authentication cookie gets invalidated and the API redirects to the Azure AD/Entra ID sign-in page instead of throwing a proper 401 HTTP status code. Because of this redirect, and we do not support CORS, browser will throw a CORS error.
So, how do you resolve or workaround this?
Preferably, the solution would be for you to implement your application architecture to follow the OAuth2 and OIDC model. That is, the front end app acquires a access token and passes it in a authorization header to the API (this is what your making your XMLHttpRequest to). Please follow our samples…
Not every scenario will be listed here as every environment and app architecture is different, however, these are the most common.
Scenario: Web App + Web API using authentication cookie
Otherwise meaning your app purely uses cookie authentication and does not follow the proper OAuth2 and Open ID Connect implementation. Your web app or framework is making XMLHttpRequest calls to its API endpoint and using the Web Apps authentication cookie.
When reviewing the Fiddler capture and looking at the XMLHttpRequest request, the XMLHttpRequest might look something like this (Notice the cookie)…
If your using Asp.Net or Asp.Net Core. You will need to customize your Azure AD/Entra ID Configuration to not use the token lifetime as the session lifetime.
For more information see…
You can configure the API authentication to thrown an error instead of performing a redirect. This is just an example if your using Asp.Net Core…
Then, you can implement additional XMLHttpRequest logic to check if the request is done and whether it was a redirect or a 401. You will need to handle this to tell the client to have the user sign-in again. In most cases, simply refreshing the page will allow the user re-authenticate. For example…
client.onreadystatechange = () => {
// API call failed (401) or there was a redirect
if ((client.readyState === client.DONE && client.responseURL == "") || client.Status == 401) {
// Handle error such as Refreshing page should allow user to re-authenticate
window.location.reload(true)
}
};
Scenario: Standalone API using access token
When reviewing the Fiddler capture and looking at the XMLHttpRequest request, it might look something like this (Notice the Authorization header)…
If you are passing a access token to your API resource, ensure the token is valid in the first place. Check to see if the token is expired and if it is, request for a new access token.
If your using MSAL.js, then use acquireTokenSilent everytime to acquire a new token before passing it to your API. Do not cache this token yourself. Always use acquireTokenSilent to get the cached token directly from MSAL.
Ensure JWT Bearer authentication is used (Not Open ID Connect authentication). This implementation will vary depending on the authentication middleware you are using. Please review the docs for your authentication middleware as each middleware has its own implementation strategy. JWT Bearer Authentication should throw a 401 error to the client if the token is not valid. The client should handle the error and request for a new token as needed. If Open ID Connect Authentication scheme is used, the API will attempt to redirect the request to Azure AD/Entra ID or B2C, and this is what will cause the CORS error and it is much harder for the client to handle this scenario.
Here are just a couple examples on how to set up JWT Bearer authentication (Look at the ToDoService/API portion of the samples)
If your application is behind a load balancer, please check your load balancer for its session lifetime settings. This could be called Session persistence or session affinity.
Scenario: CORS error on Token endpoint
The only supported flow for Single Page Applications is Authorization Code flow with PKCE and Refresh Token flow while having the redirect address configured as Single Page Application.
Based on OAuth2 specs and Security best practices, you should NOT be using Resource Owner Password Credential flow or any of the Confidential Client flows i.e. Client Credentials or On-behalf-of flows.
All other flows will not be supported in Single Page Applications. Azure AD/Entra ID and B2C will not add the CORS headers for the unsupported flows.
Scenario: You are using Azure AD Application Proxy
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.
Do not forget the basic principles of Open ID Connect and OAuth2. When you want to protect an API with OAuth2 and Azure AD, you must pass an access token that will be validated. So if you want to test with SwaggerUI, on accessing the API portion, SwaggerUI must be configured to authenticate, acquire an access token, and pass it to the API.
builder.Services.AddSwaggerGen(options =>
{
var AzureAdMetadata = builder.Configuration["SwaggerClient:Metadata"]!;
options.AddSecurityDefinition("Oauth2", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
//Description = "Please enter a valid token",
Name = "Authorization",
Type = SecuritySchemeType.OpenIdConnect,
BearerFormat = "JWT",
Scheme = "Bearer",
OpenIdConnectUrl = new Uri(AzureAdMetadata),
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Oauth2" }
},
new string[]{}
}
});
});
// ...
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.OAuthClientId(builder.Configuration["SwaggerClient:ClientId"]);
options.OAuth2RedirectUrl(builder.Configuration["SwaggerClient:RedirectUrl"]);
options.OAuthScopes(new[] { builder.Configuration["SwaggerClient:Scopes"], "openid", "profile", "offline_access" });
options.OAuthUsePkce();
options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
//options.RoutePrefix = string.Empty;
});
Don’t forget to add your redirect URI “https://YOUR_APP_PATH/oauth2-redirect.html” as a single page application reply address on the client application registration.
Your appsettings.json will look something like this…
{
/*
The following identity settings need to be configured
before the project can be successfully executed.
For more info see https://aka.ms/dotnet-template-ms-identity-platform
*/
"SwaggerAPI": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "your_domain.onmicrosoft.com",
"TenantId": "YOUR_TENANT_ID",
"ClientId": "YOUR_API_APP_ID",
"Audience": "AUD_CLAIM_FROM_ACCESS_TOKEN"
},
"SwaggerClient": {
"Metadata": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration",
"ClientId": "YOUR_CLIENT_APP_ID",
"RedirectUrl": "https://YOUR_APP_PATH/oauth2-redirect.html",
"Scopes": "SCOPE_VALUE_FOR_YOUR_API"
},
And your authentication code if using Microsoft Identity Web would look like this…
When all said and done, after launching your Swagger app, you should now see an Authorize button at the top of your page. This will load the “Swagger Client” to acquire the access token. Then, when you test your API calls from the Swagger UI, it will automatically pass the access token.
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 are developing a WebAssembly authentication app and trying to implement Roles based access control. You are getting a similar error like…
You are not authorized to access this resource.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]Authorization failed. These requirements were not met: RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (ROLE_NAME)
The WebAssembly Authentication stack appears to cast the roles claim into a single string. We need this User Factory to modify its behavior so that each role has its own unique value.
Create the Custom User Factory
First, create a custom User Factory (CustomUserFactory.cs)…
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using System.Security.Claims;
using System.Text.Json;
public class CustomUserFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public CustomUserFactory(IAccessTokenProviderAccessor accessor)
: base(accessor)
{
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
var claimsIdentity = (ClaimsIdentity?)user.Identity;
if (account != null && claimsIdentity != null)
{
MapArrayClaimsToMultipleSeparateClaims(account, claimsIdentity);
}
return user;
}
private void MapArrayClaimsToMultipleSeparateClaims(RemoteUserAccount account, ClaimsIdentity claimsIdentity)
{
foreach (var prop in account.AdditionalProperties)
{
var key = prop.Key;
var value = prop.Value;
if (value != null && (value is JsonElement element && element.ValueKind == JsonValueKind.Array))
{
// Remove the Roles claim with an array value and create a separate one for each role.
claimsIdentity.RemoveClaim(claimsIdentity.FindFirst(prop.Key));
var claims = element.EnumerateArray().Select(x => new Claim(prop.Key, x.ToString()));
claimsIdentity.AddClaims(claims);
}
}
}
}
Add the roles mapping and CustomUserFactory to your authentication Middleware
Don’t forget to add App Roles to your app registration, add your user to the app role, and configure your application to use the app role you have created.
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.