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 an custom developed Web app or Web API using Asp.Net or Asp.Net Core and you want to control which issuers have access to your app. There are a couple ways to do this.
Use Multiple Authentication schemes
One way to do this and is probably the most recommended way is to perform what is documented here…
In this solution, you’ll want to have different Web App or API endpoints for your different users based on the authentication scheme to be used. Otherwise meaning having different entry points.
We have another blog that talk more about how to implement multiple authentication schemes for B2C apps that need to handle multiple custom B2C policies…
If you really want to stick to one entry point, here is another option.
If you have a simple scenario where the Identity Provider is Azure Active Directory and the same signing keys can be used, you can simply add a list of Issuers you can validate… (Keep in mind each tenant in Azure AD will have a different issuer value)
When you want to validate multiple tokens from different issuers and the signing keys are different, this gets tricky. Now you have to consider how are you going to discover the signing keys.
The following code snippet is just an example of custom logic you could build…
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
// IdentityModelEventSource.ShowPII is used for debugging
IdentityModelEventSource.ShowPII = true;
var tvp = new TokenValidationParameters
{
# Event handler for deciding which signing keys to use
IssuerSigningKeyResolver = (s, token, kid, tvp) => {
IConfigurationManager<OpenIdConnectConfiguration> configManager = null!;
// Support AAD/B2C
if (token.Issuer.Contains("login.microsoftonline.com") || token.Issuer.Contains("sts.windows.net") || token.Issuer.Contains("b2clogin.com"))
{
configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{token.Issuer}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
}
// Support other IdP
else
{
configManager = new ConfigurationManager<OpenIdConnectConfiguration>(builder.Configuration["OtherIdp:Metadata"], new OpenIdConnectConfigurationRetriever());
}
OpenIdConnectConfiguration config = configManager.GetConfigurationAsync(System.Threading.CancellationToken.None).GetAwaiter().GetResult();
return config.SigningKeys;
},
ValidAudiences = builder.Configuration.GetSection("OtherIdp:ValidAudiences").Get<List<string>>(),
ValidIssuers = builder.Configuration.GetSection("OtherIdp:ValidIssuers").Get<List<string>>(),
};
Multiple Audience Validation
If your tokens are coming with different “aud” claim, you can add additional Audience validations…
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration, "AzureAd");
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
//...
// Other token validation parameters
//...
var tvp = new TokenValidationParameters
{
//...
// Other token validation parameters
//...
ValidAudiences = builder.Configuration.GetSection("AzureAd:ValidAudiences").Get<List<string>>(),
};
options.TokenValidationParameters = tvp;
Your appsettings.json should look something like this…
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
The AAD Graph PowerShell SDK allowed you to use a client secret for the Application only ( Service Principal ) login flow – also known as the client_credentials grant flow. The documentation for the new Microsoft Graph PowerShell SDK does not tell you how to use a client secret but instead, uses the more secure certificate method for the flow: Use app-only authentication with the Microsoft Graph PowerShell SDK | Microsoft Docs This post will show you how you can still use a client secret if you want by obtaining an access token and then using the -AccessToken parameter on the Connect-MgGraph command.
Assuming that you already have an app registration configured for this and it has the proper application permissions consented to for the request you want to make, we will use the PowerShell command “Invoke-RestMethod” to obtain an access token using the client_credentials grant flow.
This first part of the script will obtain an access token with any consented Microsoft Graph application permissions. Once the access token is obtained, we can then set the -AccessToken parameter on the Connect-MgGraph request and make our graph requests accordingly.
Don’t forget to disconnect once you’re done. If you’re performing a long running task ( such as paging through records ) you may need to renew your access token with the same method as in the first part.
Note: You can also use the ROPC flow to get the access token the same way as the client credentials flow. We don’t recommend the ROPC flow and by default, it is disabled for federated users unless you have allow this with a home realm discovery policy.
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:
There are times a web application may need to log in a user and call different backend Azure AD protected web APIs. The web application would need to obtain different Access Tokens, one for each web API. In this post I will attempt to demonstrate how this can be done using MIcrosoft.Identity.Web nuget package. This sample shows how to get tokens for Microsoft Graph resource and a custom web API resource. The project was developed using Visual Studio 2022 and .Net 6 framework.
App Registrations
I have both a web application and a web API registered in my Azure AD tenant with the following configuration:
Web App
Redirect URI: set the redirect URI for Web platform
Secret: create a client secret in the ‘Certificates & secrets’ blade
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
/*
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
*/
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.MapControllers();
app.Run();
The sample uses ITokenAcqisition to get the access token for the downstream API as followed. The AuthorizeForScopes atrribute decoration on the controller is for handling dynamic consent if the requested API permission has not been consented yet. It’s important to have the same scopes defined in both the AuthorizeForScopes attribute and the GetAccessTokenForUserAsync call in the controller in order for consent to work correctly if needed.
Note: The web app should call GetAccessTokenForUserAsync each time an access token is needed.
Change the values in the following sections of the appsettings.json file with your own web application’s registration value and the scopes for your downstream web APIs:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
"TenantId": "[Enter the tenant ID of the registered web app]",
"ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc",
"ClientSecret": "[Enter the web application Client Secret]"
},
"GraphScope": {
"CalledApiScopes": "user.read.all application.read.all",
"CalledApiUrl": "https://graph.microsoft.com/v1.0"
},
"ApiScope": {
"CalledApiScopes": "api://18b050ec-9734-43b4-85a1-1b5dad64cea1/hello",
"CalledApiUrl": "https://myapi.com"
},
Run the project and browse to https://localhost:7035 and click on either ‘Graph Token’ link or the ‘WebAPI Token’ link in the menu to see the Access Token:
These signature validation errors are caused when the resource provider (not Azure AD) is unable to validate the signature of the token, either because the signing key could not be found or the signing key used was not able to validate the signature. This article will describe the most common scenarios and solutions. The concept and root cause is still the same and will continue to apply. Unfortunately, many developers and vendors implement their token validation differently.
First, get the access token being sent to the resource provider. We need to decode this to review (3) important claims:
Let’s get the easy one out of the way. If you send a Microsoft Graph access token to any other resource provider that’s not Microsoft Graph, you will get a Signature validation error. Only Microsoft Graph can validate these tokens. You will know if this is a Microsoft Graph token when taking a look at the “aud” claim and its value is one of the following:
https://graph.microsoft.us
https://graph.microsoft.us/
https://graph.microsoft.com
https://graph.microsoft.com/
https://dod-graph.microsoft.us
https://dod-graph.microsoft.us/
00000003-0000-0000-c000-000000000000
Make sure you are acquiring the correct access token for the resource provider and the “aud” claim is what the resource provider is expecting. The audience of access tokens is determined by the scope parameter that is sent in the request when acquiring access tokens. So if you want to get an access token for https://api.contoso.com, then the scope will look something like this: https://api.contoso.com/read
For more information about Azure AD and exposing your API
As for the other scenarios, the resource provider determines where to get the signing keys based on the OpenId Connect Metadata configuration and which signing key to use based on the “kid” claim in the access token. This is configured on the resource provider such as your custom built API or API Authentication layer. If you’re using a Microsoft Authentication library like MSAL or Microsoft Identity Web, the default is generally:
The OpenId Connect Metadata endpoint has a property that provides the location to the signing keys. This is the jwks_uri also known as the discovery keys endpoint. So, depending on which OpenId Connect Metadata endpoint is used, it will return a URL for the jwks_uri. Here is a table that provides a few examples:
Example 1: Metadata endpoint: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration Discovery keys endpoint: https://login.microsoftonline.com/common/discovery/v2.0/keys
Example 2: Metadata endpoint: https://login.microsoftonline.com/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/v2.0/.well-known/openid-configuration Discovery keys endpoint: https://login.microsoftonline.com/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/discovery/v2.0/keys
Example 3: Metadata endpoint: https://login.microsoftonline.us/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/v2.0/.well-known/openid-configuration Discovery keys endpoint: https://login.microsoftonline.us/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/discovery/v2.0/keys
Example 4: Metadata endpoint: https://login.microsoftonline.us/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/v2.0/.well-known/openid-configuration?appid=06051593-1954-4e1f-a75f-7e5de243aeff Discovery keys endpoint: https://login.microsoftonline.us/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/discovery/v2.0/keys?appid=06051593-1954-4e1f-a75f-7e5de243aeff
Our discovery keys endpoint may contain more than one signing key. So, if you’re manually looking for a specific key and not the signing key provided by the access token or your caching keys, your signature validation may fail as we do frequently rotate keys.
Assuming the token is issued from Azure AD and not B2C, When an application in Azure AD is enabled for SAML SSO, the signing key used to sign tokens is the SAML signing certificate that was generated when SAML SSO was enabled.
So when you look for the “kid” from the access token on the default discovery keys endpoint, usually “https://login.microsoftonline.com/common/discovery/v2.0/keys”, you may not see it listed. There are a couple solutions. First and foremost you should keep your apps seperate for OAuth2 and SAML. It is not recommened to use the same app that will perform both OAuth2 and SAML.
Solution (1): Create a new app registration and do not enable SAML SSO (This is the recommended approach)
Solution (2): You can convert the Enterprise App to use OAuth2 only. To do this…
Disable SAML SSO (this is the preferredSingleSignOnMode property on the servicePrincipal by setting it to null or oidc.)
Keep in mind {appid} is the application-id of the application in Azure AD.
Examples of where to configure OpenId Connect Metadata configurations
Make sure you set the OpenId Connect Metadata configuration based on whether the token is issued from Azure AD or Azure B2C or if you need to add “?appid={appid}”
Generally if you configure the Azure AD instance and Tenant, or Authority correctly, this will resolve your issue.
You can also use the Directory id or any verified domain. Its recommended to use the Directory ID or the initial domain provided by Azure AD (i.e. contoso.onmicrosoft.com).
Authority
If configuring a Authority, generally Instance and Tenant is not needed as Authority follows this format:
Generally the Metadata Address is built based on this Instance/Tenant/Authority configuration and will automatically concatenate “/.well-known/openid-configuration” at the end.
The following sections provide examples of manually specifying the Metadata Address.
This blog walks through how to set up MSAL.JS to authenticate directly to ADFS 2019 Server using Authorization Code Grant flow to get an Access Token and then call a Web API with that Access Token. We will go over the following steps to get this the samples working:
App Registrations for both the Single Page Application (SPA) client app and the web API app
Enable Cross-origin Request Sharing (CORS) on ADFS 2019 Server
Construct a SPA client application project
Construct a web API application project
Let’s get into the detail for each of the above steps.
App Registrations
Follow steps 1 – 8 In the App Registration in ADFS Documentation to add a new Application Group. We will register a Native Public App type and a web API type in this Application Group:
Note: unlike Azure AD, ADFS does not have a concept of Single Page Application client (platform type) so we will treat a SPA app as a generic native client app. In order for one application to call another application with an Access Token, both of these applications have to be in the same Application Group.
I use the following settings in my App Registration:
Take note of the Native application’s Client Id. We will need it later for our client project.
Web API Relying Party identifier: https://localhost:44320. Take note of this value since we will use it for both the web API sample and the SPA client app sample below.
CORS is disabled by default on ADFS 2019 Server. We need to enable CORS in order for our MSAL.JS V2 SPA application to authenticate to ADFS 2019 Server. Run the following PowerShell Command on the ADFS Server to enable CORS and set the trusted origin value to the domain of the application making the CORS request (http://localhost:3000 in this case for our SPA client app).
The code for this application is derived from this sample. The complete sample is here on github. Modify the project’s App/AuthConfig.js file to put in your app registration info:
The values for msalConfig’s auth’s (below) clientId and redirectUri comes from Native App registration above. Fill in the authority and knownAuthorities values with your ADFS environment. For apiConfig’s scopes, the web API scope should be in this format: <web API Relying Party Identifier>/<scope name>, so I set this value to https://localhost:44320/openid (we enabled openid for permission during registration).
const msalConfig = {
auth: {
clientId: "<Native application client ID>",
authority: "https://<FQDN_OF_ADFS_Server>/adfs/", // e.g https://contoso.com/adfs/
knownAuthorities: ["https://<FQDN_OF_ADFS_Server>/adfs/"], // e.g https://contoso.com/adfs/
redirectUri: "<Native Application's redirect URI>", // "http://localhost:3000" for this sample
protocolMode: "OIDC"
},
cache: {
cacheLocation: "localStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
};
// Add here the endpoints and scopes for the web API you would like to use.
const apiConfig = {
uri: "https://localhost:44321/api/values", // e.g. http://localhost:5000/api
scopes: ["<fully qualified scope name for web API>"] // e.g. ["https://localhost:44320/openid"]
};
/**
* Scopes you add here will be prompted for user consent during sign-in.
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
* For more information about OIDC scopes, visit:
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
const loginRequest = {
scopes: ["openid", "profile"]
};
/**
* Scopes you add here will be used to request a token from Azure AD to be used for accessing a protected resource.
* To learn more about how to work with scopes and resources, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
*/
const tokenRequest = {
scopes: [...apiConfig.scopes],
};
ASP.Net Web API sample
This sample project is derived from this ADFS Web API project. The Web API project was extended to support CORS request for the SPA client. Guidance for enable CORS is here. The complete web API sample is here. Find the following entries in the web.config file and replace it with your web API registration info. Use the web API’s Relying party Identifier for the “ida:Audience” value and fill in the FQDN name of your ADFS Server for the “ida:AdfsMetadataEndpoint” value.
<add key="ida:Audience" value="https://localhost:44320" />
<add key="ida:AdfsMetadataEndpoint" value="https://[Enter your AD FS hostname]/federationmetadata/2007-06/federationmetadata.xml" />
using Microsoft.Owin.Security.ActiveDirectory;
using Owin;
using System.Configuration;
using System.IdentityModel.Tokens;
namespace TodoListService
{
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.UseActiveDirectoryFederationServicesBearerAuthentication(
new ActiveDirectoryFederationServicesBearerAuthenticationOptions
{
MetadataEndpoint = ConfigurationManager.AppSettings["ida:AdfsMetadataEndpoint"],
TokenValidationParameters = new TokenValidationParameters()
{
SaveSigninToken = true,
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
}
});
}
}
}
I decorate the controller with the following attribute to enable CORS request for the controller’s methods:
[Authorize]
[EnableCors(origins: "http://localhost:3000", headers: "*", methods: "*")]
public class ValuesController : ApiController
{
Running the sample projects
Build and run the web API. It will open a browser and navigate to https://localhost:44321/
Run the SPA application: npm install followed by npm start to start the node server
Open a web browser and navigate to http://localhost:3000/
Click ‘Sign-in’ button on top right to sign in with your account at the ADFS sign in page
If sign in is successful the page should display the signed-in name, id token, and access token (this is not the web API access token)
Click the ‘Call API’ button and it should display the Access Token for the web API and the response from the web API as followed:
Customers can get the following error when authenticating to Azure Active Directory with a federated account using MSAL (or now deprecated ADAL) Authentication library.
{ error: "invalid_request", error_description: "AADSTS7500514: A supported type of SAML response was not found. The supported response types are 'Response' (in XML namespace 'urn:oasis:names:tc:SAML:2.0:protocol') or 'Assertion' (in XML namespace 'urn:oasis:names:tc:SAML:2.0:assertion').
....
error_uri: "https://login.microsoftonline.com/error?code=7500514"
}
The error is typically seen in the following environment:
A federated account using PingFederate as the Identity Provider (IDP) is used for authentication
The IDP is configured to issue a SAML 1.1 token (via WS-Trust protocol).
ADAL’s AcquireToken(string resource, string clientId, UserCredential userCredential) overload method is used
any PowerShell module that uses ADAL or MSAL’s above methods for authentication
Why is this error happening when I am using ADAL or MSAL’s API?
In order to understand this error, we need to understand a little bit about how the API works when passing in a username (User Principal Name (UPN)) and password. Since ADAL library is becoming deprecated, we will discuss this in terms of MSAL library (the recommended authentication library over ADAL). The above MSAL API has an internal heuristics to determine if the user account is managed (cloud account) or federated and will take different code paths (OAuth2 grant flows) to authenticate silently to Azure AD. For managed accounts, MSAL uses Resource Owner Password Credentials Grant (ROPC) flow, while for federated accounts, it uses SAML Assertion Grant flow for authentication. If you notice, there are two steps involved in the SAML Assertion Grant flow:
The client application authenticates to the federated IDP (typically using WS-Trust protocol) to obtain a SAML token
the client then uses the obtained SAML token to get an OAuth2 JWT token from Azure AD
The problem typically happens in step 1 where the client application (MSAL in this case) needs to parse the SAML response from the IDP to determine the SAML version. Historically, both ADAL and MSAL were developed to support how ADFS works in conjunction with Azure AD for federated accounts, so it expects certain attribute values present in the SAML response to determine the version of the SAML token. The SAML token version shows up in a couple of places: <saml:Assertion> node and <TokenType> node. Below is ADFS Response from the usernamemixed endpoint:
SAML Assertion: major version = 1 and minor version = 1
TokenType: urn:oasis:names:tc:SAML:1.0:assertion
Compare the same request when PingFederate is an IDP:
Note that Ping returns a different TokenType value: http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1 for the same SAML 1.1 token.
In step 2 of the SAML Assertion Grant flow, the value of the ‘grant_type’ parameter are different for different SAML versions. They are listed below:
urn:ietf:params:oauth:grant-type:saml2-bearer – for SAML2.0 token
urn:ietf:params:oauth:grant-type:saml1_1-bearer – for SAML1.1 token
In this case with PingFederate account, MSAL uses the wrong ‘grant_type’ parameter due to the above logic, leading to the error since there is a version mismatch between the ‘grant_type’ parameter and the assertion (containing the actual SAML token) parameter:
For comparison, here is the same token request when ADFS is the IDP:
Ok so I understand the problem. What should I do to resolve this?
The problem is typically due to a misconfiguration on the PingFederate side for Azure AD and Office 365 apps. PingFederate has the following recommended documentation for creating a connection to Azure AD:
Pay close attention to step 2d from the first doc for configuring WS-Trust setting:
And from the second documentation, select SAML 1.1 for Office 365 as the Default Token Type
Note: setting the above value for Default Token Type ensures Ping sends back the same TokenType value as ADFS (aka urn:oasis:names:tc:SAML:1.0:assertion)
Consider opening a support case with PingFederate should you require more guidance on the above documentation.
Reference
For information on how to use PostMan to perform SAML Assertion Grant flow, refer to this blog.