Azure - AD - AcquireTokenSilent giving error failed_to_acquire_token_silently - c#

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).

Related

Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential

I have a console app written with C# on the top of .NET Core 2.2 framework.
I am trying to use my app to connect Google My Business API to create posts.
But every time I try to call the REST API I get the following error
Request had invalid authentication credentials. Expected OAuth 2
access token, login cookie or other valid authentication credential.
See
https://developers.google.com/identity/sign-in/web/devconsole-project.
The code worked previously, but for some odd reason, it stopped!
Hereis an example where I get authentication token and then call the API to get a list of Google accounts.
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = "Client ID",
ClientSecret = "Client Secret",
}, new[] { "https://www.googleapis.com/auth/plus.business.manage" }, "google username", CancellationToken.None);
using (var client = new HttpClient())
{
//client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", credential.Token.AccessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var c = await client.GetAsync("https://mybusiness.googleapis.com/v4/accounts");
var accountContentss = await c.Content.ReadAsStringAsync();
c.EnsureSuccessStatusCode();
var accountContent = await c.Content.ReadAsStringAsync();
}
I am able to authenticate and get the access token with no issue. However, the second call to the API fails for some reason.
How can I correctly call Google API? Beside the AccesToken, is there something else that should get passed in the header?
It turned out to be that GoogleWebAuthorizationBroker.AuthorizeAsync() was returning an expired token! It would have been very helpful if the API returned a message stating that the token is expired instead!
To fix the issue, instead of accessing the AccessToken manually (i.e, credential.Token.AccessToken), I used await Credential.GetAccessTokenForRequestAsync() method to get an access token.
The Credential.GetAccessTokenForRequestAsync() method returns an valid token any time it is called. In other words, if the token is expired, it uses the refresh-token to generate a new one. Otherwise, it return the existing non-expired token.
I change my Authorization header to this
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await Credential.GetAccessTokenForRequestAsync());

Should TokenCache be used for offline acces with MSAL?

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)
{
...

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!

MS Graph - 401 Unauthorized seemingly with proper token and access

I am working with an ASP.NET Core 2.0 application hosted on Azure and authenticates users through Microsoft using MSAL. I am getting the basic information through the authentication process like name, username and group claims. However, I want to access some additional information through MS Graph, like the users profile photo. Initial authentication and token acquisition runs smoothly, and sending a request to https://graph.microsoft.com/beta/me returns 200 OK. When trying to call https://graph.microsoft.com/beta/me/photo/$value, however, I get a 401 - Unauthorized in return.
I have seen several other posts on this issue, but most of them concludes that the developer have either forgotten to ask for the proper consents, gotten tokens from the wrong endpoints, or similar issues. All of which I have confirmed not to be the case.
I have confirmed that the proper scopes are included in the token using https://jwt.ms/. I also tried asking for greater scopes than necessary. Currently I am using the following scopes: openid profile User.ReadBasic.All User.Read.All User.ReadWrite Files.ReadWrite.All. According to the beta reference for get user the least required permission is User.Read and according to the reference for get photo the least required permission is also User.Read. Using the Graph Explorer I have also confirmed that I should have had access to the photo using the permissions that I do, although, I have not set any pictures on my profile so it gives me a 404 response.
I am at a loss as to why I cannot get access to the profile photo so any suggestions are much appreciated. If you need more information or details, please let me know. If relevant, I have a custom middleware that handles the post-authentication process of reading the user information which also makes the additional call to MS Graph for the photo.
Edit:
I also tried https://graph.microsoft.com/beta/users/{my-user-id}/photo/$value which yielded the same results - 404 in Graph Explorer and 401 through my code
Edit 2: Code
Here is the code that I am using. This first snippet is in a middleware that puts the claims from the authenticated user in a specific format. I have just been putting a break point on the return and inspected the response object.
public async Task GetUserPhotoAsync(string userid, HttpContext context)
{
HttpClient client = new HttpClient();
//client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var result = await new TokenHelper(_settings).GetAuthenticationAsync(userid, context, new string[] { "User.ReadBasic.All", "User.Read.All", "User.ReadWrite", "Files.ReadWrite.All" });
var url = "https://graph.microsoft.com/beta/me/photo/$value";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
return;
}
Here is the function that gets the token from the cache. MSALSessionCache is some code I have borrowed from here with some tweaks to fit .net core.
public async Task<AuthenticationResult> GetAuthenticationAsync(string signedInUserId, HttpContext context, string[] scopes)
{
TokenCache userTokenCache = new MSALSessionCache(signedInUserId, context).GetMsalCacheInstance();
ConfidentialClientApplication cca =
new ConfidentialClientApplication(_settings.ClientId, $"{_settings.Domain}/{_settings.AADInstance}/v2.0", "http://localhost:5000", new ClientCredential(_settings.ClientSecret), userTokenCache, null);
if (cca.Users.Count() > 0)
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes, cca.Users.First());
return result;
}
else
{
throw new Exception();
}
}
Initial token acquisition
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async context =>
{
string signedInUserId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserId, context.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca =
new ConfidentialClientApplication(aadOptions.ClientId, aadOptions.RedirectUri, new ClientCredential(aadOptions.ClientSecret), userTokenCache, null);
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(context.ProtocolMessage.Code, new string[] { "User.ReadBasic.All", "User.Read.All", "User.ReadWrite", "Files.ReadWrite.All" });
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
Edit 3: Using the /v1.0 endpoint
As per Marc LaFleur's request I have tried the v1.0 endpoint with the same result. https://graph.microsoft.com/v1.0/me gives a 200 OK response code while https://graph.microsoft.com/v1.0/me/photo/$value returns 401 Unauthorized
I had the same problem with the Microsoft Graph giving a 401 Unauthorized exception when I was trying to query a user's photo or the photo's metadata on both the /V1.0 and /beta API endpoints. Like you, I verified I had the right tokens and was able to successfully access the user profile API.
In my case, I found it was because the photo for the user I was testing with hadn't been set. Once I assigned a photo I was able to successfully call both the photo value and photo metadata beta endpoints.
The v1.0 endpoints still gave me a 401 Unauthorized exception, but in my application I only use AzureAD, not Exchange. Based on #MarcLaFleur comments and the API documentation, this sounds like "expected" behaviour.
Why it returns a 401 Unauthorized instead of something like a 404 Not Found, or returning null values, I don't know.

Cant authenticate user silently with ADAL for Office 365 REST API on ASP.NET MVC

So I'm trying to implement persistent tokens for our office authentication so that the user does not have to sign into office each time they are in a new session. The code I currently have to authenticating the user is as below.
string authority = "https://login.microsoftonline.com/common";
var tokenCache = new ADALTokenCache(User.Identity.GetUserId());
AuthenticationContext authContext = new AuthenticationContext(authority, tokenCache );
var token = authContext.AcquireTokenSilentAsync(scopes, clientId, new UserIdentifier(userId, UserIdentifierType.RequiredDisplayableId));
But everything I've tried so far gives me the error below
The Exception is: "Failed to acquire token silently. Call method AcquireToken"
The method Im using to aquire the token in the first place is as below
string authority = "https://login.microsoftonline.com/common";
var fileCache = new ADALTokenCache(User.Identity.GetUserId());
AuthenticationContext authContext = new AuthenticationContext(authority, fileCache);
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
authCode, redirectUri, credential, scopes);
And the token cache im using is a db implementation which I made from a tutorial which I cannnot find again, if I watch the db I can see that new tokens are being inserted into the db when AcquireTokenByAuthorizationCodeAsync is called.
Update:
This is my result from authResult when calling AcquireTokenByAuthorizationCodeAsync
I have marked Virbonet's answer as the solution but I have not fixed it but he did explain to me where I was going wrong
AcquireTokenSilent cannot work if you are passing /common in the authority. Using "common" is equivalent to declaring that you don' know what tenant is the user from, hence ADAL cannot return a cached token form a specific tenant - user interaction is required to determine which tenant should be used.
If you want to call AcquireTokenSilent you need to initialize the authority with the exact tenant of the incoming user, as in "https://login.microsoftonline.com/"+tenantID here tenantID is the tenantID from the current ClaimsPrincipal.
This is the function call you need to use: AcquireTokenByAuthorizationCode() but not AcquireTokenSilent().
Hope this helps.

Categories

Resources