-
Notifications
You must be signed in to change notification settings - Fork 373
on behalf of
If you are building a web API on to of ASP.NET Core, we recommend that you use Microsoft.Identity.Web. See Web APIs with Microsoft.Identity.Web.
You might want to check the decision tree: Is MSAL.NET right for me?.
- A client (web, desktop, mobile, single-page application) - not represented on the picture below - calls a protected web API, providing a JWT bearer token in its "Authorization" HTTP header.
- The protected web API validates the incoming user token, and uses MSAL.NET
AcquireTokenOnBehalfOf
method to request from Azure AD another token so that it can, itself, call another web API (named the downstream web API) on behalf of the user.
This flow, named the On-Behalf-Of flow (OBO), is illustrated by the top part of the picture below. The bottom part is a daemon scenario, also possible for web APIs.
The OBO call is done by calling the AcquireTokenOnBehalf method on the IConfidentialClientApplication
interface.
This call looks in the cache by itself - so you do not need to call AcquireTokenSilent
and does not store refresh tokens.
For scenarios where continuous access is needed without an assertion, see OBO for long lived processes
Note: Make sure to pass an access token, not an ID token, into the
AcquireTokenOnBehalfOf
method. The purpose of an ID token is as a confirmation that a user was authenticated and it contains some user-related information. While an access token determines whether a user has access to a resource, which is more appropriate in this On-Behalf-Of scenario. MSAL is focused on getting good access tokens. ID tokens are also obtained and cached but their expiry is not tracked. So it's possible for an ID token to expire andAcquireTokenSilent
will not refresh it.
private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityToken jwtToken, ClaimsPrincipal principal, HttpContext httpContext)
{
UserAssertion userAssertion;
IEnumerable<string> requestedScopes;
if (jwtToken != null)
{
userAssertion = new UserAssertion(jwtToken.RawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
requestedScopes = scopes ?? jwtToken.Audiences.Select(a => $"{a}/.default");
}
else
{
throw new ArgumentOutOfRangeException("tokenValidationContext.SecurityToken should be a JWT Token");
}
// Create the application
var application = BuildConfidentialClientApplication(httpContext, principal);
// .Result to make sure that the cache is filled-in before the controller tries to get access tokens
var result = await application.AcquireTokenOnBehalfOf(
requestedScopes.Except(scopesRequestedByMsalNet),
userAssertion)
.ExecuteAsync()
}
One OBO scenario is when a web API runs long running processes on behalf of the user (for example, OneDrive which creates albums for you). This can be implemented as such:
- Before you start a long running process, call:
string sessionKey = // custom key or null
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.InitiateLongRunningProcessInWebApi(
scopes,
userAccessToken,
ref sessionKey)
.ExecuteAsync();
userAccessToken
is a user access token used to call this web API. sessionKey
will be used as a key when caching and retrieving the OBO token. If set to null
, MSAL will set it to the assertion hash of the passed-in user token. It can also be set by the developer to something that identifies a specific user session, like the optional sid
claim from the user token (for more information, see Provide optional claims to your app). InitiateLongRunningProcessInWebApi
doesn't check the cache; it will use the user token to acquire a new OBO token from AAD, which will then be cached and returned.
- In the long-running process, whenever OBO token is needed, call:
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.AcquireTokenInLongRunningProcess(
scopes,
sessionKey)
.ExecuteAsync();
Pass the sessionKey
which is associated with the current user's session and will be used to retrieve the related OBO token. If the token is expired, MSAL will use the cached refresh token to acquire a new OBO access token from AAD and cache it. If no token is found with this sessionKey
, MSAL will throw a MsalClientException
. Make sure to call InitiateLongRunningProcessInWebApi
first.
It is strongly recommended to use a distributed persisted cache in a web API scenario. Since these APIs store the refresh token, MSAL will not suggest an expiration, as refresh tokens have a long lifetime and can be used over and over again.
It is recommended that you set L1 and L2 eviction policies manually, for example a max size for the L1 cache and a sliding expiration for the L2.
In a case when AcquireTokenInLongRunningProcess
throws an exception when it cannot find a token and the L2 cache has a cache entry for the same cache key, verify that the L2 cache read operation completed successfully. AcquireTokenInLongRunningProcess
is different from the InitiateLongRunningProcessInWebApi
and AcquireTokenOnBehalfOf
, in that it is if the cache read fails, this method is unable to acquire a new token from AAD because it does not have an original user assertion. If using Microsoft.Identity.Web.TokenCache to enable distributed cache, set OnL2CacheFailure event to retry the L2 call and/or add extra logs, which can be enabled like this.
Starting with MSAL 4.51.0, to remove cached tokens call StopLongRunningProcessInWebApiAsync
passing in a cache key. With earlier MSAL versions, it is recommended to use L2 cache eviction policies. If immediate removal is needed, delete the L2 cache node associated with the sessionKey
.
-
Web APIs expose scopes. For more information, see Quickstart: Configure an application to expose web APIs (Preview).
-
Web APIs decide which version of token they want to accept. For your own web API, you can change the property in the manifest named
accessTokenAcceptedVersion
(to 1 or 2). For more information, see Azure Active Directory app manifest.
If you are building a web API on to of ASP.NET Core, we recommend that you use Microsoft.Identity.Web. See Web APIs with Microsoft.Identity.Web.
In an ASP.NET / ASP.NET Core Web API, OBO is typically called on the OnTokenValidated
event of the JwtBearerOptions
. The token is then not used immediately, but this call has the effect of populating the user token cache. Later, the controllers will call AcquireTokenSilent
, which will have the effect of hitting the cache, refreshing the access token if needed, or getting a new one for a new resource, but for still for the same user.
Here is what happens when a Jwt bearer token is received end validated by the Web API:
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes)
{
...
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Events.OnTokenValidated = async context =>
{
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
context.Success();
// Adds the token to the cache, and also handles the incremental consent and claim challenges
tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
await Task.FromResult(0);
};
});
return services;
}
And here is the code in the actions of the API controllers, calling downstream APIs:
private async Task GetTodoList(bool isAppStarting)
{
...
//
// Get an access token to call the To Do service.
//
AuthenticationResult result = null;
try
{
result = await _app.AcquireTokenSilent(Scopes, accounts.FirstOrDefault())
.ExecuteAsync()
.ConfigureAwait(false);
}
...
// Once the token has been returned by MSAL, add it to the http authorization header, before making the call to access the To Do list service.
// Make sure to use an access token and not an ID token
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
// Call the To Do list service.
HttpResponseMessage response = await _httpClient.GetAsync(TodoListBaseAddress + "/api/todolist");
...
}
the GetAccountIdentifier method uses the claims associated with the identity of the user for which the Web API received the JWT:
public static string GetMsalAccountId(this ClaimsPrincipal claimsPrincipal)
{
string userObjectId = GetObjectId(claimsPrincipal);
string tenantId = GetTenantId(claimsPrincipal);
if (!string.IsNullOrWhiteSpace(userObjectId) && !string.IsNullOrWhiteSpace(tenantId))
{
return $"{userObjectId}.{tenantId}";
}
return null;
}
For more information about the On-Behalf-Of protocol, see Azure Active Directory v2.0 and OAuth 2.0 On-Behalf-Of flow.
Sample | Platform | Description |
---|---|---|
active-directory-aspnetcore-webapi-tutorial-v2 | ASP.NET Core 2.2 Web API, Desktop (WPF) | ASP.NET Core 2.1 Web API calling Microsoft Graph, itself called from a WPF application using Azure AD V2 ![]() |
Vanity URL: https://aka.ms/msal-net-on-behalf-of
- Home
- Why use MSAL.NET
- Is MSAL.NET right for me
- Scenarios
- Register your app with AAD
- Client applications
- Acquiring tokens
- MSAL samples
- Known Issues
- Acquiring a token for the app
- Acquiring a token on behalf of a user in Web APIs
- Acquiring a token by authorization code in Web Apps
- AcquireTokenInteractive
- WAM - the Windows broker
- .NET Core
- Maui Docs
- Custom Browser
- Applying an AAD B2C policy
- Integrated Windows Authentication for domain or AAD joined machines
- Username / Password
- Device Code Flow for devices without a Web browser
- ADFS support
- High Availability
- Regional
- Token cache serialization
- Logging
- Exceptions in MSAL
- Provide your own Httpclient and proxy
- Extensibility Points
- Clearing the cache
- Client Credentials Multi-Tenant guidance
- Performance perspectives
- Differences between ADAL.NET and MSAL.NET Apps
- PowerShell support
- Testing apps that use MSAL
- Experimental Features
- Proof of Possession (PoP) tokens
- Using in Azure functions
- Extract info from WWW-Authenticate headers
- SPA Authorization Code