Azure Cloud
.NET Core Angular Azure AD C# MS Graph OAuth OData SPA Visual Code Visual Studio Web API

How to Call an MS Graph API from a Web API

Welcome to today’s post.

In today’s post I will be discussing how we can call a custom Web API that accesses Microsoft Graph API resources.

In some of my recent previous posts I discussed and demonstrated the following ways we can use the Microsoft Identity platform and its libraries to authenticate access to our Microsoft 365 resources using the MS Graph API.

  1. I showed how to use the identity platform within an Angular SPA application to login to our Microsoft account using OAuth2 two phase authentication, where first obtained an access token by authenticating into our account, then consenting to authorize delegated scopes to access our account resources.
  2. I then showed how to use a Web API method to access an MS Graph calendar event resource using the same two-phase OAuth authentication.

Rather than make calls and acquire authentication token from within a .NET Core Web API, which is server side, we can take the approach of obtaining an access token from our Angular application, store it, then when we make an HTTP request to our Web API method. The access token will give us sufficient access to the AD resource as we have setup the client id, scopes and acquired the token using MS identity.

The following architectural diagram shows what I have just discussed and the end goal:

MS Graph Web API

The OAuth settings in our Angular SPA client as shown:

export const OAuthSettings = {
  appId: ‘your client-app-id',
  redirectUri: 'http://localhost:4200',
  authority: 'https://login.microsoftonline.com/consumers/',
  validateAuthority: true,
  scopes: [
    "user.read",
    "mailboxsettings.read",
    "calendars.readwrite"
  ]
};

Setting up the Web API URL reference within our Angular application can be done using the environment constant:

export const environment = {
  production: false,
  graphCustomCalendarApiUrl: "http://localhost:44311/api",
};

Then we inject the API configuration settings including the API URL to our application classes using an injection token:

import { InjectionToken } from '@angular/core';
import { environment } from '../environments/environment';

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

export class AppConfig {
    graphCustomCalendarAPI: string;
    maxRowsPerPage: string;
    title: string;
}

export const CALENDAR_DI_CONFIG: AppConfig = {
    graphCustomCalendarAPI: environment.graphCustomCalendarApiUrl,
    maxRowsPerPage: "20",
    title: "Angular Web API Graph Demo App"
};

To be able to use two-phase token acquisition from the Microsoft Identity platform we configure our application module as shown:

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { MsalModule } from '@azure/msal-angular';
import { OAuthSettings } from './reference/oauth-settings';
import { APP_CONFIG, CALENDAR_DI_CONFIG } from './app.config';
…
import { CalendarComponent } from '../app/calendar/calendar.component';
import { AuthService } from './services/auth.service';
import { ApiService } from './services/api.service';
import { AuthADInterceptor } from './security/auth.adinterceptor';
…

@NgModule({
  declarations: [
    AppComponent,
    CalendarComponent
	…
  ],
  imports: [
    HttpClientModule,
    BrowserModule,
    AppRoutingModule,
	…
    MsalModule.forRoot({
      auth: {
        clientId: OAuthSettings.appId,
        redirectUri: OAuthSettings.redirectUri
      }
    })
  ],
  providers: [
    ApiService, 
    {
      provide: APP_CONFIG, 
      useValue: CALENDAR_DI_CONFIG
    },    
    AuthService,
    {
        provide: HTTP_INTERCEPTORS,
        useClass: AuthADInterceptor,
        multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

As we can see the following changes are required to allow our application to acquire access tokens from the identity platform and pass them to our web API:

  1. In the application module we pass the OAuth settings, which include the application client id and client application redirection URI into the identity library within the imports array.
  2. Inject the web API URL into an API service class.
  3. Intercept the HTTP request into an interceptor class AuthADInterceptor, where we embed the access token into the request header.

Our Calender UI component will then make the call to the Outlook Calender event web API method.

import { Component, OnInit } from '@angular/core';
import { MsalService } from '@azure/msal-angular';
import { HttpClient } from '@angular/common/http';
import { OutlookCalendarEvent } from '../models/calendar-event'; 
import { ApiService } from '../services/api.service';

const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me/calendarview';

@Component({
  selector: 'app-calendar',
  templateUrl: './calendar.component.html',
  styleUrls: ['./calendar.component.scss']
})
export class CalendarComponent implements OnInit {

  calendar;
  events: OutlookCalendarEvent[] = [];

  constructor(private authService: MsalService, 
    private apiService: ApiService,
    private http: HttpClient) { 
    console.log(authService.getAllAccounts.length);
  }

  ngOnInit() {
    this.getCalendar();    
  }

  getCalendar() {
    this.apiService.getUserCalendarEvents().subscribe(([events]) => 
      {
	      … //process the response.
      });
	…
  }
}

Our Angular application then makes an HTTP REST API call:

https://[server]/api/OutlookCalendar/GetCalendarEvents

Your access token is then passed through the interceptor to the web API. A sample token is shown below during one of my debug sessions:

From the Web API backend, we construct a .NET Core API application.

To read the authentication token, you will need the following call to use the configuration settings to protect the Web API using the Microsoft Identity platform:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration); 

Your app settings will need the “AzureAD” section and keys for the authentication provider to be configured with the following keys and values specified:

"AzureAd": {
    "Domain": "????.onmicrosoft.com",
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "????",
    "TenantId": "????",
    "Audience": "api://????",
    "ClientSecret": "????"
},

Our controller API method to access the calendar is as follows:

[ApiController]
public class OutlookCalendarController : ControllerBase
{
  private readonly IOutlookCalendar _outlookCalendar;
  private readonly ILogger<OutlookCalendarController> _logger;

  public OutlookCalendarController(
    ILogger<OutlookCalendarController> logger, 
    IOutlookCalendar outlookCalendar)
  {
    _logger = logger;
    _outlookCalendar = outlookCalendar;
  }

  [HttpGet("api/[controller]/GetCalendarEvents")]
  public async Task<IActionResult> GetCalendarEvents()
  {
    try
    { 
      var calendarEvents = await _outlookCalendar.GetCalendarEvents();
      if (calendarEvents == null)
        return BadRequest("Calendar request failed!");
      return Ok(calendarEvents);
    }
    catch (Exception ex)
    {
      return BadRequest(ex.Message);                
    }
  }
}

Before we can utilize our service class to make calls to MS Graph, we will need an additional dependency, which is to inject the IHttpContextAccessor type into our custom services to allow the HTTP request context and header authorization key to be read. The authorization key will contain our bearer token, which will then be used to call the MS Graph API calendar method.

using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.AspNetCore.Http;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Graph.Authentication
{
    public class DeviceCodeAuthProvider : IAuthenticationProvider
    {
        private readonly IPublicClientApplication _msalClient;
        private readonly string[] _scopes;
        private readonly HttpContext _httpContext;

        public DeviceCodeAuthProvider(string appId, string[] scopes, HttpContext httpContext)
        {
            _httpContext = httpContext;

            _scopes = scopes;

            _msalClient = PublicClientApplicationBuilder
                .Create(appId)
                .WithAuthority(AadAuthorityAudience.PersonalMicrosoftAccount, true)
                .Build();
        }

        public async Task<string> GetAccessToken()
        {
            return _httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer", "");
        }

        public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage)
        {
            requestMessage.Headers.Authorization =
                new AuthenticationHeaderValue("bearer", await GetAccessToken());
        }
    }
}

The above is a cut-down version of the device authentication provider class from the Microsoft site for Graph developer tutorials. I have stripped away the device code token acquisition calls and simply determine the access token from the upstream application call within the request HTTP context.

The other thing to note is that instead of using the common tenant I am using the consumer tenant.

For a line of business application or enterprise application you will need to reconfigure your application in the Azure tenant for organization or work access instead of the personal MS account access. The MSAL client connection would then be setup the following way:

_msalClient = PublicClientApplicationBuilder
    .Create(appId)
    .WithAuthority(AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount, true)
    .Build();

Within our API method the token is then passed into a service class and HTTP requests to our Graph API are invoked with the token in the request header. The calendar service class consists of a method to call the MS Graph API and then return a collection of the calendar events:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using User.Calendar.API.Models;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Http;
using Graph.Authentication;
using User.Calendar.API.Exceptions;


namespace User.Calendar.API.Services
{
    public class OutlookCalendar: IOutlookCalendar
    {
        private readonly string[] _scopes;
        private readonly string _appId;
        private readonly string _numberCalendarDays;
        private readonly HttpContext _context;

        public OutlookCalendar(IOptions<AppConfiguration> appConfiguration,
            IHttpContextAccessor httpContextAccessor)
        {
            string scopes = appConfiguration.Value.Scopes;
            _context = httpContextAccessor.HttpContext;
            _scopes = scopes.Split(";");
            _appId = appConfiguration.Value.AppId;
            _numberCalendarDays = appConfiguration.Value.NumberCalendarDays;
        }

        public async Task<List<OutlookCalendarEvent>> GetCalendarEvents()
        {
            var authProvider = new DeviceCodeAuthProvider(_appId, _scopes, _context);

            var accessToken = authProvider.GetAccessToken().Result;

            GraphHelper.GraphCalendarHelper.Initialize(authProvider);

            var user = GraphHelper.GraphCalendarHelper.GetMeAsync().Result;

            if (user == null)
                throw new GeneralException("invalid user credentials");

            var events = ListCalendarEvents(
                user.MailboxSettings.TimeZone,
                $"{user.MailboxSettings.DateFormat} {user.MailboxSettings.TimeFormat}",
                System.Convert.ToInt16(_numberCalendarDays));

            List<OutlookCalendarEvent> outlookCalendarEvents = new List<OutlookCalendarEvent>();

            foreach (Microsoft.Graph.Event item in events)
            {
                outlookCalendarEvents.Add(new OutlookCalendarEvent()
                {
                    EventOrganizer = item.Organizer.EmailAddress.Name,
                    Subject = item.Subject,
                    Start = item.Start,
                    End = item.End
                });
            }
            return outlookCalendarEvents;
        }

        static List<Microsoft.Graph.Event> ListCalendarEvents(
            string userTimeZone, 
            string dateTimeFormat,
            int numberOfDays)
        {
            var events = GraphHelper
                .GraphCalendarHelper
                .GetCurrentWeekCalendarViewAsync(DateTime.Today, userTimeZone, numberOfDays)
                .Result.ToList();

            return events;
        }
    }
}

The Graph Helper is code I have taken from the Microsoft site graph developer samples:

https://docs.microsoft.com/en-us/graph/tutorials/dotnet-core?tutorial-step=4

I will not reproduce it here as it will require a more detailed discussion to explain it works and can be amended to suit your own application requirements.

To test our Web API we can run our Angular application, obtain the access token, then call the MS Graph resource using the Graph HTTP OData call using the MS Graph Explorer.

The token will be valid in the above case and will return a response (not shown).

To test the Web API we can use POSTMAN, passing in the token in the Authorization header value as shown:

Provided we have recent events in our Outlook Calendar events, the response will look something like this:

If you pass an invalid or expired token the MS Graph client from the Graph helper class will return a null for the user when calling the Me function of the GraphClient class:

return await graphClient.Me
    .Request()
    .Select(u => new{
        u.DisplayName,
        u.MailboxSettings
    })
    .GetAsync();

Below is the failed request submitted through POSTMAN:

As we will see, calls to Azure AD using federated identities can also be authenticated through JWT tokens, however these tokens are not quite the same as the non-federated SSO custom tokens we generate for our custom Web API methods that are authenticated using ASP.NET Identity against a custom identity data store.

Because of the differences in the tokens, we had to authenticate using different login platforms to be authorised for the target resources.

The task of unifying access for these authentication schemes is challenging, so backend API services can be dedicated to using either one of SSO or Federated authentication schemes, with the client applications using at least two authentication libraries to achieve this.

What I have shown here is a useful way to access MS Graph and authenticate not only from the API backend, but also from a web client frontend. This allows us to decouple our Graph API libraries from the client application and increase testability and reuse.

That’s all for today’s post. I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial