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.
- 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.
- 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.
Overview of How Web Applications Call Graph API Services
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:
Configuration of OAuth in Angular Application for Graph API Integration
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:
- 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.
- Inject the web API URL into an API service class.
- Intercept the HTTP request into an interceptor class AuthADInterceptor, where we embed the access token into the request header.
Implementation of the Calendar UI in Angular Application
Our Calendar UI component will then make the call to the Outlook Calendar 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:
Implementation of the Calendar API in the Web API Backend Service
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);
}
}
}
In the next section I will show how to implement the device code authentication provider, which obtains an access token from the user.
Implementation of the Device Code Authentication Provider
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();
In the next section I will show how to implement an Outlook calendar service class that makes use of our device code authentication provider and the graph helper class to display a list of calendar events.
Implementation of an Outlook Calendar Service Class
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.
Testing the MS Graph API Calls
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.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.