microsoftgraph / msgraph-sdk-java

Microsoft Graph SDK for Java
https://docs.microsoft.com/en-us/graph/sdks/sdks-overview
MIT License
403 stars 134 forks source link

java.time.format.DateTimeParseException: Text '2024-02-14T07:37:32' could not be parsed at index 19 when graphClient.applications().byApplicationId("<my app id>") .patch(app) if try to add a new certificate #1815

Closed vipetrov-bg closed 9 months ago

vipetrov-bg commented 9 months ago

Expected behavior

Perform request graphClient.applications().byApplicationId("") .patch(app) if try to add a new certificate and there are old configured certificates.

If there are not old configured certificates the call is successful!

Actual behavior

java.time.format.DateTimeParseException: Text '2024-02-14T07:37:32' could not be parsed at index 19 at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2052) at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1954) at java.base/java.time.OffsetDateTime.parse(OffsetDateTime.java:404) at java.base/java.time.OffsetDateTime.parse(OffsetDateTime.java:389) at com.microsoft.kiota.serialization.JsonParseNode.getOffsetDateTimeValue(JsonParseNode.java:98) at com.microsoft.graph.models.odataerrors.InnerError.lambda$getFieldDeserializers$1(InnerError.java:83) at com.microsoft.kiota.serialization.JsonParseNode.assignFieldValues(JsonParseNode.java:247) at com.microsoft.kiota.serialization.JsonParseNode.getObjectValue(JsonParseNode.java:198) at com.microsoft.graph.models.odataerrors.MainError.lambda$getFieldDeserializers$2(MainError.java:83) at com.microsoft.kiota.serialization.JsonParseNode.assignFieldValues(JsonParseNode.java:247) at com.microsoft.kiota.serialization.JsonParseNode.getObjectValue(JsonParseNode.java:198) at com.microsoft.graph.models.odataerrors.ODataError.lambda$getFieldDeserializers$0(ODataError.java:74) at com.microsoft.kiota.serialization.JsonParseNode.assignFieldValues(JsonParseNode.java:247) at com.microsoft.kiota.serialization.JsonParseNode.getObjectValue(JsonParseNode.java:198) at com.microsoft.kiota.http.OkHttpRequestAdapter.lambda$throwIfFailedResponse$0(OkHttpRequestAdapter.java:672) at com.microsoft.kiota.ApiExceptionBuilder.(ApiExceptionBuilder.java:26) at com.microsoft.kiota.http.OkHttpRequestAdapter.throwIfFailedResponse(OkHttpRequestAdapter.java:671) at com.microsoft.kiota.http.OkHttpRequestAdapter.send(OkHttpRequestAdapter.java:279) at com.microsoft.graph.applications.item.ApplicationItemRequestBuilder.patch(ApplicationItemRequestBuilder.java:273) at com.microsoft.graph.applications.item.ApplicationItemRequestBuilder.patch(ApplicationItemRequestBuilder.java:257)

Steps to reproduce the behavior

TokenCredential tokenCredential = new ClientCertificateCredentialBuilder().tenantId(tenantId) .clientId(clientId).pfxCertificate(pfxCertificatePath).clientCertificatePassword(pfxPassword) .build();

        String[] scopes = new String[] { "https://graph.microsoft.com/.default" };

        GraphServiceClient graphClient = new GraphServiceClient(tokenCredential, scopes);

        Application app = graphClient.applications().byApplicationId("<my app id>").get();

        List<KeyCredential> keyCredentialList = app.getKeyCredentials(); // There are old KeyCredentials

        X509Certificate certificate = ... //new certificate

        KeyCredential newKey = new KeyCredential();
        newKey.setType("AsymmetricX509Cert");
        newKey.setUsage("Verify");
        newKey.setKey(certificate.getEncoded());

        keyCredentialList.add(newKey);

        app.setKeyCredentials(keyCredentialList);

        Application updatedApp = graphClient.applications().byApplicationId("<my app id>")
                .patch(app);

Workaround

TokenCredential tokenCredential = new ClientCertificateCredentialBuilder().tenantId(tenantId) .clientId(clientId).pfxCertificate(pfxCertificatePath).clientCertificatePassword(pfxPassword) .build();

        String[] scopes = new String[] { "https://graph.microsoft.com/.default" };

        GraphServiceClient graphClient = new GraphServiceClient(tokenCredential, scopes);

        Application app = graphClient.applications().byApplicationId("<my app id>").get();

        List<KeyCredential> keyCredentialListOld = app.getKeyCredentials(); // There are old KeyCredentials

        // create a new Key Credential List and add old Key Credentials as using getters and setters
        List<KeyCredential> keyCredentialList = new ArrayList<>();
        for (KeyCredential key : keyCredentialListOld) {
            KeyCredential oldKey = new KeyCredential();
            oldKey.setDisplayName(key.getDisplayName());
            oldKey.setCustomKeyIdentifier(key.getCustomKeyIdentifier());
            oldKey.setKeyId(key.getKeyId());
            oldKey.setKey(key.getKey());
            oldKey.setStartDateTime(key.getStartDateTime());
            oldKey.setEndDateTime(key.getEndDateTime());
            oldKey.setUsage(key.getUsage());
            oldKey.setType(key.getType());
            keyCredentialList.add(oldKey);
        }

        X509Certificate certificate = ... //new certificate

        KeyCredential newKey = new KeyCredential();
        newKey.setType("AsymmetricX509Cert");
        newKey.setUsage("Verify");
        newKey.setKey(certificate.getEncoded());

        keyCredentialList.add(newKey);

        app.setKeyCredentials(keyCredentialList);

        Application updatedApp = graphClient.applications().byApplicationId("<my app id>")
                .patch(app);
kekolab commented 9 months ago

Hello there, I am actually experiencing the same bug which affects not only what described by the OP, but a lot of date-times.

TLDR;

When the remote service returns date time in the format 2024-02-11T15:25:37Z it gets correctly parsed; while when the returned datetime is in the format 2024-02-14T08:55:18, the above exception occurs. This happens because a formatter requiring a timezone is used to parse a string without timezone (instead of using a "local datetime" parser).

More detailed explanation

In the class com.microsoft.kiota.serialization.JsonParseNode the method responsible for parsing datetime strings is this one:

import java.time.OffsetDateTime;
[...]
@Nullable  public  OffsetDateTime getOffsetDateTimeValue() {
    final String stringValue = currentNode.getAsString();
    if (stringValue == null) return  null;
    return OffsetDateTime.parse(stringValue);
}

which references the method OffsetDateTime.parse(CharSequence) in the class java.time.OffsetDateTime, which is:

public  static  OffsetDateTime parse(CharSequence text) {
    return parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

which uses DateTimeFormatter.ISO_OFFSET_DATE_TIME to parse the string. The javadoc of this formatter says:

The ISO date-time formatter that formats or parses a date-time with an offset, such as '2011-12-03T10:15:30+01:00'. This returns an immutable formatter capable of formatting and parsing the ISO-8601 extended offset date-time format. The format consists of:

  • The ISO_LOCAL_DATE_TIME
  • The offset ID. If the offset has seconds then they will be handled even though this is not part of the ISO-8601 standard. The offset parsing is lenient, which allows the minutes and seconds to be optional.

The returned formatter has a chronology of ISO set to ensure dates in other calendar systems are correctly converted. It has no override zone and uses the ResolverStyle.STRICT resolver style.

meaning that the parser used expects a time-zone after the datetime (either with a 'Z' at the end, for UTC, or with a +/-HH:mm, for other timezones).

The exception is thrown when a datetime without timezone is parsed with this parser.

In my experience (basically using the onedrive APIs), the datetimes for DriveItem objects are correctly parsed as they are in the correct format, while the datetimes in error messages are not parsed. Here it is the typical response when asking for a DriveItem (https://graph.microsoft.com/v1.0/me/drive/root):

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('48d31887-5fad-4d73-a9f5-3c356e68a038')/drive/root/$entity",
    "createdDateTime": "2017-07-27T02:41:36Z",
    "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ",
    "lastModifiedDateTime": "2024-02-11T15:25:37Z",
    "name": "root",
    "webUrl": "https://m365x214355-my.sharepoint.com/personal/meganb_m365x214355_onmicrosoft_com/Documents",
    "size": 106329756,
    "parentReference": {
        "driveType": "business",
        "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd"
    },
    "fileSystemInfo": {
        "createdDateTime": "2017-07-27T02:41:36Z",
        "lastModifiedDateTime": "2024-02-11T15:25:37Z"
    },
    "folder": {
        "childCount": 38
    },
    "root": {}
}

where all the datetimes have the timezone (Z, for UTC), while here it is the response when asking for an unexisting DriveItem (https://graph.microsoft.com/v1.0/me/drive/items/42), i.e. an error:

{
    "error": {
        "code": "itemNotFound",
        "message": "The resource could not be found.",
        "innerError": {
            "date": "2024-02-14T08:55:18",
            "request-id": "a54dacf0-e37e-4669-b2b5-371591bd77e7",
            "client-request-id": "a54dacf0-e37e-4669-b2b5-371591bd77e7"
        }
    }
}

where you can see the date misses the timezone.

Workaround

I haven't come up with any, actually

Possible solutions

Either the remote services return always a datetime with timezone or the java API use the DateTimeFormatter.ISO_OFFSET_DATE_TIME when parsing timezoned datetimes and DateTimeFormatter.ISO_LOCAL_DATE_TIME when parsing non-timezoned datetimes.

Ndiritu commented 9 months ago

Closing this as it's related to https://github.com/microsoftgraph/msgraph-sdk-java/issues/1785

Fix has been merged and should be released later today.

nagendrakamisetti commented 9 months ago

Hi @Ndiritu ,

Release means we will get version 6.1.1 in mvn repository right.

image

Please confirm.

Ndiritu commented 9 months ago

@nagendrakamisetti this is now fixed in the 6.2.1 release