0

I'm working on figuring out how to use Microsoft Graph API in a ASP.NET Core 3.1 Razor Pages application. I found this guide and got most of it to work (along with retrieving a token) until I realized I need to get access to the API without a user.

At the moment, I am stuck because I am not able to retrieve a token using the ITokenAcquisition GetAccessTokenForAppAsync method. It keeps resulting in a NullReferenceException. I don't know if my startup setup is wrong or what, but I can't figure it out.

System.NullReferenceException: 'Object reference not set to an instance of an object.'

I'm aware of the Get access without a user guide which I understand and can get to work, but I specifically want to use GetAccessTokenForAppAsync method because it will manage refreshing tokens for me. Otherwise, I'd have to keep querying for a new token with every API call and constantly generating valid tokens seems like a bad idea.

Startup.cs ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    ...
    
    services.AddHttpClient();
    services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
        .EnableTokenAcquisitionToCallDownstreamApi()
        // Use in-memory token cache
        // See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization
        .AddInMemoryTokenCaches();
        
    ...

}

Index.cshtml.cs. This is where I make my call to get the token:

public class IndexModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly ITokenAcquisition _tokenAcquisition;

    public IndexModel(IHttpClientFactory clientFactory, ITokenAcquisition tokenAcquisition)
    {
        _clientFactory = clientFactory;
        _tokenAcquisition = tokenAcquisition;
    }

    public async Task OnGet()
    {
        // results in NullReferenceException
        string token = await _tokenAcquisition.GetAccessTokenForAppAsync("https://graph.microsoft.com/.default", tenant:"tenantId");
    }
}

appSettings.json. The values are populated by user secrets json.

{
  "AzureAd": {
    "Instance": "",
    "ClientId": "",
    "TenantId": "",
    "CallbackPath": "",
    "ClientSecret": "",
    "TimeoutInMinutes": ""
  },
  "ConnectionStrings": {
    "DefaultConnection": ""
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
Lukas
  • 1,699
  • 1
  • 16
  • 49

1 Answers1

1

First of all, I have successfully reproduced your issue, as you can see below:

enter image description here

You are getting this because of private readonly ITokenAcquisition _tokenAcquisition;

Note: This is actually a service which helps you to acquire access token on behalf of application. You cannot consume this service as constructor variable.

Solution:

Instead of that you should use ITokenAcquisition service in the below way:

   public async Task OnGet()
        {
            var _tokenAcquisition = this.HttpContext.RequestServices
                 .GetRequiredService<ITokenAcquisition>() as ITokenAcquisition;
            string token = await _tokenAcquisition.GetAccessTokenForAppAsync("https://graph.microsoft.com/.default", tenant: "tenantId");
        }

Configuration Under Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');

    services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
        .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
        .AddInMemoryTokenCaches();

    services.AddControllersWithViews(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    });
    services.AddRazorPages()
            .AddMicrosoftIdentityUI();
}

DownstreamApi Under appsettings.json:

"DownstreamApi": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "https://graph.microsoft.com/.default"
  }

Output:

enter image description here

Note: Make sure, you have included app.UseAuthentication(); in program.cs or startup.cs file in order to get expected result.

Md Farid Uddin Kiron
  • 16,817
  • 3
  • 17
  • 43
  • Thank you for your answer. Unfortunately, this did not work for me. I am still experiencing the same issue. Maybe you set your startup differently? Or maybe I am missing something from registration of the app in Azure... – Lukas Aug 27 '21 at 20:00
  • Would you kindly share your Startup.cs. Additionally did you disabled your ITokenAquisition from services register? – Md Farid Uddin Kiron Aug 28 '21 at 01:01
  • Thank you for the reaponse. You can see the relevant part of my startup.cs in the original post. What is this I am supposed to disable? Is it the `EnableTokenAcquisitionToCallDownstreamApi()` method? – Lukas Aug 29 '21 at 13:09
  • No bro, implementation of it `ITokenAcquisition` I doubt this might cause that problem at this moment `public IndexModel(IHttpClientFactory clientFactory, ITokenAcquisition tokenAcquisition) { _clientFactory = clientFactory; _tokenAcquisition = tokenAcquisition; }` as you can see the one working exactly what you are looking for. So would you kindly test with a new controller with my sample so that you can understand where the problem is. Feel free to share any further problem. – Md Farid Uddin Kiron Aug 29 '21 at 13:17
  • I have updated the answer with the startup.cs configuration file as per your current concern if that help. Have a try. – Md Farid Uddin Kiron Aug 29 '21 at 14:09
  • Thank you again. Your solution worked. It was not a problem with constructor injection of `ITokenAcquisition`. Instead, startup was different and I did not have `.AddAuthentication`. This solution solves my question and I have accepted it. However, I forgot to explicitly state in my question that I'd like to get this to work without forcing a user to sign in. A user is required to be signed in for this to work. Do you know how to avoid that? – Lukas Aug 30 '21 at 13:53
  • If that is the case then need to change the authentication protocol, need client credentials protocol instead of this token flow. – Md Farid Uddin Kiron Aug 30 '21 at 14:03
  • Ok, I think I was wrong. It actually **doesn't** require the user to be signed in, but for some reason the set up needs `.AddAuthentication` in startup (and, of course, `app.UseAuthentication();`). Maybe it is for authenticating the the app with the API instead of the user? This is all a bit confusing, but it does work so I will continue. As I figure this out along the way, I'll likely post another answer. – Lukas Aug 30 '21 at 14:14
  • Yes there are two kind of permission required one on behalf of user another one is on behalf of app. Means anyone from this app can access the resources. – Md Farid Uddin Kiron Aug 30 '21 at 14:36
  • I think this answer needs to be updated, because it misleads the reader into thinking that it was the dependency injection what was wrong (which seemed really odd to me), but after reading Lukas comment, adding AddAuthentication is what actually solved the problem. – JACH May 04 '23 at 17:56
  • Thanks for your response, updated accordingly. – Md Farid Uddin Kiron May 05 '23 at 00:33