Search code examples
c#asp.net-mvcazuremicrosoft-graph-apiazure-ad-graph-api

Microsoft Graph Api GET request can't deserialize JSON obect when calling Applications


I'm making a GET request call to get back all application with their Id, and password expiration date. I've been working with the Graph Explore that Microsoft offers and has been a lot of help. The code I have gets the http request back and then It try's to deserialize it blows up with the given error below. I've spent a good while on SOF looking at similar post but nothing seems to work. I feel I might be missing something simple here or going about it the wrong way.

I'm also stuck on an older version of the Graph Nugget pkg v4.41.0 and Core is at v2.0.13. I went to update them and there were a lot of breaking changes thru out the app and I don't have time to rewrite everything yet.

I get the following error:

"Message": "An error has occurred.", 
"ExceptionMessage": "Cannot deserialize the current JSON object (e.g. {\"name\":\"value\"}) into type 'System.Collections.Generic.List`1[Microsoft.Graph.Application]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.\r\nTo fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.\r\nPath '['@odata.context']', line 1, position 18.",
"ExceptionType": "Newtonsoft.Json.JsonSerializationException",
"StackTrace": "   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)\r\n   at Newtonsoft.Json.Serialization...
        public async Task<List<Microsoft.Graph.Application>> GetAppExpList()
        {
           List<Microsoft.Graph.Application> apps = null;                       

           // query string for url call
           var url = this.GetGraphUrl($"{Consts.GraphUrl}?$select=id,passwordCredentials&$format=json");
           
           // tried to do this with the built in Graph methods
           List<QueryOption> options = new List<QueryOption>
           {
            new QueryOption("$select", "id,DisplayName,passwordCredentials"), 
            new QueryOption("$format", "json")
           };

           // Here I wanted to see what was brought back. No PasswordCreds for some reason 
           var test = await _graphClient.Applications.Request(options).GetAsync();


          // GET request to the Applications Graph Api
          var response = await this.HttpHandler.SendGraphGetRequest(url);            
        
          // Returns JSON data 
          if (response.IsSuccessStatusCode)
          {
            // Shows the correct data in JSON Format
            var rawData = await response.Content.ReadAsStringAsync();

            // Throws error above.    
            apps = JsonConvert.DeserializeObject<List<Microsoft.Graph.Application>>(rawData);
                            
          }

         return apps; 
    }

Here is the JSON that comes back for the var rawData = await response.Content.ReadAsStringAsync();

{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#applications(id,passwordCredentials)"
 ,"value":[
 {
    "id":"00000000-0000-0000-0000-000000000000",
    "displayName":"App 1 name Here",
    "passwordCredentials":[]
 },
 {
    "id":"00000000-0000-0000-0000-000000000001",
    "displayName":" App 2 name here",
    "passwordCredentials":[]
 },
 {
    "id":"00000000-0000-0000-0000-000000000002",
    "displayName":"App 3 name here",
    "passwordCredentials":[
       {
        "customKeyIdentifier":null,
        "displayName":"secret",
        "endDateTime":"2025-01-30T14:46:40.985Z",
        "hint":"oHI",
        "keyId":"00000000-0000-0000-0000-0000000000",
        "secretText":null,
        "startDateTime":"2023-01-31T14:46:40.985Z"
        }
     ]
  }
]

}


Solution

  • After trying the same by myself and investigating your code, it seems you're trying too much. The SDK already does the deserialization for you and you just can work with properties from objects. This code example will return all applications:

    using Azure.Core;
    using Microsoft.Graph;
    using Microsoft.Identity.Client;
    using Microsoft.Identity.Client.Extensibility;
    
    namespace ConsoleApp
    {
        public static class Program
        {
            private static readonly string ClientId = "{your client id}";
            private static readonly string[] Scopes = new[] { "https://graph.microsoft.com/.default" };
    
            // Replace with your tenant id, if it is a single tenant app.
            private static readonly string TenantId = "common";
    
            private static AccessToken _cachedToken;
    
            public static async Task Main()
            {
                var credential = DelegatedTokenCredential.Create((_, _)
                    => throw new NotSupportedException(),
                    GetOrRequestAccessToken);
    
                var graphClient = new GraphServiceClient(credential, Scopes);
    
                var options = new[]
                {
                    new QueryOption("$select", "id,displayName,passwordCredentials"),
                };
    
                var appsFound = await graphClient.Applications.Request(options).GetAsync();
    
                foreach (var app in appsFound)
                {
                    Console.WriteLine($"Id: {app.Id}");
                    Console.WriteLine($"Display Name: {app.DisplayName}");
    
                    foreach (var password in app.PasswordCredentials)
                    {
                        Console.WriteLine($"   Key Id: {password.KeyId}");
                        Console.WriteLine($"   Display Name: {password.DisplayName}");
                        Console.WriteLine($"   Start: {password.StartDateTime}");
                        Console.WriteLine($"   End: {password.EndDateTime}");
                        Console.WriteLine($"   Hint: {password.Hint}");
                    }
    
                    Console.WriteLine();
                }
    
            }
    
            private static async ValueTask<AccessToken> GetOrRequestAccessToken(TokenRequestContext _, CancellationToken __)
            {
                if (_cachedToken.ExpiresOn <= DateTimeOffset.UtcNow)
                {
                    var app = PublicClientApplicationBuilder.Create(ClientId)
                        .WithExperimentalFeatures()
                        .WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
                        .WithRedirectUri("http://localhost")
                        .Build();
    
                    var result = await app.AcquireTokenInteractive(Scopes)
                        .OnBeforeTokenRequest(request =>
                        {
                            request.Headers.Add("Origin", "http://localhost");
                            return Task.CompletedTask;
                        })
                        .ExecuteAsync();
    
                    _cachedToken = new AccessToken(result.AccessToken, result.ExpiresOn);
                }
    
                return _cachedToken;
            }
        }
    }
    

    If you like to retrieve the SecretText value from the Graph SDK, you must know, that this is not possible according to the documentation:

    secretText: Contains the strong passwords generated by Microsoft Entra ID that are 16-64 characters in length. The generated password value is only returned during the initial POST request to addPassword. There is no way to retrieve this password in the future.