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

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

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