Migrate confidential client applications from ADAL.NET to MSAL.NET
บทความ
In this how-to guide you'll migrate a confidential client application from Azure Active Directory Authentication Library for .NET (ADAL.NET) to Microsoft Authentication Library for .NET (MSAL.NET). Confidential client applications include web apps, web APIs, and daemon applications that call another service on their own behalf. For more information about confidential apps, see Authentication flows and application scenarios. If your app is based on ASP.NET Core, see Microsoft.Identity.Web.
For app registrations:
You don't need to create a new app registration. (You keep the same client ID.)
You don't need to change the preauthorizations (admin-consented API permissions).
Migration steps
Find the code that uses ADAL.NET in your app.
The code that uses ADAL in a confidential client app instantiates AuthenticationContext and calls either AcquireTokenByAuthorizationCode or one override of AcquireTokenAsync with the following parameters:
A resourceId string. This variable is the app ID URI of the web API that you want to call.
An instance of IClientAssertionCertificate or ClientAssertion. This instance provides the client credentials for your app to prove the identity of your app.
Update the code according to the confidential client scenario. Some steps are common and apply across all the confidential client scenarios. Other steps are unique to each scenario.
Confidential client scenarios:
Daemon scenarios supported by web apps, web APIs, and daemon console applications.
Web app calling web APIs supported by web apps that sign in users and call a downstream web API.
You might have provided a wrapper around ADAL.NET to handle certificates and caching. This guide uses the same approach to illustrate the process of migrating from ADAL.NET to MSAL.NET. However, this code is only for demonstration purposes. Don't copy/paste these wrappers or integrate them in your code as they are.
Daemon scenarios use the OAuth2.0 client credential flow. They're also called service-to-service calls. Your app acquires a token on its own behalf, not on behalf of a user.
Find out if your code uses daemon scenarios
The ADAL code for your app uses daemon scenarios if it contains a call to AuthenticationContext.AcquireTokenAsync with the following parameters:
A resource (app ID URI) as a first parameter
IClientAssertionCertificate or ClientAssertion as the second parameter
AuthenticationContext.AcquireTokenAsync doesn't have a parameter of type UserAssertion. If it does, then your app is a web API, and it uses the web API calling downstream web APIs scenario.
Update the code of daemon scenarios
The following steps for updating code apply across all the confidential client scenarios:
Add the MSAL.NET namespace in your source code: using Microsoft.Identity.Client;.
Instead of instantiating AuthenticationContext, use ConfidentialClientApplicationBuilder.Create to instantiate IConfidentialClientApplication.
Instead of the resourceId string, MSAL.NET uses scopes. Because applications that use ADAL.NET are preauthorized, you can always use the following scopes: new string[] { $"{resourceId}/.default" }.
Replace the call to AuthenticationContext.AcquireTokenAsync with a call to IConfidentialClientApplication.AcquireTokenXXX, where XXX depends on your scenario.
In this case, replace the call to AuthenticationContext.AcquireTokenAsync with a call to IConfidentialClientApplication.AcquireTokenClient.
Here's a comparison of ADAL.NET and MSAL.NET code for daemon scenarios:
ADAL
MSAL
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public partial class AuthWrapper
{
const string ClientId = "Guid (AppID)";
const string authority
= "https://login.microsoftonline.com/{tenant}";
// App ID URI of web API to call
const string resourceId = "https://target-api.domain.com";
X509Certificate2 certificate = LoadCertificate();
public async Task<AuthenticationResult> GetAuthenticationResult()
{
var authContext = new AuthenticationContext(authority);
var clientAssertionCert = new ClientAssertionCertificate(
ClientId,
certificate);
var authResult = await authContext.AcquireTokenAsync(
resourceId,
clientAssertionCert,
);
return authResult;
}
}
using Microsoft.Identity.Client;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public partial class AuthWrapper
{
const string ClientId = "Guid (Application ID)";
const string authority
= "https://login.microsoftonline.com/{tenant}";
// App ID URI of web API to call
const string resourceId = "https://target-api.domain.com";
X509Certificate2 certificate = LoadCertificate();
IConfidentialClientApplication app;
public async Task<AuthenticationResult> GetAuthenticationResult()
{
var app = ConfidentialClientApplicationBuilder.Create(ClientId)
.WithCertificate(certificate)
.WithAuthority(authority)
.Build();
// Setup token caching https://learn.microsoft.com/azure/active-directory/develop/msal-net-token-cache-serialization?tabs=aspnet
// For example, for an in-memory cache with 1GB limit, use
app.AddInMemoryTokenCache(services =>
{
// Configure the memory cache options
services.Configure<MemoryCacheOptions>(options =>
{
options.SizeLimit = 1024 * 1024 * 1024; // in bytes (1 GB of memory)
});
}
var authResult = await app.AcquireTokenForClient(
new [] { $"{resourceId}/.default" })
// .WithTenantId(specificTenant)
// See https://aka.ms/msal.net/withTenantId
.ExecuteAsync()
.ConfigureAwait(false);
return authResult;
}
}
Benefit from token caching
If you don't setup token caching, the token issuer will throttle you, resulting in errors. It also takes a lot less to get a token from the cache (10-20ms) than it is from ESTS (500-30000ms).
Web APIs that call downstream web APIs use the OAuth2.0 on-behalf-of (OBO) flow. The web API uses the access token retrieved from the HTTP Authorize header and it validates this token. This token is then exchanged against a token to call the downstream web API. This token is used as a UserAssertion instance in both ADAL.NET and MSAL.NET.
Find out if your code uses OBO
The ADAL code for your app uses OBO if it contains a call to AuthenticationContext.AcquireTokenAsync with the following parameters:
A resource (app ID URI) as a first parameter
IClientAssertionCertificate or ClientAssertion as the second parameter
A parameter of type UserAssertion
Update the code by using OBO
The following steps for updating code apply across all the confidential client scenarios:
Add the MSAL.NET namespace in your source code: using Microsoft.Identity.Client;.
Instead of instantiating AuthenticationContext, use ConfidentialClientApplicationBuilder.Create to instantiate IConfidentialClientApplication.
Instead of the resourceId string, MSAL.NET uses scopes. Because applications that use ADAL.NET are preauthorized, you can always use the following scopes: new string[] { $"{resourceId}/.default" }.
Replace the call to AuthenticationContext.AcquireTokenAsync with a call to IConfidentialClientApplication.AcquireTokenXXX, where XXX depends on your scenario.
In this case, we replace the call to AuthenticationContext.AcquireTokenAsync with a call to IConfidentialClientApplication.AcquireTokenOnBehalfOf.
Here's a comparison of sample OBO code for ADAL.NET and MSAL.NET:
ADAL
MSAL
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public partial class AuthWrapper
{
const string ClientId = "Guid (AppID)";
const string authority
= "https://login.microsoftonline.com/common";
X509Certificate2 certificate = LoadCertificate();
public async Task<AuthenticationResult> GetAuthenticationResult(
string resourceId,
string tokenUsedToCallTheWebApi)
{
var authContext = new AuthenticationContext(authority);
var clientAssertionCert = new ClientAssertionCertificate(
ClientId,
certificate);
var userAssertion = new UserAssertion(tokenUsedToCallTheWebApi);
var authResult = await authContext.AcquireTokenAsync(
resourceId,
clientAssertionCert,
userAssertion,
);
return authResult;
}
}
using Microsoft.Identity.Client;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public partial class AuthWrapper
{
const string ClientId = "Guid (Application ID)";
const string authority
= "https://login.microsoftonline.com/common";
X509Certificate2 certificate = LoadCertificate();
IConfidentialClientApplication app;
public async Task<AuthenticationResult> GetAuthenticationResult(
string resourceId,
string tokenUsedToCallTheWebApi)
{
var app = ConfidentialClientApplicationBuilder.Create(ClientId)
.WithCertificate(certificate)
.WithAuthority(authority)
.Build();
// Setup token caching https://learn.microsoft.com/azure/active-directory/develop/msal-net-token-cache-serialization?tabs=aspnet
// For example, for an in-memory cache with 1GB limit. For OBO, it is recommended to use a distributed cache like Redis.
app.AddInMemoryTokenCache(services =>
{
// Configure the memory cache options
services.Configure<MemoryCacheOptions>(options =>
{
options.SizeLimit = 1024 * 1024 * 1024; // in bytes (1 GB of memory)
});
}
var userAssertion = new UserAssertion(tokenUsedToCallTheWebApi);
var authResult = await app.AcquireTokenOnBehalfOf(
new string[] { $"{resourceId}/.default" },
userAssertion)
// .WithTenantId(specificTenant)
// See https://aka.ms/msal.net/withTenantId
.ExecuteAsync()
.ConfigureAwait(false);
return authResult;
}
}
Web apps that sign in users and call web APIs on behalf of users employ the OAuth2.0 authorization code flow. Typically:
The app signs in a user by executing a first leg of the authorization code flow by going to the Microsoft identity platform authorize endpoint. The user signs in and performs multi-factor authentications if needed. As an outcome of this operation, the app receives the authorization code. The authentication library isn't used at this stage.
The app executes the second leg of the authorization code flow. It uses the authorization code to get an access token, an ID token, and a refresh token. Your application needs to provide the redirectUri value, which is the URI where the Microsoft identity platform endpoint will provide the security tokens. After the app receives that URI, it typically calls AcquireTokenByAuthorizationCode for ADAL or MSAL to redeem the code and to get a token that will be stored in the token cache.
The app uses ADAL or MSAL to call AcquireTokenSilent to get tokens for calling the necessary web APIs from the web app controllers.
Find out if your code uses the auth code flow
The ADAL code for your app uses auth code flow if it contains a call to AuthenticationContext.AcquireTokenByAuthorizationCodeAsync.
Update the code by using the authorization code flow
The following steps for updating code apply across all the confidential client scenarios:
Add the MSAL.NET namespace in your source code: using Microsoft.Identity.Client;.
Instead of instantiating AuthenticationContext, use ConfidentialClientApplicationBuilder.Create to instantiate IConfidentialClientApplication.
Instead of the resourceId string, MSAL.NET uses scopes. Because applications that use ADAL.NET are preauthorized, you can always use the following scopes: new string[] { $"{resourceId}/.default" }.
Replace the call to AuthenticationContext.AcquireTokenAsync with a call to IConfidentialClientApplication.AcquireTokenXXX, where XXX depends on your scenario.
In this case, replace the call to AuthenticationContext.AcquireTokenAsync with a call to IConfidentialClientApplication.AcquireTokenByAuthorizationCode.
Here's a comparison of sample authorization code flows for ADAL.NET and MSAL.NET:
ADAL
MSAL
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public partial class AuthWrapper
{
const string ClientId = "Guid (AppID)";
const string authority
= "https://login.microsoftonline.com/common";
private Uri redirectUri = new Uri("host/login_oidc");
X509Certificate2 certificate = LoadCertificate();
public async Task<AuthenticationResult> GetAuthenticationResult(
string resourceId,
string authorizationCode)
{
var ac = new AuthenticationContext(authority);
var clientAssertionCert = new ClientAssertionCertificate(
ClientId,
certificate);
var authResult = await ac.AcquireTokenByAuthorizationCodeAsync(
authorizationCode,
redirectUri,
clientAssertionCert,
resourceId,
);
return authResult;
}
}
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using System;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public partial class AuthWrapper
{
const string ClientId = "Guid (Application ID)";
const string authority
= "https://login.microsoftonline.com/{tenant}";
private Uri redirectUri = new Uri("host/login_oidc");
X509Certificate2 certificate = LoadCertificate();
public IConfidentialClientApplication CreateApplication()
{
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(ClientId)
.WithCertificate(certificate)
.WithAuthority(authority)
.WithRedirectUri(redirectUri.ToString())
.WithLegacyCacheCompatibility(false)
.Build();
// Add a token cache. For details about other serialization
// see https://aka.ms/msal-net-cca-token-cache-serialization
app.AddInMemoryTokenCache();
return app;
}
// Called from 'code received event'.
public async Task<AuthenticationResult> GetAuthenticationResult(
string resourceId,
string authorizationCode)
{
IConfidentialClientApplication app = CreateApplication();
var authResult = await app.AcquireTokenByAuthorizationCode(
new[] { $"{resourceId}/.default" },
authorizationCode)
.ExecuteAsync()
.ConfigureAwait(false);
return authResult;
}
}
Calling AcquireTokenByAuthorizationCode adds a token to the token cache when the authorization code is received. To acquire extra tokens for other resources or tenants, use AcquireTokenSilent in your controllers.
public partial class AuthWrapper
{
// Called from controllers
public async Task<AuthenticationResult> GetAuthenticationResult(
string resourceId2,
string authority)
{
IConfidentialClientApplication app = CreateApplication();
AuthenticationResult authResult;
var scopes = new[] { $"{resourceId2}/.default" };
var account = await app.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId());
try
{
// try to get an already cached token
authResult = await app.AcquireTokenSilent(
scopes,
account)
// .WithTenantId(specificTenantId)
// See https://aka.ms/msal.net/withTenantId
.ExecuteAsync().ConfigureAwait(false);
}
catch (MsalUiRequiredException)
{
// The controller will need to challenge the user
// including asking for claims={ex.Claims}
throw;
}
return authResult;
}
}
Benefit from token caching
Because your web app uses AcquireTokenByAuthorizationCode, it needs to use a distributed token cache for token caching. For details, see Token cache for a web app or web API.
app.UseInMemoryTokenCaches(); // or a distributed token cache.
Handling MsalUiRequiredException
When your controller attempts to acquire a token silently for different
scopes/resources, MSAL.NET might throw an MsalUiRequiredException as expected if the user needs to re-sign-in, or if the
access to the resource requires more claims (because of a Conditional Access
policy). For details on mitigation see how to Handle errors and exceptions in MSAL.NET.
Resilience. MSAL.NET helps make your app resilient through:
Microsoft Entra ID Cached Credential Service (CCS) benefits. CCS operates as a Microsoft Entra backup.
Proactive renewal of tokens if the API that you call enables long-lived tokens through continuous access evaluation.
Security. You can acquire Proof of Possession (PoP) tokens if the web API that you want to call requires it. For details, see Proof Of Possession tokens in MSAL.NET
Performance and scalability. If you don't need to share your cache with ADAL.NET, disable the legacy cache compatibility when you're creating the confidential client application (.WithLegacyCacheCompatibility(false)) to significantly increase performance.
The following troubleshooting information makes two assumptions:
Your ADAL.NET code was working.
You migrated to MSAL by keeping the same client ID.
If you get an exception with either of the following messages:
AADSTS700027: Client assertion contains an invalid signature. [Reason - The key was not found.]
AADSTS90002: Tenant 'aaaabbbb-0000-cccc-1111-dddd2222eeee' not found. This may happen if there are no activesubscriptions for the tenant. Check to make sure you have the correct tenant ID. Check with your subscriptionadministrator.
Troubleshoot the exception using these steps:
Confirm that you're using the latest version of MSAL.NET.
Confirm that the authority host that you set when building the confidential client app and the authority host that you used with ADAL are similar. In particular, is it the same cloud (Azure Government, Microsoft Azure operated by 21Vianet, or Azure Germany)?
MsalClientException
In multi-tenant apps, specify a common authority when building the app to target a specific tenant such as, the tenant of the user when calling a web API. Since MSAL.NET 4.37.0, when you specify .WithAzureRegion at the app creation, you can no longer specify the Authority using .WithAuthority during the token requests. If you do, you'll get the following error when updating from previous versions of MSAL.NET:
MsalClientException - "You configured WithAuthority at the request level, and also WithAzureRegion. This is not supported when the environment changes from application to request. Use WithTenantId at the request level instead."
To remediate this issue, replace .WithAuthority on the AcquireTokenXXX expression by .WithTenantId. Specify the tenant using either a GUID or a domain name.