Should TokenCache be used for offline acces with MSAL? - c#

I am implementing authentication using MSAL and I need some guidance for handling refresh tokens.
My Angular Web App is authenticating with my ASP.NET Web API using MSAL. Web API requires some scopes for accessing Microsoft Graph, so it uses "On Behalf Of" OAuth 2.0 flow to get an access token for calling MS Graph. This part is done and works.
The problem is that MS Graph will be called after some time by my .NET daemon app (using OBO flow) when access token will expire.
What I need is to get refresh token by my Web API and cache it (e.g. in SQL database) so it can be read by daemon app and used to obtain a valid access token.
I suppose that the TokenCache for the confidential client application is the right way to do this but I'm not sure how to get a valid access token by daemon app.
Here is the code of my daemon app I want to use to get access token from AAD:
var userAssertion = new UserAssertion(
<accessToken>,
"urn:ietf:params:oauth:grant-type:jwt-bearer");
var authority = authEndpoint.TrimEnd('/') + "/" + <tenant> + "/";
var clientCredencial = new ClientCredential(<clientSecret>);
var authClient = new ConfidentialClientApplication(<clientId>, authority, <redirectUri>,
clientCredencial, <userTokenCache>, null);
try
{
var authResult =
await authClient.AcquireTokenOnBehalfOfAsync(<scopes>, userAssertion, authority);
activeAccessToken = authResult.AccessToken;
}
catch (MsalException ex)
{
throw;
}
Should I provide <userTokenCache> to get the refresh token form cache? If yes, UserAssertion requires an <accessToken> to be provided, but I don't know what value should be used.
Or should I make a token request on my own and get the refresh token from the response since it is not supported by MSAL? Then I could store the refresh token in the database and use it as <accessToken> with null as <userTokenCache> in daemon app.
I thought it is possible to get the refresh token using MSAL, but I found it is not.
Update
I forgot to say that all of my apps use the same Application ID (this is due to the limitations of the AADv2 endpoint, although I just found that it was removed from the docs at Nov 2nd 2018).
Why not client credentials flow?
Communication with MS Graph could be performed in Web API (using OBO flow) but the task may be delayed by the user, e.g. send mail after 8 hours (Web API will store tasks in the database). The solution for this case is an app (daemon) that runs on schedule, gets tasks from the database and performs calls to MS Graph. I prefer not to give admin consent to any of my apps because it is very important to get consent from the user. If the consent is revoked, call to MS Graph should not be performed. That is why the daemon app should use the refresh token to get access token from AAD for accessing MS Graph (using OBO flow).
I hope it is clear now. Perhaps I should not do it this way. Any suggestion would be appreciated.

MSAL does handle the refresh token itself, you just need to handle the cache serialization. - the userTokenCache is used by the OBO call, and you use the refresh token by calling AcquireTokenSilentAsycn first (that's what refreshes tokens)
- the applicationTokenCache is used by the client credentials flow (AcquireTokenForApplication).
I'd advise you to have a look at the following sample which illustrates OBO: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2, in particular TodoListService/Extensions/TokenAcquisition.cs#L275-L294
the code is :
var accounts = await application.GetAccountsAsync();
try
{
AuthenticationResult result = null;
var allAccounts = await application.GetAccountsAsync();
IAccount account = await application.GetAccountAsync(accountIdentifier);
result = await application.AcquireTokenSilentAsync(scopes.Except(scopesRequestedByMsalNet), account);
return result.AccessToken;
}
catch (MsalUiRequiredException ex)
{
...
Now the cache is itself initialized from the bearer token that is sent by your client to your Web API. See
TodoListService/Extensions/TokenAcquisition.cs#L305-L336
private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityToken jwtToken, AuthenticationProperties properties, ClaimsPrincipal principal, HttpContext httpContext)
{
try
{
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");
}
var application = CreateApplication(httpContext, principal, properties, null);
// Synchronous call to make sure that the cache is filled-in before the controller tries to get access tokens
AuthenticationResult result = application.AcquireTokenOnBehalfOfAsync(scopes.Except(scopesRequestedByMsalNet), userAssertion).GetAwaiter().GetResult();
}
catch (MsalUiRequiredException ex)
{
...

Related

MSAL - PublicClientApplication - GetAccountsAsync() doesn't return any Accounts

I'm developing a little WPF-App that is supposed to query some data from the MS Graph API. I want to use SSO, so the user doesn't have to login to the app seperatly.
The app is run on a Azure AD joined device. The user is an AADC synchronized AD user. The AAD tenant is federated with ADFS. The user authenticates with Hello for Business (PIN) or via Password. The resulting problem is the same.
I can confirm that the user got a PRT via:
dsregcmd /status
AzureAdPrt: YES
In case it matters: The app registration in Azure AD is set to "Treat application as public client". And the following redirect URIs are configured:
https://login.microsoftonline.com/common/oauth2/nativeclient
https://login.live.com/oauth20_desktop.srf
msalxxxxxxx(appId)://auth
urn:ietf:wg:oauth:2.0:oob
Based on the examples I found, I'm using the following code to try to get an access token. However the GetAccountsAsync() method doesn't return any users nor does it throw any error or exception.
Can anyone tell me, what I'm missing here?
Any help would be much appreciated!
PS: When I try this using "Interactive Authentication" it works fine.
public GraphAuthProvider(string appId, string tenantId, string[] scopes)
{
_scopes = scopes;
try
{
_msalClient = PublicClientApplicationBuilder
.Create(appId)
.WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
.WithTenantId(tenantId)
.Build();
}
catch (Exception exception)
{
_log.Error(exception.Message);
_log.Error(exception.StackTrace);
throw;
}
}
public async Task<string> GetAccessToken()
{
_log.Info("Starting 'GetAccessToken'...");
var accounts = await _msalClient.GetAccountsAsync();
_userAccount = accounts.FirstOrDefault();
// If there is no saved user account, the user must sign-in
if (_userAccount == null)
{
_log.Info("No cached accounts found. Trying integrated authentication...");
[...]
}
else
{
// If there is an account, call AcquireTokenSilent
// By doing this, MSAL will refresh the token automatically if
// it is expired. Otherwise it returns the cached token.
var userAccountJson = await Task.Factory.StartNew(() => JsonConvert.SerializeObject(_userAccount));
_log.Info($"Found cached accounts. _userAccount is: {userAccountJson}");
var result = await _msalClient
.AcquireTokenSilent(_scopes, _userAccount)
.ExecuteAsync();
return result.AccessToken;
}
}
To be able to have IAccounts returned from MSAL (which access the cache), it must have the cache bootstrapped at some point. You are missing the starting point, which in your case is AcquireTokenInteractive.
It is recommended to use the following try/catch pattern on MSAL:
try
{
var accounts = await _msalClient.GetAccountsAsync();
// Try to acquire an access token from the cache. If an interaction is required, MsalUiRequiredException will be thrown.
result = await _msalClient.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Acquiring an access token interactively. MSAL will cache it so you can use AcquireTokenSilent on future calls.
result = await _msalClient.AcquireTokenInteractive(scopes)
.ExecuteAsync();
}
Use this try/catch pattern instead of you if/else logic and you will be good to go.
For further reference, there is this msal desktop samples which covers a bunch of common scenarios.
Update
If you are instantiating a new _msalClient on every action, then this explains why the other calls are not working. You can either have _msalClient as a static/singleton instance or implement a serialized token cache. Here is a cache example
As there are some questions regarding non-interactive authentication in the comments, this is how I finally got this working:
Use WAM and configure the builder like this:
var builder = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority($"{Instance}{TenantId}")
.WithDefaultRedirectUri()
.WithWindowsBroker(true);
Configure this redirect URI in the Azure App registration:
ms-appx-web://microsoft.aad.brokerplugin/{client_id}
A code example is available here
In-case anyone has a similar problem, I had an issue where both GetAccountAsync and GetAccountsAsync (the latter being now deprecated), were both sometimes returning null. All I needed to do was make sure all my authentication adjacent libraries were up to date.
I think there was an issue where the in-memory token caching wasn't always working as intended, which seems to be fixed by a simple update.

How to get the oauth refresh token?

I have a web app which utlizes Oauth and is unable to reuse the authToken due to the error below.
{"AADSTS70002: Error validating credentials. AADSTS54005: OAuth2 Authorization
code was already redeemed, please retry with a new valid code or use an
existing refresh token.\r\nTrace ID: 30c342a7-f16a-4a05-a4a8-
c7ee2c722300\r\nCorrelation ID: 3a5c99d1-ca1c-4cd7-bd36-
cce721bf05b6\r\nTimestamp: 2018-11-21 00:26:18Z"}
I'm told this is a know issue/update here and here.
...okay, fine so now I'm trying to get the refresh token so I can regenerate my access token but I'm having trouble getting something to work.
I have tried the ones below:
https://learn.microsoft.com/en-us/bingads/shopping-content/code-example-authentication-oauth - this one does not seem to work and throws an exception when I try to get the accesstoken or refresh token. stating that one or more errors have occured.
https://auth0.com/docs/api/authentication#authorization-code-pkce- - but does not return the refresh token. Could this be because I don't have the code_verifier? If so, how would I get that?
Authorization Code (PKCE) Image
Below is a code sample which I am using - problem here is that I can only use this once and once It has been redemed I cannot retrive it silently as it no longer exists in the cache.
ClientCredential clientcred = new ClientCredential(Constants.ClientId, Constants.AppKey);
TokenCache TC = new TokenCache();
AuthenticationContext AC = new AuthenticationContext(Constants.Authority, TC);
//Set token from authentication result
AuthenticationResult authenticationResult = await AC.AcquireTokenByAuthorizationCodeAsync(
Constants.Code,
new Uri(Constants.postLogoutRedirectUri + "Index"), clientcred);
return authenticationResult.AccessToken;
You need to call OAuth2 authorize endpoint with offline_access scope to get refresh token.
You should call AcquireTokenByAuthorizationCodeAsync only once when you receive authorization code and should not use the result. azure ad sample
You need to call AcquireTokenSilently when you want to get access token. azure ad sample
This azure ad sample use a TokenCache implementation by user id.
Authorize request
Token request
Good luck!

How to acquire graph token without using sessions?

I have an application that is utilizing Azure AD authentication. I also need to access the Microsoft Graph API for user data. Every example I have found that makes requests to the Graph API is utilizing a cached session token, but since I am using JWT obviously I have no need for storing session state. How can I get a JWT with the proper audience using a JWT with my app as the audience?
For example, here is a request to retrieve a token from the Microsoft Graph AspNetCore Sample:
_userTokenCache = new SessionTokenCache(userId, _memoryCache).GetCacheInstance();
var cca = new ConfidentialClientApplication(
_appId,
_redirectUri,
_credential,
_userTokenCache,
null);
var result = await cca.AcquireTokenSilentAsync(_scopes, cca.Users.First());
return result.AccessToken;
Which utilizes the memory cache to pull the token from a Challenge() redirect sign-in with OpenId Connect cookie. However, since I am using JWT, I already have a bearer token, but with the wrong authority. What do I need to do to acquire a new token that I can use to access the Graph API? I still want the tokens to be authorized for my application id, so I would want a new token that allows me to access the API with server-side rest requests.
Edit: Incorrectly tagged as Azure AD Graph, retagged to Microsoft Graph.
Edit Edit: To clarify, each of the samples I've seen so far is using Session cookies as so:
services.AddAuthentication(sharedOptions => {
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
However, I am using JWT so I don't have a token cached:
app.UseJwtBearerAuthentication(new JwtBearerOptions {
Authority = $"{instance}{tenant}",
Audience = audience,
SaveToken = true
});
The JWT that I get from requests to login.microsoftonline.com have my application as the audience, whereas the JWT generated by these samples have https://graph.microsoft.com as the audience. So I need to get (I presume at least) a token for this audience using only the token I got from my standard authentication request.
Don't confuse how you manage your token (i.e. token cache) with the tokens themselves. The reason you cache a token is simply so you can request a refreshed token as needed (refresh_token). The refresh token is only provided for certain sceanios (i.e. when using the authorization_code flow and you've requested the offline_access scope).
If you're using a flow without a refresh token (i.e implicit or client_credentials) then you may not need to cache your token. You generally should still cache them since there is an overhead cost to fetching a token from AAD and caching allows you to only retrieve a new token when the existing one expires.
Using DelegateAuthenticationProvider with an existing Token
All that said, it sounds like you've already got a token in hand. Since the entire point of MSAL (which is where ConfidentialClientApplication comes from) it to retrieve and manage tokens for you, I'm not exactly sure why you'd want to do this. I would simply skip MSAL entirely and just use your existing token.
If you're using the Microsoft Graph .NET Client Library you can drop MSAL entirely and simply use your existing token (access_token) via the DelegateAuthenticationProvider:
var graphServiceClient = new GraphServiceClient(
new DelegateAuthenticationProvider((requestMessage) => {
requestMessage.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", token.access_token);
return Task.FromResult(0);
})
);
As for the "proper audience", I'm not sure I understand the context. Your token will need to include scopes for Microsoft Graph but how you define them depends a bit on how you are getting your token.
v1 Endpoint
If you're using the older Azure AD OAUTH endpoint (aka the v1 Endpoint) then you need to configure your Application permissions via the Azure Portal. In order to switch between different APIs (called "Resources") you need to request offline_access and user the refresh_token. Switching involves requesting a refreshed token while passing in a new resource. The resulting token will then work with that resource.
For example, if my default resource is a SharePoint Online instance (https://tenant.sharepoint.com) then I would normally refresh my token with something like this:
private async Task<string> RequestTokenAsync() {
var data = new Dictionary<string, string>();
data.Add("grant_type", "refresh_token");
data.Add("client_id", _clientId);
data.Add("client_secret", _clientSecret);
data.Add("resource", "https://tenant.sharepoint.com");
data.Add("redirect_uri", RedirectUri);
data.Add("refresh_token ", refresh_token);
HttpClient httpClient = new HttpClient();
var response = await httpClient.PostAsync(_tokenUri, new FormUrlEncodedContent(data));
response.EnsureSuccessStatusCode();
var result = await result.Content.ReadAsStringAsync();
}
Now if I want to make a call to Microsoft Graph I will first need to get a token for the https://graph.microsoft.com resource:
private async Task<string> RequestTokenAsync() {
var data = new Dictionary<string, string>();
data.Add("grant_type", "refresh_token");
data.Add("client_id", _clientId);
data.Add("client_secret", _clientSecret);
data.Add("resource", "https://graph.microsoft.com");
data.Add("redirect_uri", RedirectUri);
data.Add("refresh_token ", refresh_token);
HttpClient httpClient = new HttpClient();
var response = await httpClient.PostAsync(_tokenUri, new FormUrlEncodedContent(data));
response.EnsureSuccessStatusCode();
var result = await result.Content.ReadAsStringAsync();
}
Now I have two tokens, one for SharePoint and one for Microsoft Graph. I can switch between resources by simply refreshing the token for the proper resource. I do have to make sure I refresh properly however since if my refresh_token expires before I can replace it, I've lost my credentials entirely.
If this sounds complicated, it is. Generally you need to build some mechanisms to manage which tokens are live, which tokens need to be replaced, etc. This is what that token cache is all about since MSAL/ADAL handle this for you.
v2 Endpoint
The newer v2 Endpoint is far easier to work with. Rather than resources it uses scopes. These scopes include the resource identifier and can be dynamically assigned as needed.
So while in v1 we might assign user.read from Microsoft Graph and user.read from Outlook Rest API, we can now assign both at once in a single token by requesting https://graph.microsoft.com/user.read and https://outlook.office.com/user.read at the same time. This means we get a single token that can be used with either API without getting into the "refresh to switch resource" business from above.
The downside of v2 is that only a limited number of APIs support it at the moment. If you need to work across a number of APIs, you may still be better off using v1 for this reason.
Hope this helps a little.

Azure - AD - AcquireTokenSilent giving error failed_to_acquire_token_silently

We are using Azure AD to authenticate and get the refreshed access token every 30 mins. We invoke below method which acquires security token and add it to request header.
var userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId));
var credential = new ClientCredential(ConfigurationManager.AppSettings["ida:ClientId"],
ConfigurationManager.AppSettings["ida:ClientSecret"]);
try
{
var authenticationResult = authContext.AcquireTokenSilent(ConfigurationManager.AppSettings["WebAPIBaseAddress"], credential, new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
//set cookie for azure oauth refresh token - on successful login
var httpCookie = HttpContext.Current.Response.Cookies["RefreshToken"];
if (httpCookie != null)
httpCookie.Value = authenticationResult.RefreshToken;
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
}
catch
{
//Get access token using Refresh Token
var authenticationResult = authContext.AcquireTokenByRefreshToken(httpCookie.Value, credential, ConfigurationManager.AppSettings["WebAPIBaseAddress"]);
}
In above method, we have used AcquireTokenSilent method which gives us access token. Since access token lasts only for certain period of time.
After its expiry, we call AcquireTokenByRefreshToken to get refresh token.
The above code works well, however we are getting below exception randomly:
Microsoft.IdentityModel.Clients.ActiveDirectory.AdalSilentTokenAcquisitionException: Failed to acquire token silently. Call method AcquireToken
at Microsoft.IdentityModel.Clients.ActiveDirectory.AcquireTokenSilentHandler.SendTokenRequestAsync()
at Microsoft.IdentityModel.Clients.ActiveDirectory.AcquireTokenHandlerBase.<RunAsync>d__0.MoveNext()
ErrorCode: failed_to_acquire_token_silently
What could be the reason of such inconsistent behaviour? The same code is working on few environments (Stage/Dev) but its throwing error randomly on Production.
Please suggest.
We were able to resolve this. It seems to be a small mistake in the code itself.
When the AccessToken expires, it throws an exception and it tries to fetch a new one using AcquireTokenByRefreshToken in the catch block. Here we were not setting the newly received refresh token back in the Cookie.
We need to add below statement in the catch block also, so that it would get the Refresh token, which can then be passed back to generate a new Access Token.
httpCookie.Value = authenticationResult.RefreshToken;
First of all, before using AcquireTokenSilent you must invoke AcquireTokenByAuthorizationCodeAsync.
var context = new AuthenticationContext(authorityUri);
var credential = new ClientCredential(clientId, clientSecretKey);
await context.AcquireTokenByAuthorizationCodeAsync(authorizationCode, new Uri(redirectUri), credential);
AcquireTokenByAuthorizationCodeAsync stores access token and refresh token in TokenCache.DefaultShared (for user uniqueId received from auth procedure).
Assuming you do that, access tokens and refresh tokens do expire. If that happens, you must catch AdalSilentTokenAcquisitionException exception:
try
{
// currentUser = new UserIdentifier() for: ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")
AuthenticationResult authResult = await context.AcquireTokenSilentAsync(resourceUri, credential, currentUser);
return authResult.AccessToken;
}
catch (AdalSilentTokenAcquisitionException)
{
return null;
}
Invoke this method before every request to the resource. It doesn't cost much, ideally nothing, or oauth API hit with refreshToken.
But when AdalSilentTokenAcquisitionException is thrown (or it's a first call). You must call procedure that performs full access code retrieval from oauth API. What procedure? It depends on the type of auth process you're using.
With full owin auth it can be:
redirect to authority uri with {"response_type", "code" }
or invoking HttpContext.GetOwinContext().Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType); (return null from controller's action as the Challenge() method alters HTTP response to force redirection to auth server). End processing of current request (with returning null). Auth server will invoke your authorization method (AuthorizationCodeReceived event from UseOpenIdConnectAuthentication's Notifications) with new authorization code. Later, redirect back to the origin page that needs the token.
So, you may get AdalSilentTokenAcquisitionException because cache is expired and refreshToken is expired. You have to reauthenticate again (it's transparent, no login page required).

OAuth Bearer Access Token sliding expiration

Let's suppose that we're using OAuth Bearer tokens to secure our API. There is NuGet package with OWIN middleware that will do it for us: https://www.nuget.org/packages/Microsoft.Owin.Security.OAuth.
Everethig looks great, until raises question about access token expiration - we don't want to force use to re-login over and over again. As far as I understand there are three basic ways:
Make Access Token expiration time very big (1 month for instance)
Use OAuth Refresh Tokens that adds much difficulties to both Authentication Server and the user application code (described in following article http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/)
I'm curious is it possible to create the endpoint that will require access token that is about to expire and just answer with new access token to simulate kind of sliding expiration for OAuth Access Tokens?
WARNING! Here is the solution that NO ONE SHOULD USE if you're not 100% sure that your application guarantees (which is impossible) that Access Token can not be compomised (for instance, XSS vulnerability allows to steal Access Token). In this solution once Access Token leaked it can be used to indefinitely prolong the access. OAuth Refresh Tokens solve exactly this problem, limiting access in case of compromising Access Token with very short amount of time, usually about 15 minutes.
[Authorize]
public class RefreshTokenController : ApiController
{
[HttpGet]
public HttpResponseMessage ReissueToken()
{
// just use old identity
var identity = ((ClaimsPrincipal)User).Identity as ClaimsIdentity;
var ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
DateTimeOffset currentUtc = new SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.AddMinutes(30);
string token = Startup.OAuthBearerAuthOptions.AccessTokenFormat.Protect(ticket);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ObjectContent<object>(new
{
accessToken = token,
expiresIn = (int)((ticket.Properties.ExpiresUtc.Value - ticket.Properties.IssuedUtc.Value).TotalSeconds),
}, Configuration.Formatters.JsonFormatter)
};
}
}

Categories

Resources