Update Nov 2020
This answer was originally written for V2 of the MSAL client. Since then a V3 has been released which may work differently from V2.
Original answer
I'm not clear as to whether it will always check the expiration of the current AccessToken and automatically refresh it (using the RefreshToken) on method call.
A refresh token is automatically supplied when the offline_access
scope is provided, if I understand this answer correctly
...you've requested the offline_access
scope so your app receives a Refresh Token.
The description of AcquireTokenSilentAsync
implies that when an refresh token is provided, it will check the expiration date on the token, and get a new one if it's expired or close to expiring.
If access token is expired or close to expiration (within 5 minute
window), then refresh token (if available) is used to acquire a new
access token by making a network call.
It will repeat this behavior until the refresh token is expired. Optionally you can force a refresh of the access token via the refresh token by utilizing the forceRefresh
parameter on AcquireTokenSilentAsync

Lastly, I am going to quote this answer on SO since it gives a nice insight about MSAL and tokens
Just to make a small clarification, MSAL doesn't actually issue tokens
or decide a token expiration, but rather ingests an acquires token
from the Azure AD STS.
MSAL will automatically refresh your access token after expiration
when calling AcquireTokenSilentAsync. .... The default token
expirations right now are:
Access Tokens: 1 hour
Refresh Tokens: 90 days, 14 day inactive sliding window
(June 13th '17)
Regardless, is there a "best practice" for how often we should call
AcquireTokenSilentAsync() ? Should we keep track of the expiration
ourselves and call this method to update our bearer authentication
header? Should we be calling AcquireTokenSilentAsync() on every
Request?
The documentation also lists a 'Recommended call pattern' for calling the AcquireTokenSilentAsync
. The documentation also mentions that
For both Public client and confidential client applications, MSAL.NET maintains a token cache (or two caches in the case of confidential client applications), and applications should try to get a token from the cache first before any other means.
Based on examples I've seen, including the recommended call pattern from the documentation, I would argue you could simply call AcquireTokenSilentAsync
and catch the MsalUiRequiredException
as an indication that the token has expired and the user has to log in again.
I can't see how the GraphServiceClient (tangent topic, I know) using the DelegateAuthenticationProvider will do anything helpful WRT refreshing. Do we need to extend that class and perform our own refresh when the token is nearing expiration? I feel like this would/should be already in the SDK.
If I understand the DelegateAuthenticationProvider correctly, what it does is modify the requestMessage before we pass it to Graph. All we got to do is provide our access token with an authorization header for the request. We already know that when we fetch our access token, it is valid, so we can just add it.
new DelegateAuthenticationProvider(async (requestMessage) =>
{
ConfidentialClientApplication cca = new ConfidentialClientApplication(_ClientId, _Authority, _RedirectUri, new ClientCredential(_ClientSecret), _UserTokenSessionCache.GetTokenCache(identifier, httpContext), _ApplicationTokenCache.GetTokenCache());
AuthenticationResult result = await cca.AcquireTokenSilentAsync();
requestMessage.Headers.Add("Authorization", result.CreateAuthorizationHeader());
//OR
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
});
(There is no difference between either way of setting the headers)
I've been down this path and this does the trick for me. I highly advise reading their documentation, because it does gives a good insight in how to implement MSAL.Net.
I haven't had time yet to play around with the token durations yet. Nor the behavior if no refresh token is provided (if that's even possible)
I hope this helps!