Commonly Occurring Errors in Microsoft Graph Integrations and How to Troubleshoot Them (Part 1)
This article documents common integration errors that may be seen for Microsoft Graph integrations into business apps and ways to handle those errors.
Join the DZone community and get the full member experience.
Join For FreeWith the release of Exchange Server in 2007, Microsoft also introduced Exchange Web Services (EWS). These SOAP-based APIs allow developers to access Microsoft Exchange products such as calendars, contacts, etc. over the internet. As part of the Office 365 product launch, Microsoft also released the new REST-based Office 365 Unified APIs, later known as the MS Graph APIs, in 2015. Microsoft announced in 2018 that there would be no more active development for the EWS APIs. The Exchange Web Services no longer meet today's security and maintenance requirements. For this reason, the various EWS APIs will be gradually shut down from 2022 on and replaced by the MS Graph APIs. An overview of services already accessible via the MS Graph APIs can be found in the current documentation from Microsoft. Due to the switch to MS Graph, various companies have to adapt their digital products.
In this article series, we will discuss some of the lessons learned during such a transition to Microsoft Graph REST API v1.0 with the MS Graph Java SDK.
Attach Large Files to Outlook Events (Audience Claim Value Is Invalid)
According to Microsoft documentation, only attachments up to 150MB in size can be uploaded and attached for events. Attachments up to 3MB in size can be uploaded with a single POST
call. For attachments between 3MB and 150 MB, an upload session is generated. Within the session, the attachment is uploaded piece by piece via multiple PUT
calls. For the second case, consider the following example:
final String primaryUser = "office365userEmail";
final String eventID = "eventID";
final GraphServiceClient<Request> graphClient = GraphServiceClient
.builder()
.authenticationProvider(getAuthenticationProvider())
.buildClient();
final File file = File.createTempFile("testFile", "txt");
FileUtils.writeByteArrayToFile(file, "testContent".getBytes());
InputStream fileStream = new FileInputStream(file);
final AttachmentItem attachmentItem = new AttachmentItem();
attachmentItem.attachmentType = AttachmentType.FILE;
attachmentItem.name = file.getName();
attachmentItem.size = file.getTotalSpace();
final AttachmentCreateUploadSessionParameterSet attachmentCreateUploadSessionParameterSet = AttachmentCreateUploadSessionParameterSet
.newBuilder()
.withAttachmentItem(attachmentItem)
.build();
final UploadSession uploadSession = graphClient
.users(primaryUser)
.events(evendID)
.attachments()
.createUploadSession(attachmentCreateUploadSessionParameterSet)
.buildRequest()
.post();
// Called after each slice of the file is uploaded
final IProgressCallback callback = (current, max) -> System.out.println("Uploaded "+ current + " bytes of " + max + " total bytes");
final LargeFileUploadTask<AttachmentItem> uploadTask = new LargeFileUploadTask<>(uploadSession, graphClient, fileStream, file.length(), AttachmentItem.class);
// upload (default: chunkSize is 5 MB)
uploadTask.upload(0, null, callback);
In the example, you can see that first, an uploadSession
is created via a POST
request. A possible response to the POST
request could look like the following:
HTTP/1.1 201 Created Content-type: application/json
{ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession",
"uploadUrl": "https://outlook.office.com/api/gv1.0/users('a8e8e219-4931-95c1-b73d-62626fd79c32@72aa88bf-76f0-494f-91ab-2d7cd730db47')/events('AAMkAGUwNjQ4ZjIxLTQ3Y2YtNDViMi1iZjc4LTMA=')/AttachmentSessions('AAMkAGUwNjQ4ZjIxLTAAA=')?authtoken=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFTeXQ1bXdXYVh5UFJ",
"expirationDateTime": "2021-12-27T14:20:12.9708933Z",
"nextExpectedRanges": [ "0-" ] }
As a response to the POST
request, you get an upload link uploadURL
, which can be used to upload the attachment. What becomes clear here is that the upload of the attachment is done via an Outlook API and not via an MS Graph API!
If you start the upload (uploadTask.upload(0, null, callback)
) then it can happen that you get the following error:
401 : Unauthorized upon request. com.microsoft.graph.core.ClientException:
Error code: InvalidAudienceForResource Error message:
The audience claim value is invalid for current resource.
Audience claim is 'https://graph.microsoft.com/', request url is
'https://outlook.office.com/api/v2.0/Users...'.
To understand this error in more detail, consider the following code snippet of the AuthenticationHandler
of the MS Graph Java SDK:
CompletableFuture<String> future = this.authProvider.getAuthorizationTokenAsync(originalRequest.url().url());
String accessToken = (String)future.get();
return accessToken == null
? chain.proceed(originalRequest)
: chain.proceed(originalRequest.newBuilder().addHeader("Authorization", "Bearer " + accessToken).build());
For each request via graphClient
, an authorization header with an access token for the MS Graph API is appended. Also for each PUT
request sent via the upload link, uploadURL
is sent to the Outlook API. This leads to the authorization problem because the authentication credentials are not valid for the Outlook API (return code 401).
One possible solution to avoid the problem with the audience claim value is to customize the authentication provider. To do this, consider this example: for the client graphClient
, the getAuthenticationProvider()
method defines the provider. The following implementation of this method generates a provider that first checks whether or not the host for the request is the MS Graph API. If not, then a CompletableFuture
with a null
value is returned.
private IAuthenticationProvider getAuthenticationProvider() {
return new IAuthenticationProvider() {
private String hostNameToCheck = "graph";
@Override
public CompletableFuture<String> getAuthorizationTokenAsync(URL requestUrl) {
CompletableFuture<String> future = new CompletableFuture<>();
if(requestUrl.getHost().toLowerCase().contains(hostNameToCheck)){
future.complete(getToken());
} else {
future.complete(null);
}
return future;
}
};
}
Due to the fact that a null
value is returned, each request to the MS Graph API is appended by an authorization header and each request to the Outlook API remains unchanged. For that reason, the InvalidAudienceForResource
error can be avoided.
From the example we can see that the problem is not caused by the service library: it's caused by the authorization library. The problem is known and is not expected to be fixed in the near future. For this reason, this workaround becomes a permanent solution.
Read Outlook Contacts With Two Fax Numbers (Missing SingleValueLegacyExtendedProperty)
According to the design of contacts, it is not foreseen that neither professional nor private fax numbers can be created for contacts with Microsoft Graph REST API v1.0.
However, the MS Graph API allows the possibility to extend the data model of running instances of a resource with custom data fields. Consider the following example where a contact is generated and stored with both professional and personal fax numbers:
public class FaxNumbersSample implements SampleClass {
private static final String PRIVATE_FAX_NUMBER = "privateFaxNumber";
private static final String BUSINESS_FAX_NUMBER = "businessFaxNumber";
private static final String PUBLIC_GUID_STRING = "00020329-0000-0000-c000-000000000046";
private static final String NAME = "} Name ";
@Override
public void run() {
final String primaryUser = "office365userEmail";
final String REQUEST_URI = "https://graph.microsoft.com/v1.0/users/"
+ primaryUser
+ "/contacts"
+ "/";
final GraphServiceClient<okhttp3.Request> graphClient =
GraphServiceClient
.builder()
.authenticationProvider(getAuthenticationProvider())
.buildClient();
Contact contact = new Contact();
List<SingleValueLegacyExtendedProperty> pageContents = new ArrayList<>();
pageContents.add(setProperty(BUSINESS_FAX_NUMBER, "001"));
pageContents.add(setProperty(PRIVATE_FAX_NUMBER, "002"));
if (!pageContents.isEmpty()) {
contact.singleValueExtendedProperties =
new SingleValueLegacyExtendedPropertyCollectionPage(pageContents,
new SingleValueLegacyExtendedPropertyCollectionRequestBuilder(
REQUEST_URI, graphClient, new ArrayList<>()));
}
contact.givenName = "Pavel";
contact.surname = "Bansky";
LinkedList<EmailAddress> emailAddressesList = new LinkedList<EmailAddress>();
EmailAddress emailAddresses = new EmailAddress();
emailAddresses.address = "pavelb@fabrikam.onmicrosoft.com";
emailAddresses.name = "Pavel Bansky";
emailAddressesList.add(emailAddresses);
contact.emailAddresses = emailAddressesList;
LinkedList<String> businessPhonesList = new LinkedList<String>();
businessPhonesList.add("+1 732 555 0102");
contact.businessPhones = businessPhonesList;
//response to the POST request doesn't contain the extra properties
contact = graphClient
.users(primaryUser)
.contacts()
.buildRequest()
.post(contact);
//to get all properties we need to expand the GET request
contact = graphClient
.users(primaryUser)
.contacts(contact.id)
.buildRequest()
.expand(new StringBuilder()
.append("singleValueExtendedProperties($filter=id eq 'String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(PRIVATE_FAX_NUMBER)
.append("'")
.append(" or id eq 'String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(BUSINESS_FAX_NUMBER)
.append("'")
.append(" or id eq 'String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(ACADEMIC_TITLE)
.append("')").toString())
.get();
}
private SingleValueLegacyExtendedProperty setProperty(String propertyName, String propertyValue) {
final SingleValueLegacyExtendedProperty property = new SingleValueLegacyExtendedProperty();
property.id = new StringBuilder()
.append("String {")
.append(PUBLIC_GUID_STRING)
.append(NAME)
.append(propertyName).toString();
property.value = propertyValue;
return property;
}
@Override
public IAuthenticationProvider getAuthenticationProvider() {
return new IAuthenticationProvider() {
private String hostNameToCheck = "graph";
@Override
public CompletableFuture<String> getAuthorizationTokenAsync(URL requestUrl) {
CompletableFuture<String> future = new CompletableFuture<>();
if(requestUrl.getHost().toLowerCase().contains(hostNameToCheck)){
future.complete(getToken());
} else{
future.complete(null);
}
return future;
}
};
}
@Override
public String getToken() {
return null;
}
}
The example shows that the standard data model can be extended by additional properties (SingleValueLegacyExtendedProperty
). When creating the additional data fields, a fixed format for the ID must be adhered to. This can look as follows:
String {00020329-0000-0000-c000-000000000046} Name faxNumber
Thereby 00020329-0000-0000-c000-0000000046
is called GUID, which can be used for all characters in general. Beware: the return value of the POST
request doesn't contain the extra fields. If you want to read all fields, then you have to do this via an additional extended GET
request. In the example above, the GET
request is extended by the following filter:
singleValueExtendedProperties($filter=id eq 'String {00020329-0000-0000-c000-000000000046} Name businessFaxNumber' or id eq 'String {00020329-0000-0000-c000-000000000046} Name privateFaxNumber'
Although by default fax numbers cannot be created via the Microsoft Graph REST API v1.0, the SingleValueLegacyExtendedProperty
class allows storing fax numbers. This solution path has the disadvantage that fax numbers are not automatically displayed in Outlook. However, Microsoft plans to include fax numbers in the default model of MS Graph in the future, which will not only simplify the storage of fax numbers but also fax numbers will be displayed in Outlook automatically.
Opinions expressed by DZone contributors are their own.
Comments