.Net Core Graph API Token Issue - "Access token has expired or is not yet valid" - asp.net-mvc

I am developing a .Net 6 application, hosted in an Azure App Service and using Azure AD Authentication.
When viewing a Request page, I would like to check if the user belongs to an Azure Ad Group. This works sometimes, but users will periodically get an error when trying to view the page: "Access token has expired or is not yet valid."
I assume the token is being expired as if the user clears their cookies, AAD will re-authenticate them creating a new token and all is fine again, but but I haven't been able to find anything around refreshing tokens and am not sure where to go from here.
Has anyone experienced this behaviour and found a solution for it?
Here are some relevant sections of the code
Startup.cs File:
string[] initialScopes = Configuration.GetValue<string>("GraphAPI:Scopes")?.Split(' ');
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(options =>
{
Configuration.Bind("AzureAd", options);
}, initialScopes)
.AddInMemoryTokenCaches()
.AddMicrosoftGraph(options =>
{
options.Scopes = String.Join(' ', initialScopes);
});
AADGroupFunctions.cs
AADGroupFunctions.cs
private readonly GraphServiceClient _graphServiceClient;
public AADGroupFunctions(GraphServiceClient graphServiceClient)
{
_graphServiceClient = graphServiceClient;
}
public async Task<List<IADLookupModel>> FindUsersInGroup(string groupId)
{
var listOfUsers = new List<IADLookupModel>();
var filterString = $"startswith(mail, '{groupId}')";
var groups = await _graphServiceClient.Groups
.Request()
.Header("ConsistencyLevel", "eventual")
.Filter(filterString)
.Expand("members")
.Top(1)
.GetAsync();
if (groups.Any())
{
if (groups.First().Members.Any())
{
foreach (Microsoft.Graph.User user in groups.First().Members)
{
try
{
var mail = "";
if (user.Mail != null)
{
mail = user.Mail.ToLower();
listOfUsers.Add(new UserModel()
{
DisplayName = user.DisplayName,
UPN = user.UserPrincipalName.ToLower(),
Email = mail,
Description = user.JobTitle ?? ""
});
}
}
catch (Exception)
{
}
}
}
}
return listOfUsers;
}
Error Message when trying to call the FindUsersInGroup() function:
An unhandled exception occurred while processing the request.
ServiceException: Code: InvalidAuthenticationToken Message: Access token has expired or is not yet valid. Inner error: AdditionalData: date: 2022-02-21T17:37:46 request-id: [removed] client-request-id: [removed] ClientRequestld: [removed] Microsoft.Graph.HttpProvider.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
Routing

The access token have a short lifetime, sometimes like an hour or even shorter. So you need to use a refresh token to ask AzureAd for a new access token when the current one is about to expire.
see this link https://learn.microsoft.com/en-us/azure/active-directory/develop/refresh-tokens

Related

MSAL.NET OBO refresh token problems

I am trying to implement an OBO flow through to the graph API on a middle-tier API (.NET 5.0) using MSAL.NET. I'm running into two frustrating problems, and I can't find anyone having similar problems, so I think I'm misunderstanding something!
Problem 1: Whenever I call MSAL's GetAccountAsync, it always returns null when there should be an account loaded.
Problem 2: Whenever I call MSAL's AcquireTokenSilent, I always get the error "No refresh token found in the cache." even though I got one.
Here's what I have:
Once the web app authenticates, it passes through the token to a graph auth endpoint on the API:
var authenticationResult = await ClaimHelper.ClientApplication.AcquireTokenByAuthorizationCode(GraphHelpers.BasicGraphScopes, context.Code).ExecuteAsync();
var apiUserSession = await CouncilWiseAPIHelper.APIClient.Graph.AuthoriseUserAsync(authenticationResult.AccessToken);
which seems to work fine, and passes through a JWT to the API auth endpoint. The API implements an MSAL Confidential Client application and uses the SetBeforeAccess/SetAfterAccess token cache methods to save the cache to a database.
_msalClient = ConfidentialClientApplicationBuilder.Create(_graphConfig.ClientId)
.WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
.WithClientSecret(_graphConfig.ClientSecret)
.Build();
SetSerialiser(serialiser);
public void SetSerialiser(MSALTokenCacheSerialiser serialiser)
{
_msalClient.UserTokenCache.SetBeforeAccessAsync(serialiser.BeforeAccessCallbackAsync);
_msalClient.UserTokenCache.SetAfterAccessAsync(serialiser.AfterAccessCallbackAsync);
}
And the serialiser methods look like this:
public async Task BeforeAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
GraphUserTokenCache tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
if (tokenCache == null)
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache != null)
{
notification.TokenCache.DeserializeMsalV3(tokenCache.Value);
}
}
public async Task AfterAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
if (!notification.HasTokens)
{
// Delete from the cache
await _graphUserTokenCacheRepository.Delete(notification.SuggestedCacheKey);
}
if (!notification.HasStateChanged)
{
return;
}
GraphUserTokenCache tokenCache;
if (notification.SuggestedCacheKey == notification.Account.HomeAccountId.Identifier)
{
tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
}
else
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache == null)
{
var cache = notification.TokenCache.SerializeMsalV3();
tokenCache = new GraphUserTokenCache
{
Id = notification.SuggestedCacheKey,
AccountIdentifier = notification.Account.HomeAccountId.ToString(),
Value = cache
};
await _graphUserTokenCacheRepository.Add(tokenCache);
}
else
{
await _graphUserTokenCacheRepository.Update(tokenCache.Id, notification.TokenCache.SerializeMsalV3());
}
}
I can see the token BeforeAccess and AfterAccess methods being called, and I can see the caches being created in the database (encryption has been removed while I'm trying to track down this issue). If I inspect the serialised token cache being saved, it NEVER has a refresh token populated, but if I inspect the requests with fiddler I can see a refresh token was indeed provided.
Finally, here is the code for retrieving the access token which is called whenever a graph request is made:
public async Task<AuthenticationResult> GetAccessToken(string accountId, string jwtBearerToken)
{
try
{
IAccount account = null;
if (accountId.IsNotNullOrEmpty())
{
account = await _msalClient.GetAccountAsync(accountId);
}
var scope = _graphConfig.Scopes.Split(' ');
if (account == null)
{
var result = await _msalClient.AcquireTokenOnBehalfOf(scope,
new UserAssertion(jwtBearerToken))
.ExecuteAsync();
return result;
}
else
{
var result = await _msalClient.AcquireTokenSilent(scope, account)
.ExecuteAsync();
return result;
}
}
catch (MsalClientException ex)
{
ex.CwApiLog();
return null;
}
catch(Exception ex)
{
ex.CwApiLog();
return null;
}
}
When it's called with the jwtBearerToken, it will successfully call AcquireTokenOnBehalfOf() and the token is cached and a result returned, but when I come back to retrieve the account via GetAccountAsync() it always returns null even though I can see the token cache was loaded in BeforeAccessCallbackAsync().
Also, even if I call AcquireTokenSilent() immediately after acquiring the obo token with the account it just returned, I will get an exception saying there is no refresh token in the cache.
I am totally lost on what I'm doing wrong here, any help would be greatly appreciated.
I recently ran into the same problem while running a long runing OBO flow, MSAL has recently implemented an interface ILongRunningWebApi for these use cases you can go and see this new documentation
Here is an extract:
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). Starting with MSAL.NET 4.38.0, 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,
userToken,
ref sessionKey)
.ExecuteAsync();
userToken is a user 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). If the cache already contains a valid OBO token with this
sessionKey, InitiateLongRunningProcessInWebApi will return it.
Otherwise, the user token will be used 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.
Hope this helps :)

Get 404 "The resource could not be found" when call /beta/informationprotection/policy/labels

according to documentation we may use the following endpoints for fetching sensitivity labels:
/me/informationProtection/policy/labels (using delegated permissions)
/informationProtection/policy/labels (using application permission. App should have InformationProtectionPolicy.Read.All permission to use this end point)
The following C# code uses app permissions and it works on tenant1:
static void Main(string[] args)
{
string accessToken = getTokenImpl().Result;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("User-Agent", "PostmanRuntime/7.24.1");
using (var response = client.GetAsync($"https://graph.microsoft.com/beta/informationprotection/policy/labels").Result)
{
using (var content = response.Content)
{
string result = content.ReadAsStringAsync().Result;
if (response.IsSuccessStatusCode)
{
Console.WriteLine(result);
}
}
}
}
}
private static async Task<string> getTokenImpl()
{
string clientId = "...";
string clientSecret = "...";
string tenant = "{...}.onmicrosoft.com";
string authority = string.Format("https://login.microsoftonline.com/{0}", tenant);
var authContext = new AuthenticationContext(authority);
var creds = new ClientCredential(clientId, clientSecret);
var authResult = await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
return authResult.AccessToken;
}
But it doesn't work on another tenant2 - there it always returns 404 "The resource could not be found" with the following inner exception "User not found to have labels, policy is empty". Here is full response:
{
"error": {
"code": "itemNotFound",
"message": "The resource could not be found.",
"innerError": {
"code": "notFound",
"message": "User not found to have labels, policy is empty",
"target": "userId",
"exception": null,
"date": "2020-11-18T09:29:20",
"request-id": "657ad51c-9cab-49f2-a242-50929cdc6950",
"client-request-id": "657ad51c-9cab-49f2-a242-50929cdc6950"
}
}
}
Interesting that attempt to call endpoint /me/informationProtection/policy/labels with delegated permissions on the same tenant2 gives the same error, but on tenant1 it also works. Did anybody face with this problem or have idea why it may happen? Need to mention that on tenant2 earlier we created and published several sensitivity labels for specific user - this user doesn't have neither O365 license nor Azure subscription. I.e. when you try to login to SPO/Azure and create site/group - sensitivity labels were not shown at all for this user. We tried to remove these sensitivity labels and their policies with audience targeting to this user, but both end points still return error.
PS. AAD app is Ok on tenant2 - it has InformationProtectionPolicy.Read.All permission and admin consent is granted:
Update 2020-11-25: behavior has been changed on both tenants without any change from our side: now on both tenants we get 502 Bad Gateway. Does MS rolls out this functionality globally now? Here is response which we get now from /beta/me/informationProtection/policy/labels:
{
"error":{
"code":"UnknownError",
"message":"<html>\r\n<head><title>502 Bad Gateway</title></head>\r\n<body>\r\n<center><h1>502 Bad Gateway</h1></center>\r\n<hr><center>Microsoft-Azure-Application-Gateway/v2</center>\r\n</body>\r\n</html>\r\n",
"innerError":{
"date":"2020-11-25T12:59:51",
"request-id":"93557ae1-b0d9-44a9-bbea-871f18e379ea",
"client-request-id":"93557ae1-b0d9-44a9-bbea-871f18e379ea"
}
}
}
Update 2020-12-07: it started to work by its own. I.e. MS has fixed that on backend side somehow for the tenant when this issue was reproduced.

Sign In using raw HttpRequestMessage in ASP.NET MVC

I have been testing some code to sign in users to their Microsoft/school/work accounts using raw HttpRequestMessage and HttpResponseMessage. I know there are libraries available to do this but I want to test the raw approach as well (especially usage of refresh tokens), while looking for the right library to handle it.
I'm currently learning authentication, with limited knowledge of ASP.NET/Core.
I'm following this guide: https://learn.microsoft.com/en-us/graph/auth-v2-user
I've just modified the SignIn() method in AccountController in an example project that used more high level libraries to sign in.
I'm requesting an authorization code.
The SignIn() code:
public void SignIn()
{
using (var httpClient = new HttpClient())
{
try
{
var tenant = "my tenant id";
var clientId = ConfigurationManager.AppSettings["ida:AppID"];
var responseType = "id_token+code";
var redirectURI = ConfigurationManager.AppSettings["ida:RedirectUri"];
var responseMode = "form_post";//query";
var appScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
var scopes = $"openid profile offline_access {appScopes}";
var state = "12345";
//var prompt = "consent";
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize", tenant);
var body = string.Format("client_id={1}&response_type={2}&redirect_uri={3}&response_mode={4}&scope={5}&state={6}", tenant, clientId, responseType, redirectURI, responseMode, scopes, state);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).Result;
var content = response.Content.ReadAsStringAsync().Result;
}
catch (Exception ex)
{
}
}
//if (!Request.IsAuthenticated)
//{
// // Signal OWIN to send an authorization request to Azure
// Request.GetOwinContext().Authentication.Challenge(
// new AuthenticationProperties { RedirectUri = "/" },
// OpenIdConnectAuthenticationDefaults.AuthenticationType);
//}
}
I'm just returning void from the method now because I'm not sure what I should return yet.
Debugging and looking at the response variable, the status code is 200, and has some other information to it. However, the content of the HttpResponseMessage, when I paste it into a file and opening it in a browser, displays (or redirects to) https://login.microsoftonline.com/cookiesdisabled, which shows a message saying that I could not be logged in because my browser blocks cookies. However, I don't think this really is the case.
How can I resolve this and have the user log in and consent, and get the authorization code?
I couldn't really find any example in ASP.NET that uses this raw approach. Is it not recommended?
You should fistly understand how OAuth 2.0 authorization code flow works in Azure AD V2.0 :
Microsoft identity platform and OAuth 2.0 authorization code flow
The general process would be like :
When login in client application, user will be redirect to Azure AD login endpoint(https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize) and provides info like which client(client_id) in which tenant(tenant id) user wants to login , and redirect back to which url(redirect_uri) after successful login.
User enter credential , Azure AD validate credential and issue code and redirect user back to redirect url provided in step 1 (Also match one of the redirect_uris you registered in the portal).
The client application will get the code and send http post request with code to acquire access token .
So if you want to manally implement the code flow in your application , you can refer to below code sample :
public async Task<IActionResult> Login()
{
string authorizationUrl = string.Format(
"https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize?response_type=code&client_id={1}&redirect_uri={2}&scope={3}",
"tenantID", "ClientID", "https://localhost:44360/Home/CatchCode",
"openid offline_access https://graph.microsoft.com/user.read");
return Redirect(authorizationUrl);
}
private static readonly HttpClient client = new HttpClient();
public async Task<ActionResult> CatchCode(string code)
{
var values = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "client_id", "XXXXXX"},
{ "code", code},
{ "redirect_uri", "https://localhost:44360/Home/CatchCode"},
{ "scope", "https://graph.microsoft.com/user.read"},
{ "client_secret", "XXXXXXXXXXX"},
};
var content = new FormUrlEncodedContent(values);
//POST the object to the specified URI
var response = await client.PostAsync("https://login.microsoftonline.com/cb1c3f2e-a2dd-4fde-bf8f-f75ab18b21ac/oauth2/v2.0/token", content);
//Read back the answer from server
var responseString = await response.Content.ReadAsStringAsync();
//you can deserialize an Object use Json.NET to get tokens
}
That just is simple code sample which will get Microsoft Graph's access token , you still need to care about url encode and catch exception , but it shows how code flow works .

Azure Mobile Services LoginAsync method not working with Microsoft Auth Token

I have successfully been able to get an access_token (or authenticationToken for Microsoft tokens) using the client side authentication in my Xamarin forms App. I am able to get further user information (email, name, etc.) using the same access token. Now, when I try to pass that token to my Azure Mobile Service backend, I get a 401 error.
Here is my code:
private async System.Threading.Tasks.Task<string> MSGetUserInfo(Account account)
{
// Reference: http://graph.microsoft.io/en-us/docs/overview/call_api
// Note that Microsoft don't recognize the access_token header entry, but rely instead on an Authorization header entry
var client = new HttpClient();
var userInfoRequest = new HttpRequestMessage()
{
RequestUri = new Uri("https://graph.microsoft.com/v1.0/me"),
Method = HttpMethod.Get,
};
// Add acccess Bearer
userInfoRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", account.Properties["access_token"]);
using (var response = await client.SendAsync(userInfoRequest).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
Models.User user = new Models.User();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var jobject = JObject.Parse(responseString);
var userName = (string)jobject["userPrincipalName"];
// Check username is valid
if (String.IsNullOrEmpty(userName))
{
throw new Exception("Username was not set for authenticated user");
}
else
user.ProviderLoginId = userName;
var userDisplayName = (string)jobject["displayName"];
// Replace display name if invalid
if (String.IsNullOrWhiteSpace(userDisplayName))
{
userDisplayName = userName;
}
else
user.Name = userDisplayName;
var userEmail = (string)jobject["mail"];
// Replace email if invalid
if (String.IsNullOrWhiteSpace(userEmail))
{
userEmail = userName;
}
else
user.Email = userEmail;
Valufy.App.currentUser = user;
}
else
{
throw new Exception("OAuth2 request failed: " + await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
}
return "success";
}
The above code snippet works in getting my user details. Now when I try to use the same token in the subsequent call, I get a 404:
public async Task<bool> Authenticate(string token)
{
string message = string.Empty;
var success = false;
JObject objToken = new JObject();
//objToken.Add("access_token", token); //for facebook and google
objToken.Add("authenticationToken", token); //for microsoft
try
{
// Sign in with Facebook login using a server-managed flow.
if (user == null)
{
//ProviderAuth("MICROSOFT");
user = await syncMgr.CurrentClient
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount, objToken);
if (user != null)
{
success = true;
message = string.Format("You are now signed-in as {0}.", user.UserId);
}
}
}
catch (Exception ex)
{
message = string.Format("Authentication Failed: {0}", ex.Message);
}
// Display the success or failure message.
// await new MessageDialog(message, "Sign-in result").ShowAsync();
return success;
}
Is there something that I am doing wrong? Any and all assistance is appreciated.
According to your description, I followed this Git sample about Microsoft Graph Connect Sample for UWP (REST). I could get the access_token and it could work as expected with Microsoft Graph API (e.g. Get a user). But when I use this access_token as the authenticationToken token object for MobileServiceClient.LoginAsync, I could also get 401 Unauthorized.
Then I checked the managed client for Azure Mobile Apps about Authenticate users. For Client-managed authentication flow, I found that the official code sample about using Microsoft Account is working with Live SDK as follows:
// Request the authentication token from the Live authentication service.
// The wl.basic scope should always be requested. Other scopes can be added
LiveLoginResult result = await liveIdClient.LoginAsync(new string[] { "wl.basic" });
if (result.Status == LiveConnectSessionStatus.Connected)
{
session = result.Session;
// Get information about the logged-in user.
LiveConnectClient client = new LiveConnectClient(session);
LiveOperationResult meResult = await client.GetAsync("me");
// Use the Microsoft account auth token to sign in to App Service.
MobileServiceUser loginResult = await App.MobileService
.LoginWithMicrosoftAccountAsync(result.Session.AuthenticationToken);
}
Note: As LiveConnectSession states about AuthenticationToken:
The authentication token for a signed-in and connected user.
While check the authentication with Microsoft Graph, I could only find the access_token instead of AuthenticationToken.
UPDATE:
I have checked LiveLogin for WP8 and Microsoft Account Authentication for Mobile Apps via Fiddler to capture the authorize requests. I found that MS account authentication has the similar authorize request as Live SDK.
I assumed that you need to leverage Live SDK to authenticate the user when using client side authentication with Microsoft account. I found the Live SDK download page is not exist, you could follow the Live SDK for WP8 to get started with Live SDK.
UPDATE2:
For the client-flow authentication (Microsoft Account), you could leverage MobileServiceClient.LoginWithMicrosoftAccountAsync("{Live-SDK-session-authentication-token}"), also you could use LoginAsync with the token parameter of the value {"access_token":"{the_access_token}"} or {"authenticationToken":"{Live-SDK-session-authentication-token}"}. I have tested LoginAsync with the access_token from MSA and retrieve the logged info as follows:

Microsoft Graph API - Updating password

I am using the Microsoft Graph API sample project. I am able to login fine.
I am trying to update the password of the user that is logged in using the following code:
public async Task<bool> UpdatePassword(GraphServiceClient graphClient, string newPassword)
{
User me = await graphClient.Me.Request().UpdateAsync(new User
{
PasswordProfile = new PasswordProfile
{
Password = newPassword,
ForceChangePasswordNextSignIn = false
},
});
return true;
}
When I execute the code, I get the following error:
{
Status: 500
Message: "No offeractions were provided for validating consent."
Internal error: "empty offerActions array."
}
Any idea what I might be doing incorrectly?
I gave access to everything "Users" related via the App Registration Portal at https://apps.dev.microsoft.com
Thank you!
There is a baked-in ChangePassword() function that you'll want to use for this:
await graphClient.Me.ChangePassword("current-pwd, "new-pwd").Request().PostAsync();

Resources