'Outlook Calendar .Net Core Web API GetList (tokenCredential parameter cannot be null. (Parameter 'tokenCredential'))

I have an API project related to Outlook calendar integration. With this project, you can access your outlook account. I want to list all the events in the calendar part after accessing it. I looked at your transcript, but there were parts I didn't understand. How can I get this list? This document but I don't understand: https://docs.microsoft.com/en-us/graph/api/user-list-calendars?view=graph-rest-1.0&tabs=csharp

appsettings.json (Authorize)

  {
   "AzureAd": {
   "Instance": "https://login.microsoftonline.com/",
   "Domain": "outlook.com.tr",
   "TenantId": "*********************************",
   "ClientId": "*********************************"
  },
   "Logging": {
   "LogLevel": {
   "Default": "Information",
   "Microsoft": "Warning",
   "Microsoft.Hosting.Lifetime": "Information"
    }
  },
 "AllowedHosts": "*"

}

startup.cs

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Threading.Tasks;
  using Microsoft.AspNetCore.Authentication.JwtBearer;
  using Microsoft.AspNetCore.Builder;
  using Microsoft.AspNetCore.Hosting;
  using Microsoft.AspNetCore.HttpsPolicy;
  using Microsoft.AspNetCore.Mvc;
  using Microsoft.Extensions.Configuration;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.Logging;
  using NSwag;
  using NSwag.AspNetCore;
  using NSwag.Generation.Processors.Security;

namespace EvetOutlookAPI
{
   public class Startup
   {
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the 
     container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        // Enable JWT Bearer Authentication
        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(options =>
        {
            Configuration.Bind("AzureAd", options);
            // Authority will be Your AzureAd Instance and Tenant Id
            options.Authority = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/v2.0";

            // The valid audiences are both the Client ID(options.Audience) and api://{ClientID}
            options.TokenValidationParameters.ValidAudiences = new string[] { Configuration["AzureAd:ClientId"], $"api://{Configuration["AzureAd:ClientId"]}" };
        });

        AddSwagger(services);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });

        // Add Swagger UI
        app.UseOpenApi();
        app.UseSwaggerUi3(settings =>
        {
            settings.OAuth2Client = new OAuth2ClientSettings
            {
                // Use the same client id as your application.
                // Alternatively you can register another application in the portal and use that as client id
                // Doing that you will have to create a client secret to access that application and get into space of secret management
                // This makes it easier to access the application and grab a token on behalf of user
                ClientId = Configuration["AzureAd:ClientId"],
                AppName = "Swagger-UI-Client",
            };
        });
    }

    /// <summary>
    /// Function to Generate Swagger UI Document and authenticate Swagger UI against the Azure Ad Application
    /// </summary>
    /// <param name="services"></param>
    private void AddSwagger(IServiceCollection services)
    {
        services.AddOpenApiDocument(document =>
        {
            document.AddSecurity("bearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme
            {
                Type = OpenApiSecuritySchemeType.OAuth2,
                Description = "Azure AAD Authentication",
                Flow = OpenApiOAuth2Flow.Implicit,
                Flows = new OpenApiOAuthFlows()
                {
                    Implicit = new OpenApiOAuthFlow()
                    {
                        Scopes = new Dictionary<string, string>
                    {
                        { $"api://{Configuration["AzureAd:ClientId"]}/Calendars.ReadWrite", "Access Application" },
                    },
                        AuthorizationUrl = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/v2.0/authorize",
                        TokenUrl = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/v2.0/token",
                    },
                },
            });

            // To add bearer token in request to APIs with Authorize attribute
            document.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("bearer"));
        });
    }
}

}

I use the Microsoft.Graph library for these operations.

This is the part I want to list.

CalendarController

    [Authorize]
    [HttpGet("api/Get/Calendar")]
    public async Task<IActionResult> Get()
    {
        GraphServiceClient graphClient = new GraphServiceClient(tokenCredential: null);

        var calendars = await graphClient.Me.Calendars
                .Request()
                .GetAsync();

        return (IActionResult) calendars;
      }
    }

What I'm wondering is: How can I make an endpoint that returns the calendar list from Outlook?

enter image description here enter image description here



Solution 1:[1]

For setting Swagger to give authentication, I followed this blog and set it successfully in my side, here's my code, and I installed <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> for using Swagger.

using Microsoft.Identity.Web;

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(opts =>
    {
        opts.Filters.Add(typeof(SampleActionFilter));
    });
    services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");
    services.AddSwaggerGen(c =>
    {
        c.OperationFilter<CustomHeaderSwaggerAttribute>();
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication1", Version = "v1" });
        c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.OAuth2,
            Flows = new OpenApiOAuthFlows()
            {
                Implicit = new OpenApiOAuthFlow()
                {
                    AuthorizationUrl = new Uri(Configuration["AuthorizationUrl"]),
                    TokenUrl = new Uri(Configuration["TokenUrl"]),
                    Scopes = new Dictionary<string, string> {
                {
                    Configuration["ApiScope"], "read the api"
                }
            }
                }
            }
        });
        c.AddSecurityRequirement(new OpenApiSecurityRequirement() {
                {
                    new OpenApiSecurityScheme {
                        Reference = new OpenApiReference {
                                Type = ReferenceType.SecurityScheme,
                                    Id = "oauth2"
                            },
                            Scheme = "oauth2",
                            Name = "oauth2",
                            In = ParameterLocation.Header
                    },
                    new List < string > ()
                }
            });
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApi v1");
            c.OAuthClientId(Configuration["OpenIdClientId"]);
            c.OAuthUseBasicAuthenticationWithAccessCodeGrant();
        });
    }
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}


"AuthorizationUrl": "https://login.microsoftonline.com/tenant_name.onmicrosoft.com/oauth2/v2.0/authorize",
"TokenUrl": "https://login.microsoftonline.com/tenant_name.onmicrosoft.com/oauth2/v2.0/token",
"ApiScope": "api://clientid_of_the_app_exposed_api/User.Read",
"OpenIdClientId": "azure_ad_clientid",
"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "azure_ad_clientid",
    "Domain": "tenant_name.onmicrosoft.com",
    "TenantId": "common",
    "Audience": "clientid_of_the_app_exposed_api"
}

enter image description here

=========================================================

In my humble opinion, your requirement should be creating an api in your project and then when users calling this api, the api will call ms graph api on behalf of the user to get calendar lists. The code and screenshot you provided is mostly related to swagger which is used for test, but I think we'd better to make your api work first.

This is my api method, when I send a request to this api with a token which can be used to call graph api to get all calendars, it can work for me.

public class CalendarController : ControllerBase
{
    [HttpGet]
    //[Authorize]
    //[RequiredScope("Calendars.ReadWrite")]
    public async Task<IUserCalendarsCollectionPage> Get()
    {
        string header = HttpContext.Request.Headers["Authorization"];
        string token = header.Remove(0,7);
        GraphServiceClient graphClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", new DelegateAuthenticationProvider(async (requestMessage) =>
        {
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
        }));
        var calendars = await graphClient.Me.Calendars
                .Request()
                .GetAsync();
        return calendars;
                }
}

Then if you want to test this api with Swagger, I haven't finished that part.

The next, I think it's not good to do so... Normally, when we host an api which used to call graph api, we should use on-behalf-flow, which means you need to expose an api in azure ad, then you can send a request with a legal access token to your controller, then the token will be vailated and be used to generate another access token by on-behalf-flow to call graph api. It looks to bring much trouble...

Another way is that you have your own authentication method which will validate if the incoming request is legal, if it's legal, then decode the incoming token to get the princple and get the use id, then you can use client credential flow to generate access token to call the graph api alone with the use id.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1