Why did azure graph service client start to throw different exceptions? - c#

I am using azure graph service client to add/update users and groups in Office365.
I relied on its exceptions which were of type ServiceException and contained status code and message, which I used to determine what to do next with this result.
It worked well, because when I tried to retrieve user I simply used graphClient.Users[email].Request().GetAsync() and I got ServiceException with status of 'NotFound' I knew that user does not exist and what should I do about.
Unfortunately now it has changed and when user does not exist I get System.Exception with following message Object reference not set to an instance of an object.
I'm guessing that it still means that user was not found, but it could also mean something different so my custom logic might break if I catch it and treat it like user does not exist.
Does anybody know why it's like that? Did anything change lately with exceptions from graph client or is it temporar bug on graph service client side?
// UPDATE
Here's the code I used to create graph service client and run the method to retrieve a user:
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("client_id")
.WithTenantId("tenant_id")
.WithClientSecret("client_secret")
.Build();
var scopes = new[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync();
}
catch(Exception)
{
// error handling
}
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
}));
var user = graphClient.Users["email"].Request().GetAsync();

Related

MSAL Issues when integrating Azure AD Authentication and Authorization

I have enabled authentication for azure ad by following some sample apps from MS as shown below. I then use the authorize statements below as well so my application should be locked down unless the user is authenticated. My issue in my testing environment is if i restart IIS express then when it starts back up I am not required to log back in if the browser is still open. I can openly navigate my application but when any function related to MS Graph is called it fails with error: ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. So its as if my application thinks I am still authorized but really I am not because the MS Graph api call fails because of token related issues. If I force myself to logout then log back in everything works. For my live site if a user is inactive for some time but the browser is left open this same issue occurs and they are not forced to relogin so these issues again can happen until they force logout and re log back in. What have I setup wrong or need to add to force relogin before these issues arise? Or can I keep the token working without forcing the user to relogin?
// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
});
//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
//Tap into this event to add a UserID Claim to a new HttpContext identity
OnTokenValidated = context =>
{
//This query returns the UserID from the DB by sending the email address in the claim from Azure AD
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
})
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
Then in all of my controllers I use:
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
Adding
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(initialScopes);
before my graph api call fixes my issue. However, I feel as though I shouldnt need to call this everytime but only if the token expires. So is there a way to check if the token expires?
There are two things that worked for me. One was trying to get a token manually if an error occurred using the following:
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(initialScopes);
The other solution that seemed to work the same was to add the following catch as well:
try
{
Console.WriteLine($"{svcex}");
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
_consentHandler.ChallengeUser(initialScopes, claimChallenge);
}
catch (Exception ex2)
{
_consentHandler.HandleException(ex2);
}
I do not know enough about what consent handler does to provide more info on why this works but will update my answer once I do.

Getting error while using DataFactoryManagementClient.pipelines method

I am trying to automate ADF pipeline runs from .NET. Was able to create a client and when I try to do the below call
var pipeline = client.Pipelines.Get(resourceGroup, dataFactoryName, "test_1");
I am getting the error as Microsoft.Rest.Azure.CloudException: 'The document could not be retrieved because it does not exist.
Complete code below:
// Authenticate and create a data factory management client
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(applicationId)
.WithAuthority("https://login.microsoftonline.com/" + tenantID)
.WithClientSecret(authenticationKey)
.WithLegacyCacheCompatibility(false)
.WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
.Build();
AuthenticationResult result = await app.AcquireTokenForClient(new string[] { "https://management.azure.com//.default" }).ExecuteAsync();
ServiceClientCredentials cred = new TokenCredentials(result.AccessToken);
DataFactoryManagementClient client = new DataFactoryManagementClient(cred)
{
SubscriptionId = subscriptionId
};
Loos like it was a silly mistake. pipeline, I was trying to access from here was deleted by another process and hence this call failed.

MSAL System.InvalidOperationException: CompactToken parsing failed with error code: 80049217

I'm using MSAL for .NET to acquire tokens for my Graph API requests, but out of sudden, I'm getting following error, which I can see a lot of post about, but no solution of reason why Error 80049217 happens? Does anyone know why this error occurs and maybe a solution to avoid the error?
System.InvalidOperationException: CompactToken parsing failed with
error code: 80049217
UPDATE 22-01-10
Example of method to acquire access token (Client is instance of HttpClient reused by all threads using the factory class containing this method. _confidentialClient is an instance of IConfidentialClientApplication in the MSAL .NET library):
private IConfidentialClientApplication _confidentialClient;
public void Initialize()
{
// Construct the ConfidentialClientApplication
_confidentialClient =
ConfidentialClientApplicationBuilder.Create("clientId")
.WithClientSecret("clientSecret")
.WithAuthority("authority")
.Build();
}
// Multiple threads will access this method
private async Task GetAccessToken()
{
try
{
Console.WriteLine("Acquire token....");
// Is the .AcquireTokenForClient method thread safe??
var result = await _confidentialClient.AcquireTokenForClient("https://graph.microsoft.com/.default").ExecuteAsync();
if(Client.DefaultRequestHeaders.Authorization?.Parameter == result.AccessToken)
{
Console.WriteLine("Token havn't changed.");
return;
}
Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
Console.WriteLine("Acquire token successfully!");
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
All threads interacting with Graph API by this factory class will start by calling the GetAccessToken method to make sure the HttpClient has a valid AccessToken in the Authorization header. As far as I have read about IConfidentialClientApplication, the AcquireTokenForClient() will look for valid tokens in the internal cache, and if there isn't any, acquiring a new one, which is why the method is always invoked by any thread.
UPDATE 22-01-13:
Added some logic of how the IConfidentialClientApplication is built.
It seemed that you wrote this code in your asp.net core backend project, and you wanna a method to help generate access token for different scopes without entering user name/password to sign in, so that it can serve different scenario. But you made a mistake here.
See this document first. In a server/daemon application, you can only use client credential flow to generate access token, so the scope for graph api should be https://graph.microsoft.com/.default, and this section provides the sample code to use client credential flow in your asp.net core app. Here's the snippet.
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "common";
// Values from app registration
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);

Microsoft Graph API daemon - Error: ResourceNotFound Message: Resource could not be discovered

I'm trying to create a daemon using Microsoft Graph API v1.0.
I've registered my app with application permission Calendars.ReadWrite and User.Read.All with granted admin consent.
I get the access token correctly and I call GetUserId that returns the user id for setting requestURI.
After that I want to retrieve Outlook Calendar:
var id = await GetUserId(result.AccessToken);
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
String requestURI = $"https://graph.microsoft.com/v1.0/users/{id}/calendars";
var response = await httpClient.GetAsync(requestURI);
var responseString = await response.Content.ReadAsStringAsync();
but I get this error:
{
"error": {
"code": "ResourceNotFound",
"message": "Resource could not be discovered.",
"innerError": {
"request-id": "5ecd547b-9281-4824-94e5-095691e759aa",
"date": "2020-01-14T16:44:16"
}
}
}
When I set requestURI to users/{id} or organization the request works fine, but adding /calendars, /events, or /mailFolder results in the above error.
I think my problem is that I used a Personal Account. Do I need to use a Work or School Account? Is it possible to use a Personal Account? Is there another reason for my error?
Update: Code for retrieving a token:
app = ConfidentialClientApplicationBuilder
.Create(ClientId)
.WithClientSecret(ClientSecret)
.WithAuthority($"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token&grant_type=client_credentials&resource=https://graph.microsoft.com")
.Build();
string[] scopesClient =
new string[] { $"https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(scopesClient).ExecuteAsync();
}
catch (MsalServiceException ex) when(ex.Message.Contains("AADSTS70011"))
{
}
You're Authority isn't quite right:
resource=https://graph.microsoft.com is a legacy setting and not used for the v2 Endpoint (aka authentication with Scopes rather than Resources).
The ConfidentialClientApplicationBuilderhandes setting the OAuth Grant automatically so specifying grant_type=client_credentials is not needed.
The Authority should only contain the authentication authority (https://login.microsoftonline.com/) and the tenant id. The easiest way to handle this is using the AzureCloudInstance.AzurePublic enumeration
Your token code should look something like this:
app = ConfidentialClientApplicationBuilder
.Create(ClientId)
.WithClientSecret(ClientSecret)
.WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
.Build();
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await app
.AcquireTokenForClient(scopes)
.ExecuteAsync();
}
catch (MsalServiceException ex)
{
}
Note: You will not be able to use this method with an #outlook.com account. Personal Accounts do not support client_credentials.
In order to call /{user-id}/events,/calendar or /mailFolder to work the user must have mailbox on Exchange Online if you are using client credentials for Daemon application.
We are generally getting this {"error":{"code":"ResourceNotFound","message":"Resource could not be discovered."}} error for organizational users when license is not assigned to the users or mailbox is not configured for them.
In Microsoft personal Account user does not have a mailbox (which make sense with null value as below), so the call wouldn’t work.
It seems assigning license to a guest account (Microsoft personal account in this case) is not possible and hence the user account never gets access to the calendar service (part of o365 exchange online). due to which it cannot retrieve the calendar information of personal Outlook account.
The documentation states that you have to add the CalenderRead and CalenderReadWrite both. If not, I would use delegated permission if you are using personal account. I would also go to ms.jwt, where they check your token and tell you what is the permission you have and what do you, need to call calendar endpoints

"The scope https://graph.microsoft.com/Calendars.Read is not valid" error while getting access token for my app

I am trying to use MS graph API to access user's calendar events but while trying to get the access token for my app that I registered in azure,
I am getting the following error:
Error : The scope https://graph.microsoft.com/Calendars.Read is not
valid.
Below is my code:
string token = string.Empty;
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create("ClientID")
.WithTenantId("TenantID")
.WithClientSecret("ClientSecret")
.Build();
string[] scopes = new string[] { "https://graph.microsoft.com/Calendars.Read" };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
token = result.AccessToken;
var graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) => {
requestMessage
.Headers
.Authorization = new AuthenticationHeaderValue("bearer", token);
return Task.FromResult(0);
}));
var events = await graphServiceClient.Users["user1#onTestMicrosoft.com"].Events.Request().GetAsync();
}
catch (MsalServiceException ex)
{
// Case when ex.Message contains:
// AADSTS70011 Invalid scope. The scope has to be of the form "https://resourceUrl/.default"
// Mitigation: change the scope to be as expected
}
What am I doing wrong here? I have already granted permission to Calendars.Read in azure portal while registering my app there: https://www.screencast.com/t/jTjnB4SX5I
A couple of things are going on here.
When you use the client credentials flow, you're required to use a scope of the form {resource}/.default, where {resource} is the URL of the thing you want access to. In this case, your scope should be https://graph.microsoft.com/.default. (Source)
You have not configured any application permissions on your app registration. From your screenshot, you've only configured delegated permissions, which are user permissions that require a logged in user. Add Calendars.Read as an application permission and that should get you going.

Categories