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
  • 105,241
  • 11
  • 83
  • 163
Lukas
  • 1,101
  • 1
  • 9
  • 27

1 Answers1

0

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 aquire access token on behalf of application. You cannot consume this service as constructor variable.

Solution:

Instead of that you should use ITokenAcquisition service as 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

Hope it would resolve your problem accordingly.

Md Farid Uddin Kiron
  • 9,849
  • 3
  • 14
  • 34
  • 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