Using Microsoft Graph API in ASP.NET Core

This post shows how Microsoft Graph API can be used in both ASP.NET Core UI web applications and also ASP.NET Core APIs for delegated identity flows. The ASP.NET Core applications are secured using Microsoft.Identity.Web. In the API project, the Graph API client is used in a delegated flow with user access tokens getting an access token for the graph API on behalf of the identity created from the access token used to request the API.

Code: https://github.com/damienbod/AspNetCoreUsingGraphApi

Using Graph API from an ASP.NET Core UI application

Using the Graph API client in an ASP.NET Core UI web application can be implemented using the Microsoft.Identity.Web.MicrosoftGraph nuget package. This can be added to the project file as well as the Azure authentication packages. If using the beta version, switch to the Microsoft.Identity.Web.MicrosoftGraphBeta nuget package.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>c27d164f-2839-4f2b-a533-da54a470d29a</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Identity.Web" Version="1.3.0" />
    <PackageReference Include="Microsoft.Identity.Web.UI" Version="1.3.0" />
    <PackageReference Include="Microsoft.Identity.Web.MicrosoftGraphBeta" Version="1.3.0" />
  </ItemGroup>
  
</Project>

The application authentication and the authorization are setup in the Startup class. The AddMicrosoftGraph method is used to add the required scopes for your Graph API calls. The AddMicrosoftIdentityWebAppAuthentication method is used in the UI ASP.NET Core application.

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

	services.AddHttpClient();
	services.AddScoped<GraphApiClientUI>();
	services.AddScoped<ApiService>();

	services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
		.AddMicrosoftGraph(
			Configuration["DownstreamApi:BaseUrl"],
			Configuration.GetValue<string>("DownstreamApi:Scopes"))
		.AddInMemoryTokenCaches();

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

	services.AddRazorPages();
}

The GraphServiceClient service can be added directly in the services and used to access the Graph API. This is really simple in a UI application and you don’t need to handle token requests or anything else like this, all is implemented in the Microsoft.Identity.Web packages. But if you require special scopes or would like to handle this yourself, this is possible and the GraphServiceClient instance can be created as shown below.

public class GraphApiClientUI
{
	private readonly GraphServiceClient _graphServiceClient;

	public GraphApiClientUI(ITokenAcquisition tokenAcquisition,
		GraphServiceClient graphServiceClient)
	{
		_graphServiceClient = graphServiceClient;
	}

	public async Task<User> GetGraphApiUser()
	{
		return await _graphServiceClient.Me.Request()
			.GetAsync().ConfigureAwait(false);
	}

The Graph API data can be returned in the UI views of the ASP.NET Core application.

[Authorize]
public class HomeController : Controller
{
	private readonly GraphApiClientUI _graphApiClientUI;

	public HomeController(GraphApiClientUI graphApiClientUI)
	{
		_graphApiClientUI = graphApiClientUI;
	}

	[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
	public async Task<IActionResult> Index()
	{
		var user = await _graphApiClientUI.GetGraphApiUser()
			.ConfigureAwait(false);

		ViewData["ApiResult"] = user.DisplayName;

		return View();
	}

Using Graph API from an ASP.NET Core API

Using Graph API from an ASP.NET Core API application is different to a UI application. The Graph API is called on behalf of the identity created from the access token calling the API. This is a delegated user access token. The Azure AD client security for the API can be setup using the AddMicrosoftIdentityWebApiAuthentication method.

public void ConfigureServices(IServiceCollection services)
{
	services.AddHttpClient();
	services.AddScoped<GraphApiClientDirect>();

	services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi()
		.AddInMemoryTokenCaches();

	services.AddControllers(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			// .RequireClaim("email") 
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	});

	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new OpenApiInfo { 
			Title = "WebAPiUsingGraphApi", Version = "v1" });
	});
}

In the service responsible for implementing the Graph API client, the ITokenAcquisition interface is required as well as the IHttpClientFactory interface. When creating new instances of the GraphServiceClient, the IHttpClientFactory interface is used to create the HttpClient which is used by the Graph API client.

public class GraphApiClientDirect
{
	private readonly ITokenAcquisition _tokenAcquisition;
	private readonly IHttpClientFactory _clientFactory;

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

A new access token is requested for the required scopes using the GetAccessTokenForUserAsync method. This returns a delegated access token and the token is then used in the DelegateAuthenticationProvider. The GraphServiceClient client is created using the HttpClient which was created using the IHttpClientFactory _clientFactory. The lifecycle of the HttpClients are handled correctly then.

private async Task<GraphServiceClient> GetGraphClient(string[] scopes)
{
	var token = await tokenAcquisition.GetAccessTokenForUserAsync(
	 scopes).ConfigureAwait(false);

	var client = _clientFactory.CreateClient();
	client.BaseAddress = new Uri("https://graph.microsoft.com/beta");
	client.DefaultRequestHeaders.Accept.Add(
		new MediaTypeWithQualityHeaderValue("application/json"));

	GraphServiceClient graphClient = new GraphServiceClient(client)
	{
		AuthenticationProvider = new DelegateAuthenticationProvider(
		async (requestMessage) =>
		{
			requestMessage.Headers.Authorization = 
				new AuthenticationHeaderValue("bearer", token);
		})
	};

	graphClient.BaseUrl = "https://graph.microsoft.com/beta";
	return graphClient;
}

The Graph API client can then be used to request data from Azure Microsoft Graph API.

public async Task<User> GetGraphApiUser()
{
	var graphclient = await GetGraphClient(
		new string[] { "User.ReadBasic.All", "user.read" })
	   .ConfigureAwait(false);

	return await graphclient.Me.Request()
	   .GetAsync().ConfigureAwait(false);
}

This could also be used to request files from Sharepoint or any other resource made available through the Graph rest APIs.

public async Task<string> GetSharepointFile()
{
	var graphclient = await GetGraphClient(
		new string[] { "user.read", "AllSites.Read" }
	).ConfigureAwait(false);

	var user = await graphclient.Me.Request().GetAsync().ConfigureAwait(false);

	if (user == null)
		throw new NotFoundException($"User not found in AD.");

	var sharepointDomain = "damienbodtestsharing.sharepoint.com";
	var relativePath = "/sites/TestDoc";
	var fileName = "aad_ms_login_02.png";

	var site = await graphclient
		.Sites[sharepointDomain]
		.SiteWithPath(relativePath)
		.Request()
		.GetAsync().ConfigureAwait(false);

	var drive = await graphclient
		.Sites[site.Id]
		.Drive
		.Request()
		.GetAsync().ConfigureAwait(false);

	var items = await graphclient
		.Sites[site.Id]
		.Drives[drive.Id]
		.Root
		.Children
		.Request().GetAsync().ConfigureAwait(false);

	var file = items
		.FirstOrDefault(f => f.File != null && f.WebUrl.Contains(fileName));

	var stream = await graphclient
		.Sites[site.Id]
		.Drives[drive.Id]
		.Items[file.Id].Content
		.Request()
		.GetAsync().ConfigureAwait(false);

	var fileAsString = StreamToString(stream);
	return fileAsString;
}

The Graph API client service can then be used in the API which is protected using the Microsoft.Identity.Web packages.

[Authorize]
[ApiController]
[Route("[controller]")]
public class GraphCallsController : ControllerBase
{
	private readonly GraphApiClientDirect _graphApiClientDirect;

	public GraphCallsController(GraphApiClientDirect graphApiClientDirect)
	{
		_graphApiClientDirect = graphApiClientDirect;
	}

	[HttpGet]
	public async Task<string> Get()
	{
		var user = await _graphApiClientDirect.GetGraphApiUser()
			.ConfigureAwait(false);

		return user.DisplayName;
	}

}

When using the Graph API client in ASP.NET Core applications for delegated access user access tokens, the correct initialization should be used. If creating the instance yourself, use the IHttpClientFactory to create the HttpClient instance used in the client. If you create Graph API requests using different scopes, you would also need to use the ITokenAcquisition and the IHttpClientFactory interfaces to create the GraphApiClient.

Links:

https://developer.microsoft.com/en-us/graph/

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient

2 comments

  1. […] Using Microsoft Graph API in ASP.NET Core (Damien Bowden) […]

  2. […] Using Microsoft Graph API in ASP.NET Core – Damien Bowden […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: