0

I'm attempting to build an application that can read from Microsoft Graph both as the user and as the application. For example, I need to call https://graph.microsoft.com/beta/me/calendar/events as the logged in user to get their events for the day. However, I also need the application itself to fetch all of the events for all of the conference rooms using https://graph.microsoft.com/beta/users/confRoom@mydomain.com/events.

I have successfully created the application in Azure AD and provided the appropriate Graph permissions to both delegates and applications as well as approved them for my tenant.

enter image description here

I have also used Microsoft.Identity builder configuration in Program.cs to successfully connect to my tenant and fetch my personal Graph information.

However, what I'd like to do now is be able to inject two graph connections. One for the application and one for the user. That way, when my application needs to fetch data as the application, I can call one Graph connection, and when it needs to fetch data as the user, I can call the other Graph connection.

Is it possible to configure and inject two Graph connectors, one as the user and the other as the application, in the same application?

Tiny Wang
  • 10,423
  • 1
  • 11
  • 29
Randy Slavey
  • 544
  • 4
  • 19
  • Q: I'm attempting to build an application that can read from Microsoft Graph both as the user and as the application. A: That typically means you write two applications, one with a UI (e.g. a WPF or WinForms .exe, or an ASP.NET web app), and the other a "headless" web app (e.g. an ASP.Net controller). Of course, you can include both controllers and UI pages in the same physical ASP.Net app/project, but logically they're "separate and distinct". – paulsm4 May 26 '22 at 23:33
  • is there any progress? – Tiny Wang May 31 '22 at 01:30
  • Alas, I'm on vacation. Looking forward to testing your solution next week. – Randy Slavey Jun 01 '22 at 02:03
  • @paulsm4 alas, there was a business case for both. I needed the user to auth as themselves to even get into the application and to fetch their /me/ graph data, but there are certain items that Graph only exposes at the application level (e.g. Calendars) for any user that is not /me (i.e., I can't see a person's calendar using delegation without being added as an Exchange delegate to their mailbox.) App-level permissions allow this. – Randy Slavey Jun 06 '22 at 23:07

2 Answers2

2

It's possible I think.

Firstly, when we need the web app to have the ability to call graph api on behalf of a user(using https://graph.microsoft.com/beta/me), we need to integrate microsoft authentication module so that uses can sign in first then app can know who is me.

Then in asp.net core application, microsoft provide graph sdk for helping calling ms graph api.

Now we need to integrate ms sign in module and graph sdk into your asp.net core app, you can refer to this sample or my code(based on asp.net core MVC 5) below.

Adding login in partial view page named _LoginPartial.cshtml and add it into _layout.cshtml:

@using System.Security.Principal

<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
        <li class="nav-item">
            <span class="navbar-text text-dark">Hello @User.Identity.Name!</span>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
        </li>
}
else
{
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
        </li>
}
</ul>

enter image description here

Then add configurations in appsettings.json:

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "your_tenant_name.onmicrosoft.com",
    "TenantId": "tenant_id",
    "ClientId": "azure_ad_app_client_id", 
    "ClientSecret": "client_secret", 
    "CallbackPath": "/signin-oidc",//you need to add redirect url in azure portal->azure ad->your app->authentication->web platform->add redirect url like https://localhost:44321/signin-oidc
    "SignedOutCallbackPath ": "/signout-callback-oidc"
  }

Then modify Startup.cs, don't forget adding app.UseAuthentication();

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Authorization;
public void ConfigureServices(IServiceCollection services)
{
    services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
        .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" })
        .AddMicrosoftGraph(options =>
        {
            options.Scopes = string.Join(' ', new string[] { "user.read" });
        })
        .AddInMemoryTokenCaches();

    // Require authentication
    services.AddControllersWithViews(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    })
    // Add the Microsoft Identity UI pages for signin/out
    .AddMicrosoftIdentityUI();
}

Then in the controller, update it like this, you can call graph api with /me endpoint.

using Microsoft.AspNetCore.Authorization;
using Microsoft.Graph;
using Azure.Identity;

[Authorize]
public class HomeController : Controller
{
    private readonly GraphServiceClient _graphClient;
    public HomeController(GraphServiceClient graphClient)
    {
        _graphClient = graphClient;
    }

    public async Task<IActionResult> IndexAsync()
    {
        var me = await _graphClient.Me.Request().GetAsync();
        ViewBag.Myname = me.DisplayName;
        return View();
    }
}

Then when you want the app to call graph api on behalf of the application itself, we need to use client credential flow. So here's my sample, just modify the controller method:

public async Task<IActionResult> IndexAsync()
{
    var me = await _graphClient.Me.Request().GetAsync();
    ViewBag.Myname = me.DisplayName;

    var scopes = new[] { "https://graph.microsoft.com/.default" };
    var tenantId = "your_tenant_name.onmicrosoft.com";
    var clientId = "azure_ad_client_id";
    var clientSecret = "client_secret";
    var clientSecretCredential = new ClientSecretCredential(
        tenantId, clientId, clientSecret);
    var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
    var user = await graphClient.Users["tinytest@your_tenant_name.onmicrosoft.com"].Request().GetAsync();
    ViewBag.Username = user.DisplayName;
    return View();
}

The nuget packages I installed:

<PackageReference Include="Microsoft.Graph" Version="4.19.0" />
    <PackageReference Include="Microsoft.Identity.Web" Version="1.24.1" />
    <PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.24.1" />
    <PackageReference Include="Microsoft.Identity.Web.UI" Version="1.24.1" />

Test result:

enter image description here

Tiny Wang
  • 10,423
  • 1
  • 11
  • 29
  • I went through your solution, but I wanted to use DI, and your solution for the application graph service client couldn't be configured as DI if there was already an existing one. Turns out it was all unnecessary, anyway. See my answer. – Randy Slavey Jun 06 '22 at 23:00
0

After numerous attempts at varying ways to inject two graph clients, it turns out there's a built-in method for it.

        var me = await graphServiceClient.Me.Request().GetAsync();
        var otherUser = await graphServiceClient.Users["test.user@domain.com"].Request().WithAppOnly().GetAsync();

The key method there is "WithAppOnly()", which says to fetch from Graph using the app permissions not the delegated user permissions. The only scopes I had to add were "profile user.read.all", with "profile" being delegated and "user.read.all" being an application permissions:

enter image description here

Edit: Found solution here: How to dependency inject Microsoft Graph client in ASP.Net Core 6 Web Api

Randy Slavey
  • 544
  • 4
  • 19