Tutorial: Sign in users and call the Microsoft Graph API from a JavaScript single-page application
In this tutorial, you build a JavaScript single-page application (SPA) that signs in users and calls Microsoft Graph by using the implicit flow of OAuth 2.0. This SPA uses MSAL.js v1.x, which uses the implicit grant flow for SPAs. For all new applications, use MSAL.js v2.x and the authorization code flow with PKCE and CORS. The authorization code flow provides more security than the implicit flow.
In this tutorial:
- Create a JavaScript project with
npm
- Register the application in the Azure portal
- Add code to support user sign-in and sign-out
- Add code to call the Microsoft Graph API
- Test the app
- Gain an understanding of how the process works behind the scenes
At the end of this tutorial, you'll have the following folder and file structure (listed in order of creation):
sampleApp/
├── JavaScriptSPA/
│ ├── authConfig.js
│ ├── authPopup.js
│ ├── graph.js
│ ├── graphConfig.js
│ ├── index.html
│ └── ui.js
├── package.json
├── package-lock.json
├── node_modules/
│ └── ...
└── server.js
Prerequisites
- Node.js for running a local web server.
- Visual Studio Code or another editor for modifying project files.
- A modern web browser. The app that you build in this tutorial uses ES6 conventions and does not support Internet Explorer.
How the sample app works
The application that you create in this tutorial enables a JavaScript SPA to query the Microsoft Graph API. This querying can also work for a web API that's set up to accept tokens from the Microsoft identity platform. After the user signs in, the SPA requests an access token and adds it to the HTTP requests through the authorization header. The SPA will use this token to acquire the user's profile and emails via the Microsoft Graph API.
The Microsoft Authentication Library (MSAL) for JavaScript handles token acquisition and renewal.
Set up the web server or project
If you prefer, you can download the project files.
To configure the code sample before you run it, skip to the registration step.
Create the project
Make sure that Node.js is installed, and then create a folder to host the application. Name the folder sampleApp. In this folder, an Express web server is created to serve the index.html file.
By using a terminal (such as the Visual Studio Code integrated terminal), locate the project folder and move into it. Then enter:
npm init
A series of prompts appears for creation of the application. Notice that the folder sampleApp is now all lowercase. The items in parentheses
()
are generated by default.package name: (sampleapp) version: (1.0.0) description: entry point: (index.js) test command: git repository: keywords: author: license: (ISC)
Feel free to experiment. However, for the purposes of this tutorial, you don't need to enter anything. Select the Enter key to continue to the next prompt.
The final consent prompt contains the following output if you didn't enter any values in the previous step.
{ "name": "sampleapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } Is this OK? (yes)
Select the Enter key, and the JSON is written to a file called package.json.
Install the required dependencies by entering the following code:
npm install express --save npm install morgan --save
Express.js is a Node.js module that simplifies the creation of web servers and APIs. Morgan.js is used to log HTTP requests and errors. Installing them creates the package-lock.json file and node_modules folder.
Create a .js file named server.js in your current folder, and add the following code:
const express = require('express'); const morgan = require('morgan'); const path = require('path'); //Initialize Express. const app = express(); // Initialize variables. const port = 3000; // process.env.PORT || 3000; // Configure the morgan module to log all requests. app.use(morgan('dev')); // Set the front-end folder to serve public assets. app.use(express.static('JavaScriptSPA')) app.get('*', function (req, res) { res.sendFile(path.join(__dirname + '/JavaScriptSPA/index.html')); }); // Start the server. app.listen(port); console.log('Listening on port ' + port + '...');
You now have a server to serve the SPA. At this point, the folder structure should look like this:
sampleApp/
├── package.json
├── package-lock.json
├── node_modules/
│ └── ...
└── server.js
In the next steps, you'll create a new folder for the JavaScript SPA and set up the user interface (UI).
Tip
When you set up an Azure Active Directory (Azure AD) account, you create a tenant. This is a digital representation of your organization. It's primarily associated with a domain, like Microsoft.com. If you want to learn how applications can work with multiple tenants, refer to the application model.
Create the SPA UI
Create a new folder, JavaScriptSPA, and then move into that folder.
Create an index.html file for the SPA. This file implements a UI that's built with the Bootstrap 4 framework. The file also imports script files for configuration, authentication, and API calls.
In the index.html file, add the following code:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <title>Quickstart | MSAL.JS Vanilla JavaScript SPA</title> <!-- msal.js with a fallback to backup CDN --> <script src="https://alcdn.msauth.net/browser/2.30.0/js/msal-browser.js" integrity="sha384-L8LyrNcolaRZ4U+N06atid1fo+kBo8hdlduw0yx+gXuACcdZjjquuGZTA5uMmUdS" crossorigin="anonymous"></script> <!-- adding Bootstrap 4 for UI components --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-o4ufwq3oKqc7IoCcR08YtZXmgOljhTggRwxP2CLbSqeXGtitAxwYaUln/05nJjit" crossorigin="anonymous"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <a class="navbar-brand" href="/">MS Identity Platform</a> <div class="btn-group ml-auto dropleft"> <button type="button" id="signIn" class="btn btn-secondary" onclick="signIn()">Sign In</button> <button type="button" id="signOut" class="btn btn-success d-none" onclick="signOut()">Sign Out</button> </div> </nav> <br> <h5 class="card-header text-center">Vanilla JavaScript SPA calling MS Graph API with MSAL.JS</h5> <br> <div class="row" style="margin:auto" > <div id="card-div" class="col-md-3 d-none"> <div class="card text-center"> <div class="card-body"> <h5 class="card-title" id="welcomeMessage">Please sign-in to see your profile and read your mails</h5> <div id="profile-div"></div> <br> <br> <button class="btn btn-primary" id="seeProfile" onclick="seeProfile()">See Profile</button> <br> <br> <button class="btn btn-primary d-none" id="readMail" onclick="readMail()">Read Mails</button> </div> </div> </div> <br> <br> <div class="col-md-4"> <div class="list-group" id="list-tab" role="tablist"> </div> </div> <div class="col-md-5"> <div class="tab-content" id="nav-tabContent"> </div> </div> </div> <br> <br> <!-- importing bootstrap.js and supporting .js libraries --> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <!-- importing app scripts (load order is important) --> <script type="text/javascript" src="./authConfig.js"></script> <script type="text/javascript" src="./graphConfig.js"></script> <script type="text/javascript" src="./ui.js"></script> <!-- replace the next line with authRedirect.js if you want to use the redirect flow --> <!-- <script type="text/javascript" src="./authRedirect.js"></script> --> <script type="text/javascript" src="./authPopup.js"></script> <script type="text/javascript" src="./graph.js"></script> </body> </html>
Create a file named ui.js, and add this code to access and update the Document Object Model (DOM) elements:
// Select DOM elements to work with const welcomeDiv = document.getElementById("welcomeMessage"); const signInButton = document.getElementById("signIn"); const signOutButton = document.getElementById('signOut'); const cardDiv = document.getElementById("card-div"); const mailButton = document.getElementById("readMail"); const profileButton = document.getElementById("seeProfile"); const profileDiv = document.getElementById("profile-div"); function showWelcomeMessage(account) { // Reconfiguring DOM elements cardDiv.classList.remove('d-none'); welcomeDiv.innerHTML = `Welcome ${account.name}`; signInButton.classList.add('d-none'); signOutButton.classList.remove('d-none'); } function updateUI(data, endpoint) { console.log('Graph API responded at: ' + new Date().toString()); if (endpoint === graphConfig.graphMeEndpoint) { const title = document.createElement('p'); title.innerHTML = "<strong>Title: </strong>" + data.jobTitle; const email = document.createElement('p'); email.innerHTML = "<strong>Mail: </strong>" + data.mail; const phone = document.createElement('p'); phone.innerHTML = "<strong>Phone: </strong>" + data.businessPhones[0]; const address = document.createElement('p'); address.innerHTML = "<strong>Location: </strong>" + data.officeLocation; profileDiv.appendChild(title); profileDiv.appendChild(email); profileDiv.appendChild(phone); profileDiv.appendChild(address); } else if (endpoint === graphConfig.graphMailEndpoint) { if (data.value.length < 1) { alert("Your mailbox is empty!") } else { const tabList = document.getElementById("list-tab"); tabList.innerHTML = ''; // clear tabList at each readMail call const tabContent = document.getElementById("nav-tabContent"); data.value.map((d, i) => { // Keeping it simple if (i < 10) { const listItem = document.createElement("a"); listItem.setAttribute("class", "list-group-item list-group-item-action") listItem.setAttribute("id", "list" + i + "list") listItem.setAttribute("data-toggle", "list") listItem.setAttribute("href", "#list" + i) listItem.setAttribute("role", "tab") listItem.setAttribute("aria-controls", i) listItem.innerHTML = d.subject; tabList.appendChild(listItem) const contentItem = document.createElement("div"); contentItem.setAttribute("class", "tab-pane fade") contentItem.setAttribute("id", "list" + i) contentItem.setAttribute("role", "tabpanel") contentItem.setAttribute("aria-labelledby", "list" + i + "list") contentItem.innerHTML = "<strong> from: " + d.from.emailAddress.address + "</strong><br><br>" + d.bodyPreview + "..."; tabContent.appendChild(contentItem); } }); } } }
Register the application
Before you proceed with authentication, register the application on Azure AD:
Sign in to the Azure portal.
Go to Azure Active Directory.
On the left panel, under Manage, select App registrations. Then, on the top menu bar, select New registration.
For Name, enter a name for the application (for example, sampleApp). You can change the name later if necessary.
Under Supported account types, select Accounts in this organizational directory only.
In the Redirect URI section, select the Web platform from the dropdown list.
To the right, enter
http://localhost:3000/
.Select Register.
The Overview page of the application opens. Note the Application (client) ID and Directory (tenant) ID values. You'll need both of them when you create the authConfig.js file in later steps.
Under Manage, select Authentication.
In the Implicit grant and hybrid flows section, select ID tokens and Access tokens. ID tokens and access tokens are required because this app must sign in users and call an API.
Select Save. You can go back to the Overview page by selecting it on the left panel.
You can change the redirect URI anytime by going to the Overview page and selecting Add a Redirect URI.
Configure the JavaScript SPA
In the JavaScriptSPA folder, create a new file, authConfig.js. Then copy the following code. This code contains the configuration parameters for authentication (client ID, tenant ID, redirect URI).
const msalConfig = { auth: { clientId: "Enter_the_Application_Id_Here", authority: "Enter_the_Cloud_Instance_Id_Here/Enter_the_Tenant_Info_Here", redirectUri: "Enter_the_Redirect_URI_Here", }, cache: { cacheLocation: "sessionStorage", // This configures where your cache will be stored storeAuthStateInCookie: false, // Set this to "true" if you're having issues on Internet Explorer 11 or Edge } }; // Add scopes for the ID token to be used at Microsoft identity platform endpoints. const loginRequest = { scopes: ["openid", "profile", "User.Read"] }; // Add scopes for the access token to be used at Microsoft Graph API endpoints. const tokenRequest = { scopes: ["Mail.Read"] };
Modify the values in the
msalConfig
section. Refer to the Overview page of the application for these values:Enter_the_Application_Id_Here
is the Application (client) ID value for the application that you registered.Enter_the_Cloud_Instance_Id_Here
is the instance of the Azure cloud. For the main or global Azure cloud, enterhttps://login.microsoftonline.com
. For national clouds (for example, China), refer to National clouds.Replace
Enter_the_Tenant_info_here
with the Directory (tenant) ID (a GUID) or Tenant name value (for example, contoso.onmicrosoft.com).Enter_the_Redirect_URI_Here
is the default URL that you set in the previous section:http://localhost:3000/
.
Tip
There are other options for Enter_the_Tenant_info_here
, depending on what you want your application to support:
- If your application supports accounts in any organizational directory, replace this value with organizations.
- If your application supports accounts in any organizational directory and personal Microsoft accounts, replace this value with common. To restrict support to personal Microsoft accounts only, replace this value with consumers.
Use the MSAL to sign in the user
In the JavaScriptSPA folder, create a new .js file named authPopup.js, which contains the authentication and token acquisition logic. Add the following code:
const myMSALObj = new Msal.UserAgentApplication(msalConfig);
function signIn() {
myMSALObj.loginPopup(loginRequest)
.then(loginResponse => {
console.log('id_token acquired at: ' + new Date().toString());
console.log(loginResponse);
if (myMSALObj.getAccount()) {
showWelcomeMessage(myMSALObj.getAccount());
}
}).catch(error => {
console.log(error);
});
}
function signOut() {
myMSALObj.logout();
}
function callMSGraph(theUrl, accessToken, callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
callback(JSON.parse(this.responseText));
}
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader('Authorization', 'Bearer ' + accessToken);
xmlHttp.send();
}
function getTokenPopup(request) {
return myMSALObj.acquireTokenSilent(request)
.catch(error => {
console.log(error);
console.log("silent token acquisition fails. acquiring token using popup");
// fallback to interaction when the silent call fails
return myMSALObj.acquireTokenPopup(request)
.then(tokenResponse => {
return tokenResponse;
}).catch(error => {
console.log(error);
});
});
}
function seeProfile() {
if (myMSALObj.getAccount()) {
getTokenPopup(loginRequest)
.then(response => {
callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
profileButton.classList.add('d-none');
mailButton.classList.remove('d-none');
}).catch(error => {
console.log(error);
});
}
}
function readMail() {
if (myMSALObj.getAccount()) {
getTokenPopup(tokenRequest)
.then(response => {
callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
}).catch(error => {
console.log(error);
});
}
}
Use tokens for validation
The first time a user selects the Sign In button, the signIn
function that you added to the authPopup.js file calls MSAL's loginPopup
function to start the sign-in process. This function opens a pop-up window that prompts the user to enter their credentials.
After a successful sign-in, the user is redirected back to the original index.html page. The msal.js file receives and processes an ID token, and the information in the token is cached. The ID token contains basic information about the user, such as the user's display name. If you plan to use any data in the ID token for any purpose, make sure that your back-end server validates the token to guarantee that the token was issued to a valid user for your application.
The app that you create in this tutorial calls acquireTokenSilent
and/or acquireTokenPopup
to acquire an access token. The app uses this token to query the Microsoft Graph API for the user's profile info. If you need a sample that validates the ID token, refer to the sample application in GitHub, which uses an ASP.NET web API for token validation.
Get a user token interactively
After the initial sign-in, users shouldn't need to reauthenticate every time they need to request a token to access a resource. Most of the time, the app will use acquireTokenSilent
to acquire tokens. But you might force users to interact with the Microsoft identity platform in situations like these:
- Users need to reenter their credentials because the password has expired.
- An application is requesting access to a resource and needs the user's consent.
- Two-factor authentication is required.
Calling acquireTokenPopup
opens a pop-up window (or acquireTokenRedirect
redirects users to the Microsoft identity platform). In that window, users need to interact by confirming their credentials, giving consent to the required resource, or completing the two-factor authentication.
Get a user token silently
The acquireTokenSilent
method handles token acquisition and renewal without any user interaction. After loginPopup
(or loginRedirect
) is executed for the first time, subsequent calls use acquireTokenSilent
to get tokens for accessing protected resources. (Calls to request or renew tokens are made silently.)
The acquireTokenSilent
method might fail in some cases, such as when a user's password expires. The application can handle this exception in two ways:
Making a call to
acquireTokenPopup
immediately, which triggers a user sign-in prompt. This pattern is commonly used in online applications where no unauthenticated content is available to the user. The sample that you create in this tutorial uses this pattern.Making a visual indication to the user that an interactive sign-in is required. The user can then select the right time to sign in, or the application can retry
acquireTokenSilent
at a later time.This pattern is commonly used when the user can use other functionality of the application without being disrupted. For example, unauthenticated content might be available in the application. In this situation, the user can decide when they want to sign in to access the protected resource or refresh the outdated information.
Note
This tutorial uses the loginPopup
and acquireTokenPopup
methods by default. If you're using Internet Explorer as your browser, we recommend that you use the loginRedirect
and acquireTokenRedirect
methods because of a known issue with the way Internet Explorer handles pop-up windows.
If you want to see how to achieve the same result by using redirect methods, see the sample code.
Call the Microsoft Graph API by using the acquired token
In the JavaScriptSPA folder, create a .js file named graphConfig.js, which stores the Representational State Transfer (REST) endpoints. Add the following code:
const graphConfig = { graphMeEndpoint: "Enter_the_Graph_Endpoint_Here/v1.0/me", graphMailEndpoint: "Enter_the_Graph_Endpoint_Here/v1.0/me/messages" };
Enter_the_Graph_Endpoint_Here
is the instance of the Microsoft Graph API. For the global Microsoft Graph API endpoint, you can replace this withhttps://graph.microsoft.com
. For national cloud deployments, refer to the Microsoft Graph API documentation.Create a file named graph.js, which will make a REST call to the Microsoft Graph API. The SPA can then access web services in a simple and flexible way without any processing. Add the following code:
function callMSGraph(endpoint, token, callback) { const headers = new Headers(); const bearer = `Bearer ${token}`; headers.append("Authorization", bearer); const options = { method: "GET", headers: headers }; console.log('request made to Graph API at: ' + new Date().toString()); fetch(endpoint, options) .then(response => response.json()) .then(response => callback(response, endpoint)) .catch(error => console.log(error)) }
More information about REST calls against a protected API
The sample application that you create in this tutorial uses the callMSGraph()
method to make an HTTP GET
request against a protected resource that requires a token. The request then returns the content to the caller.
This method adds the acquired token in the HTTP Authorization header. For the sample application, the resource is the Microsoft Graph API me
endpoint, which displays the user's profile information.
Test the code
Now that you've set up the code, you need to test it:
Configure the server to listen to a TCP port that's based on the location of the index.html file. For Node.js, you can start the web server to listen to the port that you specified earlier. Run the following commands at a command-line prompt from the JavaScriptSPA folder:
npm install npm start
In the browser, enter
http://localhost:3000
. You should see the contents of the index.html file and a Sign In button on the upper right of the screen.
Important
Be sure to enable pop-ups and redirects for your site in your browser settings.
After the browser loads your index.html file, select Sign In. You're prompted to sign in with the Microsoft identity platform.
Provide consent for application access
The first time that you sign in to your application, you're prompted to grant it access to your profile and sign you in. Select Accept to continue.
View application results
After you sign in, you can select Read More under your displayed name. Your user profile information is returned in the displayed Microsoft Graph API response.
More information about scopes and delegated permissions
The Microsoft Graph API requires the User.Read
scope to read a user's profile. By default, this scope is automatically added in every application that's registered on the registration portal. Other APIs for Microsoft Graph, and custom APIs for your back-end server, might require more scopes. For example, the Microsoft Graph API requires the Mail.Read
scope to list the user's emails.
Note
The user might be prompted for additional consents as you increase the number of scopes.
Help and support
If you need help, want to report an issue, or want to learn about your support options, see Help and support for developers.
Next steps
Delve deeper into SPA development on the Microsoft identity platform in the first part of a scenario series:
Feedback
Submit and view feedback for