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

Introduction

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

App Registrations

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

Back-end web API:

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

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

Front-end console application:

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

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

Application Code

Front-end Console application

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

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

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

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

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

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

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

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

}


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

Web API

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

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

The complete web API project is here.

The project has a protected controller with the following attributes:

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

Here is the Program.cs file:

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

var builder = WebApplication.CreateBuilder(args);

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

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

var app = builder.Build();

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

app.UseHttpsRedirection();

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

app.MapControllers();

app.Run();

Troubleshooting CORS to Azure AD/Entra ID

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’

Notice it starts with “https://login.microsoftonline.com“, you might have a Azure B2C scenario, so in that case it might start with “https://youdomain.b2clogin.com/…

It is outside of scope for this article If the error is not generated by Azure AD/Entra ID, and the error looks something like this…

Access to XMLHttpRequest at ‘https://app.contoso.com/…

We do not provide guidance on how to resolve CORS for external domains. More than likely, you will need to enable CORS in that environment.

The following articles provides “some” additional guidance…

Let’s get started

First we need to understand what is CORS…

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.

For more information about CORS…

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

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…

https://learn.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code?tabs=apptype#single-page-applications

Solutions to consider based on scenario

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)…

GET https://app.domain.com/… 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
Cookie: .AspNet.Cookies=xyz…

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…

services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{       
  options.Events.OnRedirectToIdentityProvider = (context) =>
  {
    if (!context.Request.Headers["Origin"].IsNullOrEmpty())
    {
      context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
      context.HandleResponse();
    }
                    
    return Task.FromResult(true);
  };
}

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)…

GET https://app.domain.com/… HTTP/1.1

Host: login.microsoftonline.com
Connection: keep-alive
Authorization: Bearer eyJ0…
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

Send a valid token

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.

https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-acquire-token?tabs=javascript2

Here is just a example of how it looks when passing a token to an API…

Build single-page app calling a web API – Microsoft Entra | Microsoft Learn

Use JWT Bearer authentication

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)

https://learn.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code?tabs=apptype#web-api

Scenario: If the issue is occurring while using MSAL.js to acquire a token

Make sure you configure, authority, knownAuthorities and protocolMode correctly if your using B2C or third-party IdP.

//…
import { ProtocolMode } from '@azure/msal-common';
//…
function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication({
    auth: {
      authority: 'https://contoso.b2clogin.com/tfp/655e51e9-be5e-xxxx-xxxx-38aa6558xxxx/b2c_1_susi/v2.0/',
      clientId: 'fb2ad7b7-2032-4a66-8723-e993eb4b9004',
      redirectUri: 'http://localhost:4200',
      knownAuthorities: ['contoso.b2clogin.com'],
      protocolMode: ProtocolMode.OIDC
    },
  });
}

For more information see…

https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md

Scenario: App behind a Load balancer

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

Please see…
https://learn.microsoft.com/en-us/azure/active-directory/app-proxy/application-proxy-configure-complex-application

Adding multiple issuer and audience validation in C#

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…

https://github.com/AzureAD/microsoft-identity-web/wiki/multiple-authentication-schemes

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…

https://blogs.aaddevsup.xyz/2022/10/how-to-resolve-idx10501-errors-in-a-b2c-microsoft-identity-web-application/

Simple Multiple Issuer validation

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)

# Add AAD JWT Bearer Authentication
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);

builder.Services.Configure<JwtBearerOptions>(options =>
{ 
  var tvp = new TokenValidationParameters
  {
    ValidIssuers =  builder.Configuration.GetSection("AzureAd:ValidIssuers").Get<List<string>>(),
  };

  options.TokenValidationParameters = tvp;
});

Your appsettings.json should look something like this…

{
  "AzureAd": {
    //... 
    // Other Settings
    //...

    "ValidIssuers": [
      // Validate AAD v2 tokens
      "https://login.microsoftonline.com/{TENANT_1}/v2.0/", 
      "https://login.microsoftonline.com/{TENANT_2}/v2.0/" 
      // Validate AAD v1 tokens
      "https://sts.windows.net/{TENANT_1}/", 
      "https://sts.windows.net/{TENANT_2}/"
    ]
}

Complex Multiple Issuer Validation (and signature validation)

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…

{
  "AzureAd": {
    //... 
    // Other Settings
    //...

    "ValidAudiences": [
      "api://c60eeff9-1329-4ddb-ba4a-7e6555391c4d",
      "c60eeff9-1329-4ddb-ba4a-7e6555391c4d",
      "api://e92f9573-6f44-46b2-a4a9-1936d5f16ab5",
      "e92f9573-6f44-46b2-a4a9-1936d5f16ab5",
    ]
}

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

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

App Registration:

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

The Code:

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

Logging:

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

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

Fiddler HTTPS capture

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

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

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

import sys
import json, logging, msal, requests

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

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

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

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

result = None

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

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

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

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

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

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

References:

msal.application.ClientApplication class | Microsoft Learn

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

Microsoft Graph PowerShell SDK – Use Client Secret instead of Certificate for Service Principal login

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.

$tenantId = "{your_tenant_id}"
$clientId = "{your_app_id}"
$clientSecret = "{your_client_secret}"

$body = @{
    grant_type="client_credentials";
    client_id=$clientId;
    client_secret=$clientSecret;
    scope="https://graph.microsoft.com/.default";
}

$response = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token -Body $body
$accessToken = $response.access_token

$accessToken

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.

Connect-MgGraph -AccessToken $accessToken
$user = Get-MgUser -Filter "userPrincipalName eq 'ray@mytesttenant.com'"

$user

Disconnect-MgGraph

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.

$body = @{"client_id"="{your_client_id}"
          "scope"="{scopes}"
          "client_secret"="{your_client_secret}"
          "username"="{user upn}"
          "password"="{user password}"
          "grant_type"="password"
          }

$response = Invoke-RestMethod 'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token' -Method 'POST' -Body $body

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

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

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

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

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

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

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

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

@SpringBootApplication
public class MsalB2CWebSampleApplication {

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

HTTPS support

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

App Registration

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

Below is how logging should look like when done correctly:

Using Microsoft.Identity.Web to request multiple different Azure AD Access Tokens

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

API permission is set as followed

Web API

scope defined for the web API:

Application Code

Add .EnableTokenAcquistionToCallDownstreamApi() in Program.cs file to expose the ITokenAcquisition Service to acquire access tokens for the downstream API (See https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-call-api-app-configuration?tabs=aspnetcore#startupcs for more info). Complete code for the project is at this github link.

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.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;

namespace WebAppWebAPIs.Controllers
{
    // https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/adding-controller?view=aspnetcore-6.0&tabs=visual-studio
    [Authorize]
    public class WebAPIController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        readonly ITokenAcquisition tokenAcquisition;
        private IConfiguration _configuration;
        public WebAPIController(ILogger<HomeController> logger, ITokenAcquisition tokenAcquisition, IConfiguration configuration)
        {
            _logger = logger;
            this.tokenAcquisition = tokenAcquisition;
            _configuration = configuration;
        }

        [AuthorizeForScopes(ScopeKeySection = "ApiScope:CalledApiScopes")]
        public async Task<IActionResult> Api()
        {
            string[] scopes = _configuration.GetValue<string>("ApiScope:CalledApiScopes").Split(" "); ;
            string token = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
            ViewBag.ApiToken = token;
            return View();
        }
...

In the sample I define all my web API scopes in the appsettings.json file:

  "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"
  },

Running the sample:

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:

Other Sample

Take a look at https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/3-WebApp-multi-APIs for a different sample using Microsoft Graph SDK for .Net to call both Microsoft Graph and Azure Resource Manager endpoint.

Troubleshooting Signature validation errors

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:

  • aud
  • iss
  • kid

You can decode access tokens using https://jwt.ms

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

https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-expose-web-apis#:~:text=Azure%20Add%20Scope%201%20Sign%20in%20to%20the,or%20specify%20your%20own.%20Field%20…%20See%20More.

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:

https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

If you have configured a Tenant ID that is for example “e21bbca2-1b75-4dea-9e34-d3d95d2ec661” then the MetadataAddress would be:

https://login.microsoftonline.com/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/v2.0/.well-known/openid-configuration

If you configured a “Authority” that looks like https://login.microsoftonline.us/e21bbca2-1b75-4dea-9e34-d3d95d2ec661 then the MetadataAddress would be:

https://login.microsoftonline.us/e21bbca2-1b75-4dea-9e34-d3d95d2ec661/v2.0/.well-known/openid-configuration

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.

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-signing-key-rollover

The content of our discovery keys endpoint will look something like this:

	"keys": [
		{
			"kty": "RSA",
			"use": "sig",
			"kid": "nOo3ZDrODXEK1jKWhXslHR_KXEg",
			"x5t": "nOo3ZDrODXEK1jKWhXslHR_KXEg",
			"n": "oaLLT9hkcSj2tGf...",
			"e": "AQAB",
			"x5c": [
				"MIIDBTCCAe..."
			],
			"issuer": "https://login.microsoftonline.com/aa00d1fa-5269-4e1c-b06d-30868371d2c5/v2.0"
		},

The “kid” claim in the access token needs to match one of the available keys available on the discovery key endpoint based on the “kid” property.

If the “kid” does not match between the “kid” on the access token and the keys available on the discovery endpoint, there are two possible reasons:

  1. Azure AD and B2C uses different signing keys
  2. The application is enabled for SAML SSO.

Azure AD and B2C uses different signing keys

Next, we will take a look at the “iss” claim of the access token. For tokens issued by Azure AD, this will be one of the following values…

For tokens issued by Azure B2C, this will look something like this…

https://{your-domain}.b2clogin.com/tfp/[your-tenant-id}/{your-policy-id}/v2.0/

This is where it is really important to make sure your OpenId Connect Metadata configuration on the resource provider is configured correctly.

For tokens issued by Azure AD, make sure the OpenId Connect Metadata configuration looks something like this:

https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

For more information regarding to Azure AD OpenId Connect configurations:

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc

For tokens issued by Azure B2C, make sure the OpenId Connect Metadata configuration looks something like this:

https://{domain}.b2clogin.com/{tenant-id}/{b2c-policy}/v2.0/.well-known/openid-configuration

For more information regarding to Azure B2C OpenId Connect configurations:

https://docs.microsoft.com/en-us/azure/active-directory-b2c/openid-connect

 

The application is enabled for SAML SSO.

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.)

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.

Instance

Azure AD instance would be https://login.microsoftonline.com

For more information about Azure AD Instances:

https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud

Tenant

Tenant would be contoso.onmicrosoft.com

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:

{Instance}/{Tenant}

So, Authority would be https://login.microsoftonline.com/contoso.onmicrosoft.com

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.

Using Microsoft Identity Web

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                    .AddMicrosoftIdentityWebApp(
                       options =>
                       {
                           Configuration.Bind("AzureAd", options);
                           options.MetadataAddress = metadataAddress,
                           
                       })
                       .EnableTokenAcquisitionToCallDownstreamApi(options => Configuration.Bind("AzureAd", options), initialScopes)
                         .AddMicrosoftGraph(Configuration.GetSection("GraphAPI"))
                         .AddInMemoryTokenCaches();

For more information, see:

https://github.com/AzureAD/microsoft-identity-web/wiki/customization

Using Asp.Net standard framework using “UseWindowsAzureActiveDirectoryBearerAuthentication

Set the Metadata to https://login.microsoftonline.com/{tenant-id}/.well-known/openid-configuration

            app.UseWindowsAzureActiveDirectoryBearerAuthentication(
                new WindowsAzureActiveDirectoryBearerAuthenticationOptions
                {
                    MetadataAddress = metadataAddress,
                    Tenant = ConfigurationManager.AppSettings["ida:Tenant"],

Using Asp.Net standard framework using “UseOpenIdConnectAuthentication

You can either set the Authority as https://login.microsoftonline.com/{tenant-id}/v2.0

public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                // Sets the ClientId, authority, RedirectUri as obtained from web.config
                ClientId = clientId,
                Authority = authority,

or set the Metadata to https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration

        public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                // Sets the ClientId, authority, RedirectUri as obtained from web.config
                ClientId = clientId,
                MetadataAddress = metadataAddress,

Using Azure App Service Authentication

Please see the following article (Its configured using the Issuer URL):

https://docs.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad#-enable-azure-active-directory-in-your-app-service-app

Using Azure API Management

Configuring for Azure B2C: https://docs.microsoft.com/en-us/azure/active-directory-b2c/secure-api-management?tabs=app-reg-ga

Additional resources:

https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens#validating-tokens

https://openid.net/specs/openid-connect-discovery-1_0.htm

MSAL.JS SPA client performing Authorization Code Grant flow to ADFS 2019

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:

  1. App Registrations for both the Single Page Application (SPA) client app and the web API app
  2. Enable Cross-origin Request Sharing (CORS) on ADFS 2019 Server
  3. Construct a SPA client application project
  4. 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:

Native application Redirect URI: http://localhost:3000

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.

Enable CORS feature

Refer to Customize HTTP security response headers with AD FS 2019 for more info.

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).

Set-AdfsResponseHeaders -EnableCORS $true
Set-AdfsResponseHeaders -CORSTrustedOrigins http://localhost:3000

SPA client app sample using MSAL.JS V2

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" />

The sample uses ActiveFederationServicesBearerAuthentication middleware to integrate with ADFS Authentication.

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

  1. Build and run the web API. It will open a browser and navigate to https://localhost:44321/
  2. Run the SPA application: npm install followed by npm start to start the node server
  3. Open a web browser and navigate to http://localhost:3000/
  4. Click ‘Sign-in’ button on top right to sign in with your account at the ADFS sign in page
  5. 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)
  6. 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:

Receiving error AADSTS7500514: A supported type of SAML response was not found when authenticating to Azure AD with a federated account

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).
  • The application uses one of the following API:
    • MSAL’s AcquireTokenByUserNamePassword
    • 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:

  1. The client application authenticates to the federated IDP (typically using WS-Trust protocol) to obtain a SAML token
  2. 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:

  1. SAML Assertion: major version = 1 and minor version = 1
  2. 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.

For SAML 1.1 token, MSAL does not support any TokenType value other than urn:oasis:names:tc:SAML:1.0:assertion (what ADFS uses), so when an IDP returns a different value, MSAL incorrectly thinks the SAML token version is 2.0. See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/1871 for more info.

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:

Creating a connection to Azure Active Directory
Configuring WS-Trust STS

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.