Calling Microsoft Graph API from an Angular 5 Single Page Application (SPA)

In this post I will be discussing how to use OAuth 2 Implicit Grant to authenticate users with Azure AD and call Microsoft Graph ApI from an Angular 5 Single Page Application. The idea of consuming Microsoft Graph API in an Angular app was prompted by a discussion I had with a contact on LinkedIn recently, asking if I had worked with MS Graph API before, and since I had started learning Angular to broaden my skill set, I decided to create a simple SPA that will authenticate users via Azure AD and display users's profile information using MS Graph API. So if you are looking for how to do that, you are in the right place. I will be sharing my experience with you in this post and I hope you will find this it useful.

I will break this post down into 3 categories:
  1. Creating an Angular 5 Application
  2. Authenticating and Authorizing your App
  3. Calling Microsoft Graph API
Creating An Angular 5 Application

Since this post is not about angular per se, I will not be going into nitty-gritty of setting up an angular environment. So I will assume you already have your environment set up for Angular CLI. If you need help on that, Google is your friend 😊 . Now let's get started:
  1. Change to the directory where you want to create your app.
  2. Type ng new AngularDemo --skip-tests true  to create a folder called AngularDemo for our project. Note that we are skipping the default generated test file because we do not need it. This may take some time, so you can take a walk..
  3. When it is done, change directory to the new folder you just created i.e. type cd AngularDemo on the command prompt.
  4.  Type code . to open your project in Visual Studio Code or use your preferred code editor.
  5. Let's create a folder for our components. Open src > app and create a folder called UsersInfo under app. Hint: Create a folder by right-clicking on app folder.
  6. Now let's create two components. On the command prompt, type ng g c UserInfo/login --spec false --flat true to create login component inside the UserInfo folder. --spec means we don't want test spec file generated and --flat means we want the component created directly under UserInfo folder. This will generate 3 files for you with the following extensions: .html, .css and .ts. Note this command can also create the folder for you if not already created.
  7. Repeat step 6 to create another component called myProfile.
when you are done, bring up visual studio code, and you should see the two components you created appearing like the image below:

Now let's build our user interfaces by adding html codes to the components. But before that, let's install bootstrap to help style the pages.
  1. In the same command prompt, type npm install bootstrap@3 --save
  2. In your project, open angular-cli.json and add "../node_modules/bootstrap/dist/css/bootstrap.min.css" in the styles array. It should look like the image below:
  3. Open login.component.html, copy and paste the code below inside it:
  4. <div><button class="btn-lg btn-primary btn-block" (click)="login()">Sign in with Office 365 Account</button></div>.  This is just a button styled with bootstrap.
  5. Also open my-profile.component.html, copy and paste the code below inside it
  6. <table class="table table-striped table-hover">
    <thead>
    <tr>
    <th>Name</th>
    <th>Job Title</th>
    <th>Email</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>{{profile.displayName}}</td>
    <td>{{profile.jobTitle}}</td>
    <td>{{profile.mail}}</td>
    </tr>
    </tbody>
    </table>
  7. Now let's wire everything in the app component. Open app.component.html, copy and paste the code below inside it to replace its content.
  8. <div class="container">
    <nav class="navbar navbar-default">
    <ul class="nav navbar-nav" *ngIf="getToken">
    <li>
    <a routerLink="profile">My Profile</a>
    </li>
    </ul>
    </nav>
    <router-outlet></router-outlet>
    </div>
    Here we are creating html navigation menu for our app, and we are putting it inside a div element. We are also using *ngIf to check if getToken returns true or false. We will use this to hide the navigation element when a user is not logged in. 
  9. Next we will create routing for our navigation. open app.module.ts and import RouterModule, Routes and Router.. ie. type (without the quotation mark) "import{RouterModule, Routes, Router} from '@angular/router';" at the top of  the page.
  10. Copy and paste the code below just above @NgModule({...}) in app.module.ts
const appRoutes=[
{path: 'login', component:LoginComponent},
{path:'profile', component:MyProfileComponent }
]
Add RouterModule.forRoot(appRoutes) to import: [...] array inside @NgModule..
By now your app.module.ts should look like
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import{RouterModule, Routes, Router} from '@angular/router';


import { AppComponent } from './app.component';
import { LoginComponent } from './UserInfo/login/login.component';
import { MyProfileComponent } from './UserInfo/my-profile/my-profile.component';

const appRoutes=[
{path: 'login', component:LoginComponent},
{path:'profile', component:MyProfileComponent }
{path:'', redirectTo:'/login', pathMatch: 'full' }
]
@NgModule({
declarations: [
AppComponent,
LoginComponent,
MyProfileComponent
],
imports: [
BrowserModule,
RouterModule.forRoot(appRoutes)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
We added { path:'', redirectTo: '/login', pathMatch:'full'} code line to redirect users to the login page when they navigate to the root site i.e. http://localhost:4200. We will change this later when we are discussing authorization and authentication.
Your app is taking shape.  You can run it to see if everything is fine.
In Visual Studio code, Save all the changes, go to View and click Integrated Terminal. In the Integrated Terminal, type ng serve -o to launch the app. If everything works fine, your app should look like the image below. click on My profile to ensure you can see the page. Note you will have to remove the *ngIf directive in the app.component.html for now so that you can see the navigation. when you are satisfied, add it back.
This step completes our creating an angular 5 application. Next we will be discussing authenticating and authorizing the app.

Authenticating and Authorizing your App
Now we are getting to the interesting part. The first step here is to register your app on Application Registration Portal
  1. Click Add an app, give a name to your application say Angular Demo.
  2. Click the Create button.
  3. Click Add Platforms and select Web, make sure Allow Implicit Flow is selected. Allow Implicit Flow allows your app to be authenticated without a need for client secret.
  4. In the Redirect URLs, type http://localhost:4200.
  5. Under Delegated Permission, User.Read should be already added, if not, add it. We are using delegated permission because we want our app to access MS Graph API as the currently signed-in user. The other option is used when you don't want user interaction. 
  6. Copy and save the application Id, we will need it later.
  7. You can set the Logout URL if you want, but this article will not cover that. So leave other settings as default and click Save.
  8. Back to Visual Studio Code, now let's create a folder for our service. Services are used in angular for making http call and storing data. So we will create a service for making http request to the  authorization server.

  9. Create a folder called Services under src.
  10. Create the following files inside the Services folder: authHelper.ts and appConfig.ts
  11. Copy and paste the code below inside appConfig.ts

  12. export class AppConfig{
    public static CLIENT_ID: string ="b0f8efe3-d827-4079-6902-922390bc48f0";
    public static TENANT_ID: string = "common";
    public static GRAPH_RESOURCE: string = "https://graph.microsoft.com";
    }
    Change the Client_Id to the application Id that you copied earlier. For Tenant_Id, you can either use common or your tenant id e.g yoursite.onmcrosoft.com. We are using common because we want our app to support multi-tenancy.

  13. Copy and paste the code below inside authHelper.ts
import {AppConfig} from './appConfig';
import{Injectable} from '@angular/core';

var accessToken;
var id_token;
var stateResponse;
var cryptObj = window.crypto;
var Sessionstore = sessionStorage.AppaccessToken == 'undefined' || sessionStorage.AppaccessToken == null;
@Injectable()
export class AuthHelper {

constructor(){
if (Sessionstore) {
this.getTokenResponse();
sessionStorage.AppaccessToken=accessToken;
sessionStorage.idToken = id_token;
sessionStorage.respState = stateResponse;
}
}
private getTokenResponse() {
if (location.hash) {
if (location.hash.split('access_token=')) {
accessToken = location.hash.split('access_token=')[1].split('&')[0];
id_token =location.hash.split('id_token=')[1].split('&')[0];
stateResponse =location.hash.split('state=')[1].split('&')[0];
}
}
}
//Generates guid for nonce and state
guid() {
//An array of 8 16-bit unsigned integers
var buf = new Uint16Array(8);
cryptObj.getRandomValues(buf);
function s4(num) {
var ret = num.toString(16); //The number will show as an hexadecimal value
while (ret.length < 4) {
ret = '0' + ret;
}
return ret;
}
return s4(buf[0]) + s4(buf[1]) + '-' + s4(buf[2]) + '-' + s4(buf[3]) + '-' +
s4(buf[4]) + '-' + s4(buf[5]) + s4(buf[6]) + s4(buf[7]);
}

login() {
sessionStorage.nonce = this.guid();
sessionStorage.state = this.guid();
window.location.href = "https://login.microsoftonline.com/"+ AppConfig.TENANT_ID+
"/oauth2/authorize?client_id="+AppConfig.CLIENT_ID+"&response_type=token+id_token
&resource="+ AppConfig.GRAPH_RESOURCE+"&state=" + sessionStorage.state + "
&nonce=" + sessionStorage.nonce + "&redirect_uri=http://localhost:4200";
}
}
This code is pretty straightforward. We imported AppConfig in the AuthHelper.ts so we could access our configuration parameters and use them to build the endpoint url. We then retrieved access_token, Id_token and state from the token response sent by the authorization server using getTokenResponse() method. The guid() method is used to auto-generate random string of numbers for nonce and state parameter used in the request endpoint url. We will use these values to validate the token response later. Finally, the retrieved values are stored using sessionStorage. 

Now let's wire the login() method to the click event of the login button

  1. Open login.component.ts, add this import statement to the top of the page: import {AuthHelper} from '../../../Services/authHelper'; then type private authHelper: AuthHelper inside the LoginComponent class but just before the constructor. 
  2. Update the constructor and call the login method as shown below
 

  Before we test our login component, let's add the AuthHelper service in the Provider array of NgModule in the app.module.ts.

Open app.module.ts, add import {AuthHelper} from '../../src/Services/authHelper'; to the top of the page and add AuthHelper to the Providers: [] array. 
Now save all the changes and type ng serve -o  if your app is not already running. 
Click the login button, this should launch the sign-in page, and after signing in, it will ask that you authorize the app to read your profile. 
Click Accept. This will redirect you to your app login page. Observe the url, you should see something like #access_token..... You can use fiddler to grab the full response. If you see the access token that means everything is working fine.

Next, instead of redirecting the app to the login page after successful authentication and authorization, we want to redirect it to the profile page i.e. My profile page. But before we do that, we need to validate the claims in the id_token. For this demo, we will only be validating the nonce and state to guide against replay attack. We will check if these values are the same as the values we sent in our request to the authorization server. If they are, we will load the My Profile page but if any of them or both are different, we will return the app back to the home page with an error message in the url. Note, according to open Id specifications, token must be validated before using. For a full list of other token properties that must be validated, see the Open Id specification

To validate the token, we will use jsrsasign.js library. So let's install and import it in our project
  1. On the command prompt or Integrated terminal type npm install jsrsasign
  2. Copy and paste the following import statement at the import section of app.component.ts 
  3. import { Router } from '@angular/router';
    import { AuthHelper } from '../Services/authHelper';
    import {KJUR,jws,b64toutf8} from 'jsrsasign/lib/jsrsasign.js';
  4. Then copy the code below and replace the AppComponent class i.e. export class AppComponent{......}
  5. export class AppComponent {
    accessToken;
    getToken=sessionStorage.AppaccessToken !="undefined";
    constructor(router: Router, auth: AuthHelper) {
    this.accessToken = sessionStorage.AppaccessToken;
    if (this.accessToken === "undefined") {
    router.navigate(["/login"]);
    }
    else {
    var tokenClaims = sessionStorage.idToken.split('.');
    var header = KJUR.jws.JWS.readSafeJSONString(b64toutf8(tokenClaims[0]));
    var payload = KJUR.jws.JWS.readSafeJSONString(b64toutf8(tokenClaims[1]));
    var notValid=sessionStorage.respState !== sessionStorage.state || payload.nonce !==sessionStorage.nonce;
    if (notValid) {
    window.location.hash = '#error=Invalid_token&error_description=The+id_token+in+the+authorization+response+did+not+match+the+expected+value.+Please+try+signing+in+again.';
    console.log(payload.nonce);
    sessionStorage.clear();
    alert("Error: Inavlid token Id returned!");
    return;
    }
    else {
    router.navigate(["/profile"]);
    }
    }
    }
    }
  6. This is self explanatory. We are retrieving nonce and state values stored in the sessionStorage and comparing them against the claims in the token. We are using the jsrsasign library to parse the token claims and retrieve nonce from the token response. Other important token claims to validate are, signing key, audience(aud), and issuer (iss). You can retrieve aud and iss from the token same way i.e. payload.aud and payload.iss respectively. aud should contain your client id according to open Id specification while issuer should look like https://sts.windows.net/{tenant_id}. But for our case, we cannot validate issuer since we are building a multi-tenant application, unless we have a subscription list to check against. Hint: you can decode the token using this tool to see other claims in the token. Lastly we check if the "notValid" expression evaluates to true, if it does, the app returns the user back to the login page and display error, otherwise it takes the user to My profile page where they can see their information.
  7. Now open app.module.ts and import {HttpModule} from '@angular/http'. Then include HttpModule in the imports:[] and prodivers:[] array of NgModule. It should look like:
  8. Before we test, let's comment out or remove the line { path:'', redirectTo: '/login', pathMatch:'full'} from appRoute . We don't need it again, we have taken care of this in the app.component.ts. See step 3 and 4 above
  9. If your app is not already running, type ng serve -o to run it and test your app.
if everything is fine, you should be able to login using your office 365 or any Microsoft account and the app should redirect you to My Profile page. We are getting there. The last part is to consume Microsoft Graph API. 

Calling Microsoft Graph API
This is the last step. Copy and paste the code below at the  import section of my-profile.component.ts.
import {Http, Headers} from '@angular/http';
import {AuthHelper} from '../../../Services/authHelper';

Then copy and replace MyProfileComponent Class i.e. export class MyProfileComponent Implemeny OnInit{.....} with the code below:
export class MyProfileComponent implements OnInit {
profile: object;
constructor(http: Http, auth: AuthHelper) {
http.get("https://graph.microsoft.com/v1.0/me", {
headers: new Headers({ "Authorization": "Bearer " + sessionStorage.AppaccessToken })
})
.subscribe(res => {
if (res.status === 200) {
console.log(res.json());
this.profile = res.json();
}
})
}
ngOnInit() {
}
}

We make a GET request to https://graph.microsoft.com/v1.0/me/ which is the endpoint for user profile, set the bearer token (retrieved from sessionStorage) and parsed the response as JSON object. If you open my-profile.component.ts you will see how we retrieved values from the JSON response using dot notation. You can get more endpoints to play around with at Microsoft Graph Explorer.

Save all the chnages and run the app if not already running. Then click sign in button, you should be redirected to My profile page and see your profile information like this:
Hurrah!! You have successfully authenticated your app and consumed MS Graph API from your angular application. Note you will need to clear the session to be able to get back to the login page. You can do that in your logout method. 

References

https://docs.microsoft.com/en-us/outlook/rest/javascript-tutorial
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-token-and-claims
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-devhowto-multi-tenant-overview

Comments

  1. I too have implemented similarly using implicit flow only.

    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. Thanks for sharing this post.Keep sharing more like this.

    Guest posting sites
    Education

    ReplyDelete
  4. I don’t know what has happened to the custom of delivering consistent good articles. I hope that the custom comes alive after this..thumbs up for your work
    miniclip online, a10 games online, Jogos para crianças 2019

    ReplyDelete
  5. friv 4 game
    2player games for kids
    io jogos for kids
    I might suggest almost any information. It really is superb to see you might reveal with text by heart as well as display quality by using precious information is reasonably purely known

    ReplyDelete
  6. I like to read your posts very much. They are so awesome. The articles contain plenty of knowledge and information. They are pieces of advice so that I can solve some problems. Would you mind uploading more posts?
    free games online
    jogos friv 4 school
    jogos 360

    ReplyDelete

  7. Monopoly .io
    Baseball Hero
    Football Tricks
    I really enjoy simply reading all of your weblogs. Simply wanted to inform you that you have people like me who appreciate your work. Definitely a great post. Hats off to you! The information that you have provided is very helpful.

    ReplyDelete
  8. I accidentally saw your post and it attracted me from the first few seconds, it was excellent, hope you have more successful posts. I like this post because it contains a lot of useful information to read, maybe everyone will like me.
    kizi 2 player racing games, 2 player pbs games, friv jogos online

    ReplyDelete
  9. when Women cease to be handsome, they study to be good. To maintain their Influence over Men, they supply the Diminution of Beauty by an Augmentation of Utility. They learn to do a 1,000 Services small and great, and are the most tender and useful of all Friends when you are sick.
    kizi Games online
    free games online
    friv 2019

    ReplyDelete
  10. Ironically, I was trying to stay out of the house because it made me sad to be there without [Dillon]. I went in to work and our administrative assistant's daughter was there with two new foster pups, six weeks old. They were two out of seven from a litter that was found by the side of the road in a box. I held Libby for at least an hour, and the rest is history!
    kizi games
    io jogo online
    play friv

    ReplyDelete

Post a Comment

Popular posts from this blog

Generate Word Document From A SharePoint List Item Using Microsoft Flow

Creating SharePoint Framework Client Web Part and solving the challenges with _RequestDigest Token value using React