Firstly, sorry for the long post :)
Short Version
As a newbie to Microsoft Graph and the Microsoft cloud ecosystem I think there are too many tutorials about the same subject that have slight differences in their code snippets. This makes it difficult to ascertain which are the most authoritative.
Alongside, and possibly due to, this dynamic, I found it quite time consuming to 'pull apart' the tutorial code (https://docs.microsoft.com/en-us/graph/tutorials/javascript) quickly so that I could create my own interface and start playing around with the API.
I've included my own 'bare bones' version of the tutorial code in a post below in case it helps anyone else understand the tutorial components more quickly.
Caveats: I am a newbie and official code should be more trusted than my version of it. Also, to avoid any misunderstandings, I do appreciate the effort that went into the original tutorial code. Only tested in Chrome.
Long Version
Hello,
I came across Microsoft Graph about a week ago and was very keen to start playing around with the API, mainly with SharePoint and Lists, Groups, Teams and Planner.
Just documenting my experiences below as someone new to Microsoft Graph (and the MS cloud ecosystem in general).
I completed the tutorial below fairly painlessly and had a localhost
app up and running quickly:
https://docs.microsoft.com/en-us/graph/tutorials/javascript
When it came to trying to 'deconstruct' the components though, I got a bit overwhelmed and it has taken me about a week to get a fairly decent grasp of what is needed to create a 'single page app' and start making my own requests.
I've included my own 'bare bones' version of the tutorial code in a post below in case it helps anyone else understand the tutorial components more quickly (it's jQuery and Python'ish in style because they make things simpler for me:)).
I have several questions about it's content though (from a general logic perspective, not a specific programming perspective), such as:
- is the
cache
property in msalConfig
necessary? it is not shown in other tutorials.
- why are
MSAL
authentication and scope definition required in the graph.js
part of the application if the user has already signed in with msalApplication.loginPopup(scopes)
?
- why do
scopes
need to be defined in the app if they are defined in the Azure Active Directory App Registrations area?
- seeing as this is all browser side code, does
clientId
in config.js
need to be hidden somehow? or is that not necessary because the user has to login in order to make api requests?
In regards to the reasons I got overwhelmed, in the spirit of hopefully constructive feedback, I think it was because :
- there are many tutorials written about the same thing with slightly different config/authentication code snippets - it is hard to know which example is the most authoritative/correct , which is relevant to my scenario, and the purpose and relevance of several object properties in MSAL and Graph configurations.
I have come across this dynamic in other software companies, where lots of people are writing about the same thing. Some people think it is good because people have different learning styles. But you end up with 'developer docs', 'official docs', 'marketing docs', 'blog docs' and 'user docs' where the content overlaps, but with slight discrepancies, and the end-user is left unsure what is the most authoritative content.
- for me, there was too much
ui.js
code in the tutorial that I had to go through to see how it related to the configuration/authentication/request logic (after spending more time with it, I can see the logic is pretty separate, but I wasn't sure at first - the updatePage()
function and Views
variable threw me). anyway, that was the impetus behind creating a 'bare bones' version.
In regards to the first reason, here are some examples:
code snippets from these links are shown below for comparison:
A) https://docs.microsoft.com/en-us/graph/tutorials/javascript?tutorial-step=3
B) https://github.com/microsoftgraph/msgraph-sdk-javascript
C) https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-core
D) https://docs.microsoft.com/en-gb/azure/active-directory/develop/msal-js-initializing-client-applications
E) https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-sign-in?tabs=javascript
these links also have similar content:
https://docs.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa
https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v2-javascript
https://github.com/Azure-Samples/active-directory-javascript-graphapi-v2
https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki
https://docs.microsoft.com/en-us/graph/auth-register-app-v2
A) https://docs.microsoft.com/en-us/graph/tutorials/javascript?tutorial-step=3
// config.js
const msalConfig = {
auth: {
clientId: 'YOUR_APP_ID_HERE',
redirectUri: 'http://localhost:8080'
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
forceRefresh: false
}
};
const loginRequest = {
scopes: [
'openid',
'profile',
'user.read',
'calendars.read'
]
}
// auth.js
// Create the main MSAL instance
// configuration parameters are located in config.js
const msalClient = new Msal.UserAgentApplication(msalConfig);
if (msalClient.getAccount() && !msalClient.isCallback(window.location.hash)) {
// avoid duplicate code execution on page load in case of iframe and Popup window.
updatePage(msalClient.getAccount(), Views.home);
}
async function signIn() {
// Login
try {
await msalClient.loginPopup(loginRequest);
console.log('id_token acquired at: ' + new Date().toString());
if (msalClient.getAccount()) {
updatePage(msalClient.getAccount(), Views.home);
}
} catch (error) {
console.log(error);
updatePage(null, Views.error, {
message: 'Error logging in',
debug: error
});
}
}
function signOut() {
msalClient.logout();
}
// graph.js
// Create an options object with the same scopes from the login
const options =
new MicrosoftGraph.MSALAuthenticationProviderOptions([
'user.read',
'calendars.read'
]);
// Create an authentication provider for the implicit flow
const authProvider =
new MicrosoftGraph.ImplicitMSALAuthenticationProvider(msalClient, options);
// Initialize the Graph client
const graphClient = MicrosoftGraph.Client.initWithMiddleware({authProvider});
B) https://github.com/microsoftgraph/msgraph-sdk-javascript
// Configuration options for MSAL @see https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/MSAL.js-1.0.0-api-release#configuration-options
const msalConfig = {
auth: {
clientId: "your_client_id", // Client Id of the registered application
redirectUri: "your_redirect_uri",
},
};
const graphScopes = ["user.read", "mail.send"]; // An array of graph scopes
// Important Note: This library implements loginPopup and acquireTokenPopup flow, remember this while initializing the msal
// Initialize the MSAL @see https://github.com/AzureAD/microsoft-authentication-library-for-js#1-instantiate-the-useragentapplication
const msalApplication = new Msal.UserAgentApplication(msalConfig);
const options = new MicrosoftGraph.MSALAuthenticationProviderOptions(graphScopes);
const authProvider = new MicrosoftGraph.ImplicitMSALAuthenticationProvider(msalApplication, options);
const options = {
authProvider, // An instance created from previous step
};
const Client = MicrosoftGraph.Client;
const client = Client.initWithMiddleware(options);
C) https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-core
import * as Msal from "msal";
// if using cdn version, 'Msal' will be available in the global scope
const msalConfig = {
auth: {
clientId: 'your_client_id'
}
};
const msalInstance = new Msal.UserAgentApplication(msalConfig);
msalInstance.handleRedirectCallback((error, response) => {
// handle redirect response or error
});
var loginRequest = {
scopes: ["user.read", "mail.send"] // optional Array<string>
};
msalInstance.loginPopup(loginRequest)
.then(response => {
// handle response
})
.catch(err => {
// handle error
});
// if the user is already logged in you can acquire a token
if (msalInstance.getAccount()) {
var tokenRequest = {
scopes: ["user.read", "mail.send"]
};
msalInstance.acquireTokenSilent(tokenRequest)
.then(response => {
// get access token from response
// response.accessToken
})
.catch(err => {
// could also check if err instance of InteractionRequiredAuthError if you can import the class.
if (err.name === "InteractionRequiredAuthError") {
return msalInstance.acquireTokenPopup(tokenRequest)
.then(response => {
// get access token from response
// response.accessToken
})
.catch(err => {
// handle error
});
}
});
} else {
// user is not logged in, you will need to log them in to acquire a token
}
var headers = new Headers();
var bearer = "Bearer " + token;
headers.append("Authorization", bearer);
var options = {
method: "GET",
headers: headers
};
var graphEndpoint = "https://graph.microsoft.com/v1.0/me";
fetch(graphEndpoint, options)
.then(resp => {
//do something with response
});
D) https://docs.microsoft.com/en-gb/azure/active-directory/develop/msal-js-initializing-client-applications
// Configuration object constructed
const config = {
auth: {
clientId: "abcd-ef12-gh34-ikkl-ashdjhlhsdg"
}
}
// create UserAgentApplication instance
const myMSALObj = new UserAgentApplication(config);
function authCallback(error, response) {
//handle redirect response
}
// (optional when using redirect methods) register redirect call back for Success or Error
myMSALObj.handleRedirectCallback(authCallback);
type storage = "localStorage" | "sessionStorage";
// Protocol Support
export type AuthOptions = {
clientId: string;
authority?: string;
validateAuthority?: boolean;
redirectUri?: string | (() => string);
postLogoutRedirectUri?: string | (() => string);
navigateToLoginRequestUrl?: boolean;
};
// Cache Support
export type CacheOptions = {
cacheLocation?: CacheLocation;
storeAuthStateInCookie?: boolean;
};
// Library support
export type SystemOptions = {
logger?: Logger;
loadFrameTimeout?: number;
tokenRenewalOffsetSeconds?: number;
navigateFrameWait?: number;
};
// Developer App Environment Support
export type FrameworkOptions = {
isAngular?: boolean;
unprotectedResources?: Array<string>;
protectedResourceMap?: Map<string, Array<string>>;
};
// Configuration Object
export type Configuration = {
auth: AuthOptions,
cache?: CacheOptions,
system?: SystemOptions,
framework?: FrameworkOptions
};
E) https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-sign-in?tabs=javascript
const loginRequest = {
scopes: ["https://graph.microsoft.com/User.ReadWrite"]
}
userAgentApplication.loginPopup(loginRequest).then(function (loginResponse) {
//login success
let idToken = loginResponse.idToken;
}).catch(function (error) {
//login failure
console.log(error);
});
function authCallback(error, response) {
//handle redirect response
}
userAgentApplication.handleRedirectCallback(authCallback);
const loginRequest = {
scopes: ["https://graph.microsoft.com/User.ReadWrite"]
}
userAgentApplication.loginRedirect(loginRequest);
const config = {
auth: {
clientId: 'your_app_id',
redirectUri: "your_app_redirect_uri", //defaults to application start page
postLogoutRedirectUri: "your_app_logout_redirect_uri"
}
}
const userAgentApplication = new UserAgentApplication(config);
userAgentApplication.logout();
End of post.