Angular, Azure AD, and Microsoft Graph
Hello, world! In this post I do a 101 intro on Microsoft Graph, an important part of Azure AD and related platforms like Office 365, and it assumes you have read Angular6 and Azure AD and Angular and Azure AD with a better UX. Microsoft Graph is extensively documented by Microsoft and I extend an invitation to read it starting here. Here is my highly irresponsible definition of Microsoft Graph: I like to think of it as a repository made of nodes connected with each other with the user node being the main. Every node contains data about it and more can be found by traversing the net (graph). For example, a user node contains her name, phone, and email whereas, by traversing the graph, we can find the list of meeting attendees in a given calendar day scheduled through Office 365.
In this post, Microsoft Graph is used to honor a Marketing requirement for greeting the user with the shorter first name instead of the full name because that leaves more room for pleasant ads ;). The full name is the "name" claim value extracted from the Identity Token but other claims, like givenName, email, and jobTitle (known collectively as the user profile) are available only through Microsoft Graph. Following is an image of the Azure portal where the user profile can be seen and updated:
And since we are to invoke Microsoft Graph RESTful web services, we need to import the HTTP related modules and classes and, in the process, organize a little bit better our code. AuthService is core functionality so let's create a core module by running:
Following is the code for the new core.module.ts:
AuthService is responsible of interacting with Microsoft Graph to pull and hold the user profile so this is the adapted version:
AuthService does not use the Identity Token any more. Now, it makes an HTTP GET to the Microsoft Graph user profile endpoint, creates an instance of User, and exposes it through the user property. Below is the new version of AppComponent:
Property username value comes from the "givenName" claim, part from the user profile retrieved from Microsoft Graph. The application is ready to run so we do:
Or...
However, a 401 (Unauthorized) occurs. Details have been logged out to the console:
The InvalidAuthenticationToken code and the Access token is empty message take us to introduce the concept of the Access Token.
MSAL.UserAgentApplication provides the acquireTokenSilent method to obtain an Access Token. With that in mind, let's put our hands on the new version of AuthService:
Running this application could produce the following error (values like GUIDs will differ):
The important part is the message acquireTokenSilent error = AADSTS700051: response_type 'token' is not enabled for the application. This is because the OAuth2 Implicit Flow, supported by Azure AD for Angular SPAs, is disabled. To enable it, we must go to the Azure portal and set the oauth2AllowImplicitFlow to true in the App Registration Manifest as shown below:
All things set and done correctly and the user is greeted with her shorter, first name:
That's it for these articles about Angular, Azure AD, and Microsoft Graph. As usual, the code can be downloaded from here and it has been committed as "Microsoft Graph". I hope you find the information supplied and the associated code very useful.
In this post, Microsoft Graph is used to honor a Marketing requirement for greeting the user with the shorter first name instead of the full name because that leaves more room for pleasant ads ;). The full name is the "name" claim value extracted from the Identity Token but other claims, like givenName, email, and jobTitle (known collectively as the user profile) are available only through Microsoft Graph. Following is an image of the Azure portal where the user profile can be seen and updated:
Resource Server
In OAuth2 lexicon, Microsoft Graph is a Resource Server where the sea of stored data is provided through RESTful web services. There is an specific endpoint for every data set of interest so we have https://graph.microsoft.com/v1.0/me for the user profile and https://graph.microsoft.com/v1.0/me/messages for the user email messages. Issuing an HTTP GET to https://graph.microsoft.com/v1.0/me will produce the following response (JSON representation of the user profile):{ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity", "businessPhones": [], "displayName": "Francisco Javier Banos Lemoine", "givenName": "Francisco Javier", "jobTitle": "Technical Fellow", "mail": "franl@illyum.com", "mobilePhone": "+52 5515327887", "officeLocation": null, "preferredLanguage": "en-US", "surname": "Banos Lemoine", "userPrincipalName": "franl@illyum.com", "id": "0e844e01-5659-4cc1-a8b9-2bfbde0374c5" }
Calling Microsoft Graph from Angular6
Let's start by creating the User class for a subset of the expected response:export class User { private _mail: string; get mail() { return this._mail; } set mail(value) { this._mail = value; } private _displayName: string; get displayName() { return this._displayName; } set displayName(value) { this._displayName = value; } private _givenName: string; get givenName() { return this._givenName; } set givenName(value) { this._givenName = value; } private _surname: string; get surname() { return this._surname; } set surname(value) { this._surname = value; } private _jobTitle: string; get jobTitle() { return this._jobTitle; } set jobTitle(value) { this._jobTitle = value; } }
ng generate module core/ --patch
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ imports: [CommonModule, HttpClientModule], declarations: [] }) export class CoreModule { }
import * as Msal from 'msal'; import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, from, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { User } from './user'; @Injectable({ providedIn: 'root' }) export class AuthService { private userAgentApplication: Msal.UserAgentApplication; private _userLoggedIn: boolean; get userLoggedIn(): boolean { return this._userLoggedIn; } set userLoggedIn(value) { this._userLoggedIn = value; } private _user: User; get user() { if (this._user) { return this._user; } else { return null; } } set user(value) { this._user = value; } constructor(private httpClient: HttpClient) { this.userAgentApplication = new Msal.UserAgentApplication( '55f4dfa9-2425-44f0-83a6-d3464b2a7eaa', 'https://login.microsoftonline.com/illyum.onmicrosoft.com', null ); } public login(): Observable<string> { const graphScopes = ['user.read']; const promise = this.userAgentApplication.loginPopup(graphScopes); promise .then(_ => { this.userLoggedIn = true; this.getUser().subscribe(user => this.user = user); }) .catch(error => console.log(`loginPopup error = ${error}`)); return from(promise); } public logout(): void { this.userAgentApplication.logout(); this.userLoggedIn = false; } public getUser(): Observable<User> { const graphUrl = 'https://graph.microsoft.com/v1.0/me'; return this.httpClient.get<User>(graphUrl).pipe( catchError(this.handleError) ); } private handleError(error: HttpErrorResponse) { console.log(`handleError = ${JSON.stringify(error)}`); return throwError(null); } }
import { Component } from '@angular/core'; import { AuthService } from './core/auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Angular and Azure AD'; get userLoggedIn() { return this.authService.userLoggedIn; } get username() { if (this.authService.user) { return this.authService.user.givenName; } else { return null; } } constructor(private authService: AuthService) { } login(): void { this.authService.login(); } logout(): void { this.authService.logout(); } }
npm start
ng serve --open
handleError = { "headers": { "normalizedNames": {}, "lazyUpdate": null }, "status": 401, "statusText": "Unauthorized", "url": "https://graph.microsoft.com/v1.0/me", "ok": false, "name": "HttpErrorResponse", "message": "Http failure response for https://graph.microsoft.com/v1.0/me: 401 Unauthorized", "error": { "error": { "code": "InvalidAuthenticationToken", "message": "Access token is empty.", "innerError": { "request-id": "e267bb6a-1f83-4ff7-9119-d823f8814da1", "date": "2019-01-04T03:48:11" } } } }
Access Token
Microsoft Graph web services requires the client application to be authorized, i.e., to present an Access Token as prescribed by OAuth2. An Access Token, which is different from the Identity Token obtained in the login process, is a type of credential sent in the HTTP Authorization header like this:Authorization: Bearer AccessTokenGoesHere
import * as Msal from 'msal'; import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { Observable, from, throwError, Subscriber } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { User } from './user'; @Injectable({ providedIn: 'root' }) export class AuthService { private userAgentApplication: Msal.UserAgentApplication; private _userLoggedIn: boolean; get userLoggedIn(): boolean { return this._userLoggedIn; } set userLoggedIn(value) { this._userLoggedIn = value; } private _user: User; get user() { if (this._user) { return this._user; } else { return null; } } set user(value) { this._user = value; } constructor(private httpClient: HttpClient) { this.userAgentApplication = new Msal.UserAgentApplication( '55f4dfa9-2425-44f0-83a6-d3464b2a7eaa', 'https://login.microsoftonline.com/illyum.onmicrosoft.com', null ); } public login(): Observable<string> { const graphScopes = ['user.read']; const promise = this.userAgentApplication.loginPopup(graphScopes); promise .then(_ => { this.userLoggedIn = true; this.getUser().subscribe(user => this.user = user); }) .catch(error => console.log(`loginPopup error = ${error}`)); return from(promise); } public logout(): void { this.userAgentApplication.logout(); this.userLoggedIn = false; } public getUser(): Observable<User> { return new Observable(observer => { const graphScopes = ['user.read']; const promise = this.userAgentApplication.acquireTokenSilent(graphScopes); promise .then(accessToken => { this.getUserFromGraph(accessToken).subscribe(user => { observer.next(user); observer.complete(); }); }) .catch(error => console.log(`acquireTokenSilent error = ${error}`)); }); } private getUserFromGraph(accessToken: string): Observable<User> { const graphUrl = 'https://graph.microsoft.com/v1.0/me'; const headers = new HttpHeaders({ 'Authorization': `Bearer ${accessToken}` }); return this.httpClient.get private handleError(error: HttpErrorResponse) { console.log(`handleError = ${JSON.stringify(error)}`); return throwError(null); } }(graphUrl, { headers: headers }).pipe( catchError(this.handleError) ); }
acquireTokenSilent error = AADSTS700051: response_type 'token' is not enabled for the application Trace ID: 93a3fdbe-63dc-42e5-b26a-d8bfcfd61100 Correlation ID: e1eca2f1-1fdd-465f-ab12-07379ee6cc6d Timestamp: 2019-01-04 07:18:09Z|unsupported_response_type
All things set and done correctly and the user is greeted with her shorter, first name:
That's it for these articles about Angular, Azure AD, and Microsoft Graph. As usual, the code can be downloaded from here and it has been committed as "Microsoft Graph". I hope you find the information supplied and the associated code very useful.
Comments
Post a Comment