17 |
18 |
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/App/styles.css:
--------------------------------------------------------------------------------
1 | .navbarStyle {
2 | padding: .5rem 1rem !important;
3 | }
4 |
5 | .profileButton {
6 | margin: .5rem .5rem;
7 | }
8 |
9 |
10 | .table-responsive-ms {
11 | max-height: 39rem !important;
12 | margin-left: 1.5rem;
13 | margin-right: 1.5rem;
14 | }
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/App/ui.js:
--------------------------------------------------------------------------------
1 | // Select DOM elements to work with
2 | const signInButton = document.getElementById('signIn');
3 | const signOutButton = document.getElementById('signOut')
4 | const titleDiv = document.getElementById('title-div');
5 | const welcomeDiv = document.getElementById('welcome-div');
6 | const tableDiv = document.getElementById('table-div');
7 | const tableBody = document.getElementById('table-body-div');
8 | const footerDiv = document.getElementById('footer');
9 | const editProfileButton = document.getElementById('editProfileButton');
10 | const table = document.getElementById('table');
11 |
12 | function welcomeUser(username) {
13 |
14 | signInButton.classList.add('d-none');
15 | signOutButton.classList.remove('d-none');
16 | titleDiv.classList.add('d-none');
17 | editProfileButton.classList.remove('d-none');
18 | welcomeDiv.classList.remove('d-none');
19 | welcomeDiv.innerHTML = `Welcome ${username}!`
20 | table.style.overflow = 'scroll';
21 | }
22 |
23 | function updateTable(idTokenClaims) {
24 | tableDiv.classList.remove('d-none');
25 | footerDiv.classList.remove('d-none');
26 | const tokenClaims = createClaimsTable(idTokenClaims);
27 | Object.keys(tokenClaims).forEach((key) => {
28 | let row = tableBody.insertRow(0);
29 | let cell1 = row.insertCell(0);
30 | let cell2 = row.insertCell(1);
31 | let cell3 = row.insertCell(2);
32 | cell1.innerHTML = tokenClaims[key][0];
33 | cell2.innerHTML = tokenClaims[key][1];
34 | cell3.innerHTML = tokenClaims[key][2];
35 | })
36 |
37 | }
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/AppCreationScripts/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "Sample": {
3 | "Title": "Vanilla JavaScript single-page application (SPA) using MSAL.js to authenticate users against Azure AD B2C",
4 | "Level": 100,
5 | "Client": "Vanilla JavaScript SPA",
6 | "RepositoryUrl": "ms-identity-javascript-tutorial",
7 | "Endpoint": "AAD v2.0",
8 | "Languages": ["javascript"],
9 | "Description": "Vanilla JavaScript single-page application (SPA) using MSAL.js to authenticate users against Azure AD B2C",
10 | "Products": ["azure-active-directory-b2c", "msal-js", "msal-browser"],
11 | "Platform": "JavaScript",
12 | "Provider": "B2C"
13 | },
14 |
15 | "AADApps": [
16 | {
17 | "Id": "client",
18 | "Name": "ms-identity-javascript-c1s2",
19 | "Kind": "SinglePageApplication",
20 | "Audience": "AzureADandPersonalMicrosoftAccount",
21 | "HomePage": "http://localhost:6420",
22 | "SampleSubPath": "1-Authentication\\2-sign-in-b2c",
23 | "ReplyUrls": "http://localhost:6420, http://localhost:6420/redirect"
24 | }
25 | ],
26 | "CodeConfiguration": [
27 | {
28 | "App": "client",
29 | "SettingKind": "Replace",
30 | "SettingFile": "\\..\\App\\src\\authConfig.js",
31 | "Mappings": [
32 | {
33 | "key": "Enter_the_Application_Id_Here",
34 | "value": ".AppId"
35 | },
36 | {
37 | "key": "policyName",
38 | "value": "Enter_The_Your_policy_Name"
39 | },
40 | {
41 | "key": "b2cDomain",
42 | "value": "Enter_The_Tenant_Domain_name"
43 | }
44 | ]
45 | }
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/ReadmeFiles/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/1-Authentication/2-sign-in-b2c/ReadmeFiles/screenshot.png
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/ReadmeFiles/topology_b2c_signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/1-Authentication/2-sign-in-b2c/ReadmeFiles/topology_b2c_signin.png
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ms-identity-b2c-javascript-c1s2",
3 | "version": "1.0.0",
4 | "description": "Vanilla JavaScript single-page application (SPA) using MSAL.js to authenticate users against Azure AD B2C",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "dev": "nodemon server.js",
9 | "test": "jest --forceExit"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-tutorial.git"
14 | },
15 | "keywords": [
16 | "javascript",
17 | "msal",
18 | "authorization code",
19 | "authentication",
20 | "microsoft",
21 | "ms-identity",
22 | "azure-ad-b2c",
23 | "single-page app"
24 | ],
25 | "author": "derisen",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial/issues"
29 | },
30 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial#readme",
31 | "dependencies": {
32 | "express": "^4.17.1",
33 | "morgan": "^1.10.0"
34 | },
35 | "devDependencies": {
36 | "jest": "^27.0.6",
37 | "nodemon": "^2.0.20",
38 | "supertest": "^6.1.4"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/sample.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 |
5 | const request = require('supertest');
6 | const path = require('path');
7 | const fs = require('fs');
8 |
9 | const app = require('./server.js');
10 |
11 | jest.dontMock('fs');
12 |
13 | const html = fs.readFileSync(path.resolve(__dirname, './App/index.html'), 'utf8');
14 |
15 | describe('Sanitize index page', () => {
16 | beforeAll(async() => {
17 | global.document.documentElement.innerHTML = html.toString();
18 | });
19 |
20 | it('should have valid cdn link', () => {
21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser");
22 | });
23 | });
24 |
25 | describe('Sanitize configuration object', () => {
26 | beforeAll(() => {
27 | global.msalConfig = require('./App/authConfig.js').msalConfig;
28 | });
29 |
30 | it('should define the config object', () => {
31 | expect(msalConfig).toBeDefined();
32 | });
33 |
34 | it('should contain credentials', () => {
35 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(true);
37 | });
38 |
39 | it('should contain authority URI', () => {
40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true);
42 | });
43 | });
44 |
45 | describe('Ensure pages served', () => {
46 |
47 | beforeAll(() => {
48 | process.env.NODE_ENV = 'test';
49 | });
50 |
51 | it('should get index page', async () => {
52 | const res = await request(app)
53 | .get('/');
54 |
55 | const data = await fs.promises.readFile(path.join(__dirname, './App/index.html'), 'utf8');
56 | expect(res.statusCode).toEqual(200);
57 | expect(res.text).toEqual(data);
58 | });
59 |
60 | it('should get signout page', async () => {
61 | const res = await request(app)
62 | .get('/signout');
63 |
64 | const data = await fs.promises.readFile(path.join(__dirname, './App/signout.html'), 'utf8');
65 | expect(res.statusCode).toEqual(200);
66 | expect(res.text).toEqual(data);
67 | });
68 | });
--------------------------------------------------------------------------------
/1-Authentication/2-sign-in-b2c/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const path = require('path');
4 |
5 | const DEFAULT_PORT = process.env.PORT || 6420;
6 |
7 | // initialize express.
8 | const app = express();
9 |
10 | // Configure morgan module to log all requests.
11 | app.use(morgan('dev'));
12 |
13 | // Setup app folders.
14 | app.use(express.static('App'));
15 |
16 | // Set up a route for signout.html
17 | app.get('/signout', (req, res) => {
18 | res.sendFile(path.join(__dirname + '/App/signout.html'));
19 | });
20 |
21 | app.get('/redirect', (req, res) => {
22 | res.sendFile(path.join(__dirname + '/App/redirect.html'));
23 | });
24 |
25 | // Set up a route for index.html
26 | app.get('*', (req, res) => {
27 | res.sendFile(path.join(__dirname + '/index.html'));
28 | });
29 |
30 | app.listen(DEFAULT_PORT, () => {
31 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`)
32 | });
33 |
34 | module.exports = app;
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/authConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration object to be passed to MSAL instance on creation.
3 | * For a full list of MSAL.js configuration parameters, visit:
4 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
5 | */
6 | const msalConfig = {
7 | auth: {
8 | clientId: 'Enter_the_Application_Id_Here', // This is the ONLY mandatory field that you need to supply.
9 | authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Id_Here', // Defaults to "https://login.microsoftonline.com/common"
10 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.href
11 | postLogoutRedirectUri: '/', //Indicates the page to navigate after logout.
12 | clientCapabilities: ['CP1'], // this lets the resource owner know that this client is capable of handling claims challenge.
13 | },
14 | cache: {
15 | cacheLocation: 'localStorage', // This configures where your cache will be stored
16 | storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
17 | },
18 | system: {
19 | /**
20 | * Below you can configure MSAL.js logs. For more information, visit:
21 | * https://docs.microsoft.com/azure/active-directory/develop/msal-logging-js
22 | */
23 | loggerOptions: {
24 | loggerCallback: (level, message, containsPii) => {
25 | if (containsPii) {
26 | return;
27 | }
28 | switch (level) {
29 | case msal.LogLevel.Error:
30 | console.error(message);
31 | return;
32 | case msal.LogLevel.Info:
33 | console.info(message);
34 | return;
35 | case msal.LogLevel.Verbose:
36 | console.debug(message);
37 | return;
38 | case msal.LogLevel.Warning:
39 | console.warn(message);
40 | return;
41 | default:
42 | return;
43 | }
44 | },
45 | },
46 | },
47 | };
48 |
49 | // Add here the endpoints for MS Graph API services you would like to use.
50 | const graphConfig = {
51 | graphMeEndpoint: {
52 | uri: 'https://graph.microsoft.com/v1.0/me',
53 | scopes: ['User.Read'],
54 | },
55 | graphContactsEndpoint: {
56 | uri: 'https://graph.microsoft.com/v1.0/me/contacts',
57 | scopes: ['Contacts.Read'],
58 | },
59 | };
60 |
61 | /**
62 | * Scopes you add here will be prompted for user consent during sign-in.
63 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
64 | * For more information about OIDC scopes, visit:
65 | * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
66 | */
67 | const loginRequest = {
68 | scopes: ["User.Read"]
69 | };
70 |
71 | // exporting config object for jest
72 | if (typeof exports !== 'undefined') {
73 | module.exports = {
74 | msalConfig: msalConfig,
75 | graphConfig: graphConfig
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/authPopup.js:
--------------------------------------------------------------------------------
1 | // Create the main myMSALObj instance
2 | // configuration parameters are located at authConfig.js
3 | const myMSALObj = new msal.PublicClientApplication(msalConfig);
4 |
5 | let username = '';
6 |
7 | /**
8 | * This method adds an event callback function to the MSAL object
9 | * to handle the response from redirect flow. For more information, visit:
10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md
11 | */
12 | myMSALObj.addEventCallback((event) => {
13 | if (
14 | (event.eventType === msal.EventType.LOGIN_SUCCESS ||
15 | event.eventType === msal.EventType.ACQUIRE_TOKEN_SUCCESS) &&
16 | event.payload.account
17 | ) {
18 | const account = event.payload.account;
19 | myMSALObj.setActiveAccount(account);
20 | }
21 |
22 | if (event.eventType === msal.EventType.LOGOUT_SUCCESS) {
23 | if (myMSALObj.getAllAccounts().length > 0) {
24 | myMSALObj.setActiveAccount(myMSALObj.getAllAccounts()[0]);
25 | }
26 | }
27 | });
28 |
29 | function selectAccount() {
30 | /**
31 | * See here for more info on account retrieval:
32 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
33 | */
34 | const currentAccounts = myMSALObj.getAllAccounts();
35 | if (currentAccounts === null) {
36 | return;
37 | } else if (currentAccounts.length >= 1) {
38 | // Add choose account code here
39 | username = myMSALObj.getActiveAccount().username;
40 | showWelcomeMessage(username, currentAccounts);
41 | }
42 | }
43 |
44 | async function addAnotherAccount(event) {
45 | if (event.target.innerHTML.includes('@')) {
46 | const username = event.target.innerHTML;
47 | const account = myMSALObj.getAllAccounts().find((account) => account.username === username);
48 | const activeAccount = myMSALObj.getActiveAccount();
49 | if (account && activeAccount.homeAccountId != account.homeAccountId) {
50 | try {
51 | myMSALObj.setActiveAccount(account);
52 | let res = await myMSALObj.ssoSilent({
53 | ...loginRequest,
54 | account: account,
55 | });
56 | closeModal();
57 | handleResponse(res);
58 | window.location.reload();
59 | } catch (error) {
60 | if (error instanceof msal.InteractionRequiredAuthError) {
61 | let res = await myMSALObj.loginPopup({
62 | ...loginRequest,
63 | prompt: 'login',
64 | });
65 | handleResponse(res);
66 | window.location.reload();
67 | }
68 | }
69 | } else {
70 | closeModal();
71 | }
72 | } else {
73 | try {
74 | myMSALObj.setActiveAccount(null);
75 | const res = await myMSALObj.loginPopup({
76 | ...loginRequest,
77 | prompt: 'login',
78 | });
79 | handleResponse(res);
80 | closeModal();
81 | window.location.reload();
82 | } catch (error) {
83 | console.log(error);
84 | }
85 | }
86 | }
87 |
88 | function handleResponse(response) {
89 | /**
90 | * To see the full list of response object properties, visit:
91 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response
92 | */
93 |
94 | if (response !== null) {
95 | const accounts = myMSALObj.getAllAccounts();
96 | username = response.account.username;
97 | showWelcomeMessage(username, accounts);
98 | } else {
99 | selectAccount();
100 | }
101 | }
102 |
103 | function signIn() {
104 | /**
105 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
106 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
107 | */
108 |
109 | myMSALObj
110 | .loginPopup(loginRequest)
111 | .then(handleResponse)
112 | .catch((error) => {
113 | console.error(error);
114 | });
115 | }
116 |
117 | function signOut() {
118 | /**
119 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
120 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
121 | */
122 | const account = myMSALObj.getAccountByUsername(username);
123 | const logoutRequest = {
124 | account: account,
125 | mainWindowRedirectUri: '/',
126 | };
127 | clearStorage(account);
128 | myMSALObj.logoutPopup(logoutRequest).catch((error) => {
129 | console.log(error);
130 | });
131 | }
132 |
133 | function seeProfile() {
134 | callGraph(
135 | username,
136 | graphConfig.graphMeEndpoint.scopes,
137 | graphConfig.graphMeEndpoint.uri,
138 | msal.InteractionType.Popup,
139 | myMSALObj
140 | );
141 | }
142 |
143 | function readContacts() {
144 | callGraph(
145 | username,
146 | graphConfig.graphContactsEndpoint.scopes,
147 | graphConfig.graphContactsEndpoint.uri,
148 | msal.InteractionType.Popup,
149 | myMSALObj
150 | );
151 | }
152 |
153 | selectAccount();
154 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/authRedirect.js:
--------------------------------------------------------------------------------
1 | // Create the main myMSALObj instance
2 | // configuration parameters are located at authConfig.js
3 | const myMSALObj = new msal.PublicClientApplication(msalConfig);
4 |
5 | let username = '';
6 |
7 | /**
8 | * This method adds an event callback function to the MSAL object
9 | * to handle the response from redirect flow. For more information, visit:
10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md
11 | */
12 | myMSALObj.addEventCallback((event) => {
13 | if (
14 | (event.eventType === msal.EventType.LOGIN_SUCCESS ||
15 | event.eventType === msal.EventType.ACQUIRE_TOKEN_SUCCESS) &&
16 | event.payload.account
17 | ) {
18 | const account = event.payload.account;
19 | myMSALObj.setActiveAccount(account);
20 | }
21 |
22 | if (event.eventType === msal.EventType.LOGOUT_SUCCESS) {
23 | if (myMSALObj.getAllAccounts().length > 0) {
24 | myMSALObj.setActiveAccount(myMSALObj.getAllAccounts()[0]);
25 | }
26 | }
27 | });
28 |
29 | /**
30 | * A promise handler needs to be registered for handling the
31 | * response returned from redirect flow. For more information, visit:
32 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/acquire-token.md
33 | */
34 | myMSALObj
35 | .handleRedirectPromise()
36 | .then(handleResponse)
37 | .catch((error) => {
38 | console.error(error);
39 | });
40 |
41 | function selectAccount() {
42 | /**
43 | * See here for more info on account retrieval:
44 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
45 | */
46 |
47 | const currentAccounts = myMSALObj.getAllAccounts();
48 |
49 | if (!currentAccounts) {
50 | return;
51 | } else if (currentAccounts.length >= 1) {
52 | // Add your account choosing logic here
53 | username = myMSALObj.getActiveAccount().username;
54 | showWelcomeMessage(username, currentAccounts);
55 | }
56 | }
57 |
58 | async function addAnotherAccount(event) {
59 | if (event.target.innerHTML.includes("@")) {
60 | const username = event.target.innerHTML;
61 | const account = myMSALObj.getAllAccounts().find((account) => account.username === username);
62 | const activeAccount = myMSALObj.getActiveAccount();
63 | if (account && activeAccount.homeAccountId != account.homeAccountId) {
64 | try {
65 | myMSALObj.setActiveAccount(account);
66 | let res = await myMSALObj.ssoSilent({
67 | ...loginRequest,
68 | account: account,
69 | });
70 | handleResponse(res);
71 | closeModal();
72 | window.location.reload();
73 | } catch (error) {
74 | if (error instanceof msal.InteractionRequiredAuthError) {
75 | await myMSALObj.loginRedirect({
76 | ...loginRequest,
77 | prompt: 'login',
78 | });
79 | }
80 | }
81 | } else {
82 | closeModal();
83 | }
84 | } else {
85 | try {
86 | myMSALObj.setActiveAccount(null);
87 | await myMSALObj.loginRedirect({
88 | ...loginRequest,
89 | prompt: 'login',
90 | });
91 | } catch (error) {
92 | console.log(error);
93 | }
94 | }
95 | }
96 |
97 | function handleResponse(response) {
98 | if (response !== null) {
99 | const accounts = myMSALObj.getAllAccounts();
100 | username = response.account.username;
101 | showWelcomeMessage(username, accounts);
102 | } else {
103 | selectAccount();
104 | }
105 | }
106 |
107 | function signIn() {
108 |
109 | /**
110 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
111 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
112 | */
113 |
114 | myMSALObj.loginRedirect(loginRequest);
115 | }
116 |
117 | function signOut() {
118 |
119 | /**
120 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
121 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
122 | */
123 |
124 | // Choose which account to logout from by passing a username.
125 | const account = myMSALObj.getAccountByUsername(username);
126 | const logoutRequest = {
127 | account: account,
128 | loginHint: account.idTokenClaims.login_hint,
129 | };
130 |
131 | clearStorage(account);
132 | myMSALObj.logoutRedirect(logoutRequest);
133 | }
134 |
135 | function seeProfile() {
136 | callGraph(
137 | username,
138 | graphConfig.graphMeEndpoint.scopes,
139 | graphConfig.graphMeEndpoint.uri,
140 | msal.InteractionType.Redirect,
141 | myMSALObj
142 | );
143 | }
144 |
145 | function readContacts() {
146 | callGraph(
147 | username,
148 | graphConfig.graphContactsEndpoint.scopes,
149 | graphConfig.graphContactsEndpoint.uri,
150 | msal.InteractionType.Redirect,
151 | myMSALObj
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/favicon.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/fetch.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License.
4 | */
5 |
6 | /**
7 | * This method calls the Graph API by utilizing the graph client instance.
8 | * @param {String} username
9 | * @param {Array} scopes
10 | * @param {String} uri
11 | * @param {String} interactionType
12 | * @param {Object} myMSALObj
13 | * @returns
14 | */
15 | const callGraph = async (username, scopes, uri, interactionType, myMSALObj) => {
16 | const account = myMSALObj.getAccountByUsername(username);
17 | try {
18 | let response = await getGraphClient({
19 | account: account,
20 | scopes: scopes,
21 | interactionType: interactionType,
22 | })
23 | .api(uri)
24 | .responseType('raw')
25 | .get();
26 |
27 | response = await handleClaimsChallenge(account, response, uri);
28 | if (response && response.error === 'claims_challenge_occurred') throw response.error;
29 | updateUI(response, uri);
30 | } catch (error) {
31 | if (error === 'claims_challenge_occurred') {
32 | const resource = new URL(uri).hostname;
33 | const claims =
34 | account &&
35 | getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`)
36 | ? window.atob(
37 | getClaimsFromStorage(
38 | `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`
39 | )
40 | )
41 | : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}}
42 | let request = {
43 | account: account,
44 | scopes: scopes,
45 | claims: claims,
46 | };
47 | switch (interactionType) {
48 | case msal.InteractionType.Popup:
49 |
50 | await myMSALObj.acquireTokenPopup({
51 | ...request,
52 | redirectUri: '/redirect',
53 | });
54 | break;
55 | case msal.InteractionType.Redirect:
56 | await myMSALObj.acquireTokenRedirect(request);
57 | break;
58 | default:
59 | await myMSALObj.acquireTokenRedirect(request);
60 | break;
61 | }
62 | } else if (error.toString().includes('404')) {
63 | return updateUI(null, uri);
64 | } else {
65 | console.log(error);
66 | }
67 | }
68 | }
69 |
70 | /**
71 | * This method inspects the HTTPS response from a fetch call for the "www-authenticate header"
72 | * If present, it grabs the claims challenge from the header and store it in the localStorage
73 | * For more information, visit: https://docs.microsoft.com/en-us/azure/active-directory/develop/claims-challenge#claims-challenge-header-format
74 | * @param {object} response
75 | * @returns response
76 | */
77 | const handleClaimsChallenge = async (account, response, apiEndpoint) => {
78 | if (response.status === 200) {
79 | return response.json();
80 | } else if (response.status === 401) {
81 | if (response.headers.get('WWW-Authenticate')) {
82 | const authenticateHeader = response.headers.get('WWW-Authenticate');
83 | const claimsChallenge = parseChallenges(authenticateHeader);
84 | /**
85 | * This method stores the claim challenge to the session storage in the browser to be used when acquiring a token.
86 | * To ensure that we are fetching the correct claim from the storage, we are using the clientId
87 | * of the application and oid (user’s object id) as the key identifier of the claim with schema
88 | * cc...
89 | */
90 | addClaimsToStorage(
91 | claimsChallenge.claims,
92 | `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${new URL(apiEndpoint).hostname}`
93 | );
94 | return { error: 'claims_challenge_occurred', payload: claimsChallenge.claims };
95 | }
96 |
97 | throw new Error(`Unauthorized: ${response.status}`);
98 | } else {
99 | throw new Error(`Something went wrong with the request: ${response.status}`);
100 | }
101 | };
102 |
103 | /**
104 | * This method parses WWW-Authenticate authentication headers
105 | * @param header
106 | * @return {Object} challengeMap
107 | */
108 | const parseChallenges = (header) => {
109 | const schemeSeparator = header.indexOf(' ');
110 | const challenges = header.substring(schemeSeparator + 1).split(', ');
111 | const challengeMap = {};
112 |
113 | challenges.forEach((challenge) => {
114 | const [key, value] = challenge.split('=');
115 | challengeMap[key.trim()] = window.decodeURI(value.replace(/(^"|"$)/g, ''));
116 | });
117 |
118 | return challengeMap;
119 | }
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/graph.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The code below demonstrates how you can use MSAL as a custom authentication provider for the Microsoft Graph JavaScript SDK.
3 | * You do NOT need to implement a custom provider. Microsoft Graph JavaScript SDK v3.0 (preview) offers AuthCodeMSALBrowserAuthenticationProvider
4 | * which handles token acquisition and renewal for you automatically. For more information on how to use it, visit:
5 | * https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/AuthCodeMSALBrowserAuthenticationProvider.md
6 | */
7 |
8 | /**
9 | * Returns a graph client object with the provided token acquisition options
10 | * @param {Object} providerOptions: object containing user account, required scopes and interaction type
11 | */
12 | const getGraphClient = (providerOptions) => {
13 |
14 | /**
15 | * Pass the instance as authProvider in ClientOptions to instantiate the Client which will create and set the default middleware chain.
16 | * For more information, visit: https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md
17 | */
18 | let clientOptions = {
19 | authProvider: new MsalAuthenticationProvider(providerOptions),
20 | };
21 |
22 | const graphClient = MicrosoftGraph.Client.initWithMiddleware(clientOptions);
23 |
24 | return graphClient;
25 | };
26 |
27 | /**
28 | * This class implements the IAuthenticationProvider interface, which allows a custom authentication provider to be
29 | * used with the Graph client. See: https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/src/IAuthenticationProvider.ts
30 | */
31 | class MsalAuthenticationProvider {
32 |
33 | account; // user account object to be used when attempting silent token acquisition
34 | scopes; // array of scopes required for this resource endpoint
35 | interactionType; // type of interaction to fallback to when silent token acquisition fails
36 | claims;
37 |
38 | constructor(providerOptions) {
39 | this.account = providerOptions.account;
40 | this.scopes = providerOptions.scopes;
41 | this.interactionType = providerOptions.interactionType;
42 | const resource = new URL(graphConfig.graphMeEndpoint.uri).hostname;
43 | this.claims =
44 | this.account && getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${this.account.idTokenClaims.oid}.${resource}`)
45 | ? window.atob(
46 | getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${this.account.idTokenClaims.oid}.${resource}`)
47 | )
48 | : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}}
49 | }
50 |
51 | /**
52 | * This method will get called before every request to the ms graph server
53 | * This should return a Promise that resolves to an accessToken (in case of success) or rejects with error (in case of failure)
54 | * Basically this method will contain the implementation for getting and refreshing accessTokens
55 | */
56 | getAccessToken() {
57 | return new Promise(async (resolve, reject) => {
58 | let response;
59 |
60 | try {
61 | response = await myMSALObj.acquireTokenSilent({
62 | account: this.account,
63 | scopes: this.scopes,
64 | claims: this.claims
65 | });
66 |
67 | if (response.accessToken) {
68 | resolve(response.accessToken);
69 | } else {
70 | reject(Error('Failed to acquire an access token'));
71 | }
72 | } catch (error) {
73 | // in case if silent token acquisition fails, fallback to an interactive method
74 | if (error instanceof msal.InteractionRequiredAuthError) {
75 | switch (this.interactionType) {
76 | case msal.InteractionType.Popup:
77 | response = await myMSALObj.acquireTokenPopup({
78 | scopes: this.scopes,
79 | claims: this.claims,
80 | redirectUri: '/redirect',
81 | });
82 |
83 | if (response.accessToken) {
84 | resolve(response.accessToken);
85 | } else {
86 | reject(Error('Failed to acquire an access token'));
87 | }
88 | break;
89 |
90 | case msal.InteractionType.Redirect:
91 | /**
92 | * This will cause the app to leave the current page and redirect to the consent screen.
93 | * Once consent is provided, the app will return back to the current page and then the
94 | * silent token acquisition will succeed.
95 | */
96 | myMSALObj.acquireTokenRedirect({
97 | scopes: this.scopes,
98 | claims: this.claims,
99 | });
100 | break;
101 |
102 | default:
103 | break;
104 | }
105 | }
106 | }
107 | });
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/redirect.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/styles.css:
--------------------------------------------------------------------------------
1 | .navbarStyle {
2 | padding: .5rem 1rem !important;
3 | }
4 |
5 | .dropdown-toggle {
6 | visibility: hidden;
7 | display: none !important;
8 | }
9 |
10 | .list-group-item {
11 | cursor: pointer;
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/ui.js:
--------------------------------------------------------------------------------
1 | // Select DOM elements to work with
2 | const welcomeDiv = document.getElementById("WelcomeMessage");
3 | const signInButton = document.getElementById("SignIn");
4 | const dropdownButton = document.getElementById('dropdownMenuButton1');
5 | const cardDiv = document.getElementById("card-div");
6 | const mailButton = document.getElementById("readMail");
7 | const profileButton = document.getElementById("seeProfile");
8 | const profileDiv = document.getElementById("profile-div");
9 | const listGroup = document.getElementById('list-group');
10 |
11 | function showWelcomeMessage(username, accounts) {
12 | // Reconfiguring DOM elements
13 | cardDiv.style.display = 'initial';
14 | signInButton.style.visibility = 'hidden';
15 | welcomeDiv.innerHTML = `Welcome ${username}`;
16 | dropdownButton.setAttribute('style', 'display:inline !important; visibility:visible');
17 | dropdownButton.innerHTML = username;
18 | accounts.forEach(account => {
19 | let item = document.getElementById(account.username);
20 | if (!item) {
21 | const listItem = document.createElement('li');
22 | listItem.setAttribute('onclick', 'addAnotherAccount(event)');
23 | listItem.setAttribute('id', account.username);
24 | listItem.innerHTML = account.username;
25 | if (account.username === username) {
26 | listItem.setAttribute('class', 'list-group-item active');
27 | } else {
28 | listItem.setAttribute('class', 'list-group-item');
29 | }
30 | listGroup.appendChild(listItem);
31 | } else {
32 | if (account.username === username) {
33 | item.setAttribute('class', 'list-group-item active');
34 | } else {
35 | item.setAttribute('active', 'list-group-item');
36 | }
37 | }
38 | });
39 | }
40 |
41 | function closeModal() {
42 | const element = document.getElementById("closeModal");
43 | element.click();
44 | }
45 |
46 | function updateUI(data, endpoint) {
47 | console.log('Graph API responded at: ' + new Date().toString());
48 |
49 | if (endpoint === graphConfig.graphMeEndpoint.uri) {
50 | profileDiv.innerHTML = '';
51 | const title = document.createElement('p');
52 | title.innerHTML = "Title: " + data.jobTitle;
53 | const email = document.createElement('p');
54 | email.innerHTML = "Mail: " + data.mail;
55 | const phone = document.createElement('p');
56 | phone.innerHTML = "Phone: " + data.businessPhones[0];
57 | const address = document.createElement('p');
58 | address.innerHTML = "Location: " + data.officeLocation;
59 | profileDiv.appendChild(title);
60 | profileDiv.appendChild(email);
61 | profileDiv.appendChild(phone);
62 | profileDiv.appendChild(address);
63 |
64 | } else if (endpoint === graphConfig.graphContactsEndpoint.uri) {
65 | if (!data || data.value.length < 1) {
66 | alert('Your contacts is empty!');
67 | } else {
68 | const tabList = document.getElementById('list-tab');
69 | tabList.innerHTML = ''; // clear tabList at each readMail call
70 |
71 | data.value.map((d, i) => {
72 | if (i < 10) {
73 | const listItem = document.createElement('a');
74 | listItem.setAttribute('class', 'list-group-item list-group-item-action');
75 | listItem.setAttribute('id', 'list' + i + 'list');
76 | listItem.setAttribute('data-toggle', 'list');
77 | listItem.setAttribute('href', '#list' + i);
78 | listItem.setAttribute('role', 'tab');
79 | listItem.setAttribute('aria-controls', i);
80 | listItem.innerHTML =
81 | ' Name: ' + d.displayName + '
' + 'Note: ' + d.personalNotes + '...';
82 | tabList.appendChild(listItem);
83 | }
84 | });
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/App/utils/storageUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This method stores the claim challenge to the localStorage in the browser to be used when acquiring a token
3 | * @param {String} claimsChallenge
4 | */
5 | const addClaimsToStorage = (claimsChallenge, claimsChallengeId) => {
6 | sessionStorage.setItem(claimsChallengeId, claimsChallenge);
7 | };
8 |
9 | /**
10 | * This method retrieves the claims challenge from the localStorage
11 | * @param {string} claimsChallengeId
12 | * @returns
13 | */
14 | const getClaimsFromStorage = (claimsChallengeId) => {
15 | return sessionStorage.getItem(claimsChallengeId);
16 | };
17 |
18 | /**
19 | * This method clears localStorage of any claims challenge entry
20 | * @param {Object} account
21 | */
22 | const clearStorage = (account) => {
23 | for (var key in sessionStorage) {
24 | if (key.startsWith(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}`))
25 | sessionStorage.removeItem(key);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/AppCreationScripts/Cleanup.ps1:
--------------------------------------------------------------------------------
1 |
2 | [CmdletBinding()]
3 | param(
4 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')]
5 | [string] $tenantId,
6 | [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')]
7 | [string] $azureEnvironmentName
8 | )
9 |
10 |
11 | Function Cleanup
12 | {
13 | if (!$azureEnvironmentName)
14 | {
15 | $azureEnvironmentName = "Global"
16 | }
17 |
18 | <#
19 | .Description
20 | This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script
21 | #>
22 |
23 | # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant
24 | # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD.
25 |
26 | # Connect to the Microsoft Graph API
27 | Write-Host "Connecting to Microsoft Graph"
28 |
29 |
30 | if ($tenantId -eq "")
31 | {
32 | Connect-MgGraph -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName
33 | }
34 | else
35 | {
36 | Connect-MgGraph -TenantId $tenantId -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName
37 | }
38 |
39 | $context = Get-MgContext
40 | $tenantId = $context.TenantId
41 |
42 | # Get the user running the script
43 | $currentUserPrincipalName = $context.Account
44 | $user = Get-MgUser -Filter "UserPrincipalName eq '$($context.Account)'"
45 |
46 | # get the tenant we signed in to
47 | $Tenant = Get-MgOrganization
48 | $tenantName = $Tenant.DisplayName
49 |
50 | $verifiedDomain = $Tenant.VerifiedDomains | where {$_.Isdefault -eq $true}
51 | $verifiedDomainName = $verifiedDomain.Name
52 | $tenantId = $Tenant.Id
53 |
54 | Write-Host ("Connected to Tenant {0} ({1}) as account '{2}'. Domain is '{3}'" -f $Tenant.DisplayName, $Tenant.Id, $currentUserPrincipalName, $verifiedDomainName)
55 |
56 | # Removes the applications
57 | Write-Host "Cleaning-up applications from tenant '$tenantId'"
58 |
59 | Write-Host "Removing 'client' (ms-identity-javascript-c2s1) if needed"
60 | try
61 | {
62 | Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-MgApplication -ApplicationId $_.Id }
63 | }
64 | catch
65 | {
66 | $message = $_
67 | Write-Warning $Error[0]
68 | Write-Host "Unable to remove the application 'ms-identity-javascript-c2s1'. Error is $message. Try deleting manually." -ForegroundColor White -BackgroundColor Red
69 | }
70 |
71 | Write-Host "Making sure there are no more (ms-identity-javascript-c2s1) applications found, will remove if needed..."
72 | $apps = Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" | Format-List Id, DisplayName, AppId, SignInAudience, PublisherDomain
73 |
74 | if ($apps)
75 | {
76 | Remove-MgApplication -ApplicationId $apps.Id
77 | }
78 |
79 | foreach ($app in $apps)
80 | {
81 | Remove-MgApplication -ApplicationId $app.Id
82 | Write-Host "Removed ms-identity-javascript-c2s1.."
83 | }
84 |
85 | # also remove service principals of this app
86 | try
87 | {
88 | Get-MgServicePrincipal -filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-MgServicePrincipal -ServicePrincipalId $_.Id -Confirm:$false}
89 | }
90 | catch
91 | {
92 | $message = $_
93 | Write-Warning $Error[0]
94 | Write-Host "Unable to remove ServicePrincipal 'ms-identity-javascript-c2s1'. Error is $message. Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red
95 | }
96 | }
97 |
98 | # Pre-requisites
99 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph")) {
100 | Install-Module "Microsoft.Graph" -Scope CurrentUser
101 | }
102 |
103 | #Import-Module Microsoft.Graph
104 |
105 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) {
106 | Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser
107 | }
108 |
109 | Import-Module Microsoft.Graph.Authentication
110 |
111 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Identity.DirectoryManagement")) {
112 | Install-Module "Microsoft.Graph.Identity.DirectoryManagement" -Scope CurrentUser
113 | }
114 |
115 | Import-Module Microsoft.Graph.Identity.DirectoryManagement
116 |
117 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) {
118 | Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser
119 | }
120 |
121 | Import-Module Microsoft.Graph.Applications
122 |
123 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Groups")) {
124 | Install-Module "Microsoft.Graph.Groups" -Scope CurrentUser
125 | }
126 |
127 | Import-Module Microsoft.Graph.Groups
128 |
129 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Users")) {
130 | Install-Module "Microsoft.Graph.Users" -Scope CurrentUser
131 | }
132 |
133 | Import-Module Microsoft.Graph.Users
134 |
135 | $ErrorActionPreference = "Stop"
136 |
137 |
138 | try
139 | {
140 | Cleanup -tenantId $tenantId -environment $azureEnvironmentName
141 | }
142 | catch
143 | {
144 | $_.Exception.ToString() | out-host
145 | $message = $_
146 | Write-Warning $Error[0]
147 | Write-Host "Unable to register apps. Error is $message." -ForegroundColor White -BackgroundColor Red
148 | }
149 |
150 | Write-Host "Disconnecting from tenant"
151 | Disconnect-MgGraph
152 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/AppCreationScripts/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "Sample": {
3 | "Title": "Vanilla JavaScript single-page application using MSAL.js to authenticate users to call Microsoft Graph",
4 | "Level": 100,
5 | "Client": "Vanilla JavaScript SPA",
6 | "Service": "Microsoft Graph",
7 | "RepositoryUrl": "ms-identity-javascript-tutorial",
8 | "Endpoint": "AAD v2.0",
9 | "Languages": ["javascript"],
10 | "Description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users and calling the Microsoft Graph API on their behalf",
11 | "Products": ["azure-active-directory", "msal-js", "msal-browser"],
12 | "Platform": "JavaScript"
13 | },
14 | "AADApps": [
15 | {
16 | "Id": "client",
17 | "Name": "ms-identity-javascript-c2s1",
18 | "Kind": "SinglePageApplication",
19 | "HomePage": "http://localhost:3000/",
20 | "SampleSubPath": "2-Authorization-I\\1-call-graph",
21 | "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect",
22 | "Audience": "AzureADMyOrg",
23 | "OptionalClaims": {
24 | "IdTokenClaims": ["acct", "login_hint"]
25 | },
26 | "RequiredResourcesAccess": [
27 | {
28 | "Resource": "Microsoft Graph",
29 | "DelegatedPermissions": ["User.Read", "Contacts.Read"]
30 | }
31 | ]
32 | }
33 | ],
34 | "CodeConfiguration": [
35 | {
36 | "App": "client",
37 | "SettingKind": "Replace",
38 | "SettingFile": "\\..\\App\\authConfig.js",
39 | "Mappings": [
40 | {
41 | "key": "Enter_the_Application_Id_Here",
42 | "value": ".AppId"
43 | },
44 | {
45 | "key": "Enter_the_Tenant_Id_Here",
46 | "value": "$tenantId"
47 | }
48 | ]
49 | }
50 | ]
51 | }
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/ReadmeFiles/topology_callgraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/2-Authorization-I/1-call-graph/ReadmeFiles/topology_callgraph.png
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ms-identity-javascript-c2s1",
3 | "version": "1.0.0",
4 | "description": "Vanilla JavaScript single-page application using MSAL.js to authorize users for calling Microsoft Graph",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "dev": "nodemon server.js",
9 | "test": "jest --forceExit"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-tutorial.git"
14 | },
15 | "keywords": [
16 | "javascript",
17 | "msal",
18 | "authorization",
19 | "code",
20 | "authentication",
21 | "microsoft",
22 | "ms-identity",
23 | "azure-ad",
24 | "spa",
25 | "node.js",
26 | "msal.js"
27 | ],
28 | "author": "derisen",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial/issues"
32 | },
33 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial#readme",
34 | "dependencies": {
35 | "express": "^4.17.1",
36 | "morgan": "^1.10.0"
37 | },
38 | "devDependencies": {
39 | "jest": "^27.0.6",
40 | "nodemon": "^2.0.20",
41 | "supertest": "^6.1.4"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/sample.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 |
5 | const request = require('supertest');
6 | const path = require('path');
7 | const fs = require('fs');
8 |
9 | const app = require('./server.js');
10 |
11 | jest.dontMock('fs');
12 |
13 | const html = fs.readFileSync(path.resolve(__dirname, './App/index.html'), 'utf8');
14 |
15 | describe('Sanitize index page', () => {
16 | beforeAll(async() => {
17 | global.document.documentElement.innerHTML = html.toString();
18 | });
19 |
20 | it('should have valid cdn link', () => {
21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser");
22 | });
23 | });
24 |
25 | describe('Sanitize configuration object', () => {
26 | beforeAll(() => {
27 | global.msalConfig = require('./App/authConfig.js').msalConfig;
28 | });
29 |
30 | it('should define the config object', () => {
31 | expect(msalConfig).toBeDefined();
32 | });
33 |
34 | it('should not contain credentials', () => {
35 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(false);
37 | });
38 |
39 | it('should contain authority URI', () => {
40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true);
42 | });
43 | });
44 |
45 | describe('Ensure pages served', () => {
46 |
47 | beforeAll(() => {
48 | process.env.NODE_ENV = 'test';
49 | });
50 |
51 | it('should get index page', async () => {
52 | const res = await request(app)
53 | .get('/');
54 |
55 | const data = await fs.promises.readFile(path.join(__dirname, './App/index.html'), 'utf8');
56 | expect(res.statusCode).toEqual(200);
57 | expect(res.text).toEqual(data);
58 | });
59 | });
--------------------------------------------------------------------------------
/2-Authorization-I/1-call-graph/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const path = require('path');
4 |
5 | const DEFAULT_PORT = process.env.PORT || 3000;
6 |
7 | // initialize express.
8 | const app = express();
9 |
10 | // Configure morgan module to log all requests.
11 | app.use(morgan('dev'));
12 |
13 | // Setup app folders.
14 | app.use(express.static('App'));
15 |
16 | app.get('/redirect', (req, res) => {
17 | res.sendFile(path.join(__dirname + '/App/redirect.html'));
18 | });
19 |
20 | // Set up a route for index.html
21 | app.get('*', (req, res) => {
22 | res.sendFile(path.join(__dirname + '/index.html'));
23 | });
24 |
25 | app.listen(DEFAULT_PORT, () => {
26 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`)
27 | });
28 |
29 | module.exports = app;
30 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const cors = require('cors');
4 | const rateLimit = require('express-rate-limit');
5 |
6 | const passport = require('passport');
7 | const passportAzureAd = require('passport-azure-ad');
8 |
9 | const authConfig = require('./authConfig');
10 | const router = require('./routes/index');
11 |
12 | const app = express();
13 |
14 | /**
15 | * If your app is behind a proxy, reverse proxy or a load balancer, consider
16 | * letting express know that you are behind that proxy. To do so, uncomment
17 | * the line below.
18 | */
19 |
20 | // app.set('trust proxy', /* numberOfProxies */);
21 |
22 | /**
23 | * HTTP request handlers should not perform expensive operations such as accessing the file system,
24 | * executing an operating system command or interacting with a database without limiting the rate at
25 | * which requests are accepted. Otherwise, the application becomes vulnerable to denial-of-service attacks
26 | * where an attacker can cause the application to crash or become unresponsive by issuing a large number of
27 | * requests at the same time. For more information, visit: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
28 | */
29 | const limiter = rateLimit({
30 | windowMs: 15 * 60 * 1000, // 15 minutes
31 | max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
32 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
33 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers
34 | });
35 |
36 | // Apply the rate limiting middleware to all requests
37 | app.use(limiter)
38 |
39 | /**
40 | * Enable CORS middleware. In production, modify as to allow only designated origins and methods.
41 | * If you are using Azure App Service, we recommend removing the line below and configure CORS on the App Service itself.
42 | */
43 | app.use(cors());
44 |
45 | app.use(express.json());
46 | app.use(express.urlencoded({ extended: false }));
47 | app.use(morgan('dev'));
48 |
49 | const bearerStrategy = new passportAzureAd.BearerStrategy({
50 | identityMetadata: `https://${authConfig.metadata.authority}/${authConfig.credentials.tenantID}/${authConfig.metadata.version}/${authConfig.metadata.discovery}`,
51 | issuer: `https://${authConfig.metadata.authority}/${authConfig.credentials.tenantID}/${authConfig.metadata.version}`,
52 | clientID: authConfig.credentials.clientID,
53 | audience: authConfig.credentials.clientID, // audience is this application
54 | validateIssuer: authConfig.settings.validateIssuer,
55 | passReqToCallback: authConfig.settings.passReqToCallback,
56 | loggingLevel: authConfig.settings.loggingLevel,
57 | loggingNoPII: authConfig.settings.loggingNoPII,
58 | }, (req, token, done) => {
59 |
60 | /**
61 | * Below you can do extended token validation and check for additional claims, such as:
62 | * - check if the caller's tenant is in the allowed tenants list via the 'tid' claim (for multi-tenant applications)
63 | * - check if the caller's account is homed or guest via the 'acct' optional claim
64 | * - check if the caller belongs to right roles or groups via the 'roles' or 'groups' claim, respectively
65 | *
66 | * Bear in mind that you can do any of the above checks within the individual routes and/or controllers as well.
67 | * For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/access-tokens#validate-the-user-has-permission-to-access-this-data
68 | */
69 |
70 |
71 | /**
72 | * Lines below verifies if the caller's client ID is in the list of allowed clients.
73 | * This ensures only the applications with the right client ID can access this API.
74 | * To do so, we use "azp" claim in the access token. Uncomment the lines below to enable this check.
75 | */
76 |
77 | // const myAllowedClientsList = [
78 | // /* add here the client IDs of the applications that are allowed to call this API */
79 | // ]
80 |
81 | // if (!myAllowedClientsList.includes(token.azp)) {
82 | // return done(new Error('Unauthorized'), {}, "Client not allowed");
83 | // }
84 |
85 |
86 | /**
87 | * Access tokens that have neither the 'scp' (for delegated permissions) nor
88 | * 'roles' (for application permissions) claim are not to be honored.
89 | */
90 | if (!token.hasOwnProperty('scp') && !token.hasOwnProperty('roles')) {
91 | return done(new Error('Unauthorized'), null, "No delegated or app permission claims found");
92 | }
93 |
94 | /**
95 | * If needed, pass down additional user info to route using the second argument below.
96 | * This information will be available in the req.user object.
97 | */
98 | return done(null, {}, token);
99 | });
100 |
101 | app.use(passport.initialize());
102 |
103 | passport.use(bearerStrategy);
104 |
105 | app.use('/api', (req, res, next) => {
106 | passport.authenticate('oauth-bearer', {
107 | session: false,
108 |
109 | /**
110 | * If you are building a multi-tenant application and you need supply the tenant ID or name dynamically,
111 | * uncomment the line below and pass in the tenant information. For more information, see:
112 | * https://github.com/AzureAD/passport-azure-ad#423-options-available-for-passportauthenticate
113 | */
114 |
115 | // tenantIdOrName:
116 |
117 | }, (err, user, info) => {
118 | if (err) {
119 | /**
120 | * An error occurred during authorization. Either pass the error to the next function
121 | * for Express error handler to handle, or send a response with the appropriate status code.
122 | */
123 | return res.status(401).json({ error: err.message });
124 | }
125 |
126 | if (!user) {
127 | // If no user object found, send a 401 response.
128 | return res.status(401).json({ error: 'Unauthorized' });
129 | }
130 |
131 | if (info) {
132 | // access token payload will be available in req.authInfo downstream
133 | req.authInfo = info;
134 | return next();
135 | }
136 | })(req, res, next);
137 | },
138 | router, // the router with all the routes
139 | (err, req, res, next) => {
140 | /**
141 | * Add your custom error handling logic here. For more information, see:
142 | * http://expressjs.com/en/guide/error-handling.html
143 | */
144 |
145 | // set locals, only providing error in development
146 | res.locals.message = err.message;
147 | res.locals.error = req.app.get('env') === 'development' ? err : {};
148 |
149 | // send error response
150 | res.status(err.status || 500).send(err);
151 | }
152 | );
153 |
154 | const port = process.env.PORT || 5000;
155 |
156 | app.listen(port, () => {
157 | console.log('Listening on port ' + port);
158 | });
159 |
160 | module.exports = app;
161 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/auth/permissionUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Indicates whether the access token was issued to a user or an application.
3 | * @param {Object} accessTokenPayload
4 | * @returns {boolean}
5 | */
6 | const isAppOnlyToken = (accessTokenPayload) => {
7 | /**
8 | * An access token issued by Azure AD will have at least one of the two claims. Access tokens
9 | * issued to a user will have the 'scp' claim. Access tokens issued to an application will have
10 | * the roles claim. Access tokens that contain both claims are issued only to users, where the scp
11 | * claim designates the delegated permissions, while the roles claim designates the user's role.
12 | *
13 | * To determine whether an access token was issued to a user (i.e delegated) or an application
14 | * more easily, we recommend enabling the optional claim 'idtyp'. For more information, see:
15 | * https://docs.microsoft.com/azure/active-directory/develop/access-tokens#user-and-application-tokens
16 | */
17 | if (!accessTokenPayload.hasOwnProperty('idtyp')) {
18 | if (accessTokenPayload.hasOwnProperty('scp')) {
19 | return false;
20 | } else if (!accessTokenPayload.hasOwnProperty('scp') && accessTokenPayload.hasOwnProperty('roles')) {
21 | return true;
22 | }
23 | }
24 |
25 | return accessTokenPayload.idtyp === 'app';
26 | };
27 |
28 | /**
29 | * Ensures that the access token has the specified delegated permissions.
30 | * @param {Object} accessTokenPayload: Parsed access token payload
31 | * @param {Array} requiredPermission: list of required permissions
32 | * @returns {boolean}
33 | */
34 | const hasRequiredDelegatedPermissions = (accessTokenPayload, requiredPermission) => {
35 | const normalizedRequiredPermissions = requiredPermission.map(permission => permission.toUpperCase());
36 |
37 | if (accessTokenPayload.hasOwnProperty('scp') && accessTokenPayload.scp.split(' ')
38 | .some(claim => normalizedRequiredPermissions.includes(claim.toUpperCase()))) {
39 | return true;
40 | }
41 |
42 | return false;
43 | }
44 |
45 | /**
46 | * Ensures that the access token has the specified application permissions.
47 | * @param {Object} accessTokenPayload: Parsed access token payload
48 | * @param {Array} requiredPermission: list of required permissions
49 | * @returns {boolean}
50 | */
51 | const hasRequiredApplicationPermissions = (accessTokenPayload, requiredPermission) => {
52 | const normalizedRequiredPermissions = requiredPermission.map(permission => permission.toUpperCase());
53 |
54 | if (accessTokenPayload.hasOwnProperty('roles') && accessTokenPayload.roles
55 | .some(claim => normalizedRequiredPermissions.includes(claim.toUpperCase()))) {
56 | return true;
57 | }
58 |
59 | return false;
60 | }
61 |
62 | module.exports = {
63 | isAppOnlyToken,
64 | hasRequiredDelegatedPermissions,
65 | hasRequiredApplicationPermissions,
66 | }
67 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/authConfig.js:
--------------------------------------------------------------------------------
1 | const passportConfig = {
2 | credentials: {
3 | tenantID: "Enter_the_Tenant_Info_Here",
4 | clientID: "Enter_the_Application_Id_Here"
5 | },
6 | metadata: {
7 | authority: "login.microsoftonline.com",
8 | discovery: ".well-known/openid-configuration",
9 | version: "v2.0"
10 | },
11 | settings: {
12 | validateIssuer: true,
13 | passReqToCallback: true,
14 | loggingLevel: "info",
15 | loggingNoPII: true,
16 | },
17 | protectedRoutes: {
18 | todolist: {
19 | endpoint: "/api/todolist",
20 | delegatedPermissions: {
21 | read: ["Todolist.Read", "Todolist.ReadWrite"],
22 | write: ["Todolist.ReadWrite"]
23 | },
24 | applicationPermissions: {
25 | read: ["Todolist.Read.All", "Todolist.ReadWrite.All"],
26 | write: ["Todolist.ReadWrite.All"]
27 | }
28 | }
29 | }
30 | }
31 |
32 | module.exports = passportConfig;
33 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/controllers/todolist.js:
--------------------------------------------------------------------------------
1 | const lowdb = require('lowdb');
2 | const FileSync = require('lowdb/adapters/FileSync');
3 | const adapter = new FileSync('./data/db.json');
4 | const db = lowdb(adapter);
5 | const { v4: uuidv4 } = require('uuid');
6 |
7 | const {
8 | isAppOnlyToken,
9 | hasRequiredDelegatedPermissions,
10 | hasRequiredApplicationPermissions
11 | } = require('../auth/permissionUtils');
12 |
13 | const authConfig = require('../authConfig');
14 |
15 | exports.getTodo = (req, res, next) => {
16 | if (isAppOnlyToken(req.authInfo)) {
17 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.read)) {
18 | try {
19 | const id = req.params.id;
20 |
21 | const todo = db.get('todos')
22 | .find({ id: id })
23 | .value();
24 |
25 | res.status(200).send(todo);
26 | } catch (error) {
27 | next(error);
28 | }
29 | } else {
30 | next(new Error('Application does not have the required permissions'))
31 | }
32 | } else {
33 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) {
34 | try {
35 | /**
36 | * The 'oid' (object id) is the only claim that should be used to uniquely identify
37 | * a user in an Azure AD tenant. The token might have one or more of the following claim,
38 | * that might seem like a unique identifier, but is not and should not be used as such,
39 | * especially for systems which act as system of record (SOR):
40 | *
41 | * - upn (user principal name): might be unique amongst the active set of users in a tenant but
42 | * tend to get reassigned to new employees as employees leave the organization and
43 | * others take their place or might change to reflect a personal change like marriage.
44 | *
45 | * - email: might be unique amongst the active set of users in a tenant but tend to get
46 | * reassigned to new employees as employees leave the organization and others take their place.
47 | */
48 | const owner = req.authInfo['oid'];
49 | const id = req.params.id;
50 |
51 | const todo = db.get('todos')
52 | .filter({ owner: owner })
53 | .find({ id: id })
54 | .value();
55 |
56 | res.status(200).send(todo);
57 | } catch (error) {
58 | next(error);
59 | }
60 | } else {
61 | next(new Error('User does not have the required permissions'))
62 | }
63 | }
64 | }
65 |
66 | exports.getTodos = (req, res, next) => {
67 | if (isAppOnlyToken(req.authInfo)) {
68 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.read)) {
69 | try {
70 | const todos = db.get('todos')
71 | .value();
72 |
73 | res.status(200).send(todos);
74 | } catch (error) {
75 | next(error);
76 | }
77 | } else {
78 | next(new Error('Application does not have the required permissions'))
79 | }
80 | } else {
81 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) {
82 | try {
83 | const owner = req.authInfo['oid'];
84 |
85 | const todos = db.get('todos')
86 | .filter({ owner: owner })
87 | .value();
88 |
89 | res.status(200).send(todos);
90 | } catch (error) {
91 | next(error);
92 | }
93 | } else {
94 | next(new Error('User does not have the required permissions'))
95 | }
96 | }
97 | }
98 |
99 | exports.postTodo = (req, res, next) => {
100 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.write)
101 | ||
102 | hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.write)
103 | ) {
104 | try {
105 | const todo = {
106 | description: req.body.description,
107 | id: uuidv4(),
108 | owner: req.authInfo['oid'] // oid is the only claim that should be used to uniquely identify a user in an Azure AD tenant
109 | };
110 |
111 | db.get('todos').push(todo).write();
112 |
113 | res.status(200).json(todo);
114 | } catch (error) {
115 | next(error);
116 | }
117 | } else (
118 | next(new Error('User or application does not have the required permissions'))
119 | )
120 | }
121 |
122 | exports.deleteTodo = (req, res, next) => {
123 | if (isAppOnlyToken(req.authInfo)) {
124 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.write)) {
125 | try {
126 | const id = req.params.id;
127 |
128 | db.get('todos')
129 | .remove({ id: id })
130 | .write();
131 |
132 | res.status(200).json({ message: "success" });
133 | } catch (error) {
134 | next(error);
135 | }
136 | } else {
137 | next(new Error('Application does not have the required permissions'))
138 | }
139 | } else {
140 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.write)) {
141 | try {
142 | const id = req.params.id;
143 | const owner = req.authInfo['oid'];
144 |
145 | db.get('todos')
146 | .remove({ owner: owner, id: id })
147 | .write();
148 |
149 | res.status(200).json({ message: "success" });
150 | } catch (error) {
151 | next(error);
152 | }
153 | } else {
154 | next(new Error('User does not have the required permissions'))
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/data/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "todos": []
3 | }
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ms-identity-react-c3s1",
3 | "version": "1.0.0",
4 | "description": "A Node.js & Express web API accepting authorized calls with Azure Active Directory",
5 | "author": "derisen",
6 | "scripts": {
7 | "start": "node app.js",
8 | "dev": "nodemon app.js",
9 | "test": "jest --forceExit"
10 | },
11 | "dependencies": {
12 | "cors": "^2.8.5",
13 | "express": "^4.18.1",
14 | "express-rate-limit": "^6.5.2",
15 | "lowdb": "^1.0.0",
16 | "morgan": "^1.10.0",
17 | "passport": "^0.6.0",
18 | "passport-azure-ad": "^4.3.3",
19 | "uuid": "^9.0.0"
20 | },
21 | "main": "app.js",
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial.git"
25 | },
26 | "keywords": [
27 | "azure-ad",
28 | "ms-identity",
29 | "node",
30 | "api"
31 | ],
32 | "bugs": {
33 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/issues"
34 | },
35 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial#readme",
36 | "devDependencies": {
37 | "jest": "^28.1.1",
38 | "nodemon": "^2.0.16",
39 | "supertest": "^6.2.3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const todolist = require('../controllers/todolist');
4 |
5 | // initialize router
6 | const router = express.Router();
7 |
8 | router.get('/todolist', todolist.getTodos);
9 |
10 | router.get('/todolist/:id', todolist.getTodo);
11 |
12 | router.post('/todolist', todolist.postTodo);
13 |
14 | router.delete('/todolist/:id', todolist.deleteTodo);
15 |
16 | module.exports = router;
17 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/API/sample.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 |
3 | const app = require('./app.js');
4 |
5 | describe('Sanitize configuration object', () => {
6 | beforeAll(() => {
7 | global.config = require('./authConfig.js');
8 | });
9 |
10 | it('should define the config object', () => {
11 | expect(config).toBeDefined();
12 | });
13 |
14 | it('should not contain client Id', () => {
15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
16 | expect(regexGuid.test(config.credentials.clientID)).toBe(false);
17 | });
18 |
19 | it('should not contain tenant Id', () => {
20 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
21 | expect(regexGuid.test(config.credentials.tenantId)).toBe(false);
22 | });
23 | });
24 |
25 | describe('Ensure routes served', () => {
26 |
27 | beforeAll(() => {
28 | process.env.NODE_ENV = 'test';
29 | });
30 |
31 | it('should protect todolist endpoint', async () => {
32 | const res = await request(app)
33 | .get('/api');
34 |
35 | expect(res.statusCode).toEqual(401);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/AppCreationScripts/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "Sample": {
3 | "Author": "derisen",
4 | "Title": "JavaScript single-page application using MSAL Brwoser to authorize users for calling a Express.js web API on Azure Active Directory",
5 | "Level": 200,
6 | "Client": "JavaScript SPA",
7 | "Service": "Node.js web API",
8 | "RepositoryUrl": "ms-identity-javascript-tutorial",
9 | "Endpoint": "AAD v2.0",
10 | "Languages": ["javascript", "nodejs"],
11 | "Description": "A JavaScript single-page application using MSAL Browser to authorize users for calling a protected Express.js web API on Azure Active Directory",
12 | "Products": ["azure-active-directory", "msal-js", "msal-js", "passport-azure-ad"]
13 | },
14 | "AADApps": [
15 | {
16 | "Id": "service",
17 | "Name": "msal-node-api",
18 | "Kind": "WebApi",
19 | "Audience": "AzureADMyOrg",
20 | "SDK": "MsalNode",
21 | "SampleSubPath": "3-Authorization-II\\1-call-api\\API",
22 | "Scopes": ["Todolist.Read", "Todolist.ReadWrite"],
23 | "AppRoles": [
24 | {
25 | "AllowedMemberTypes": ["Application"],
26 | "Name": "Todolist.Read.All",
27 | "Description": "Allow this application to read every users Todolist items"
28 | },
29 | {
30 | "AllowedMemberTypes": ["Application"],
31 | "Name": "Todolist.ReadWrite.All",
32 | "Description": "Allow this application to read and write every users Todolist items"
33 | }
34 | ],
35 | "OptionalClaims": {
36 | "AccessTokenClaims": ["idtyp", "acct"]
37 | }
38 | },
39 | {
40 | "Id": "client",
41 | "Name": "msal-javascript-spa",
42 | "Kind": "SinglePageApplication",
43 | "Audience": "AzureADMyOrg",
44 | "HomePage": "http://localhost:3000",
45 | "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect",
46 | "SDK": "MsalJs",
47 | "SampleSubPath": "3-Authorization-II\\1-call-api\\SPA",
48 | "RequiredResourcesAccess": [
49 | {
50 | "Resource": "service",
51 | "DelegatedPermissions": ["Todolist.Read", "Todolist.ReadWrite"]
52 | }
53 | ]
54 | }
55 | ],
56 | "CodeConfiguration": [
57 | {
58 | "App": "service",
59 | "SettingKind": "Replace",
60 | "SettingFile": "\\..\\API\\authConfig.js",
61 | "Mappings": [
62 | {
63 | "key": "Enter_the_Application_Id_Here",
64 | "value": ".AppId"
65 | },
66 | {
67 | "key": "Enter_the_Tenant_Info_Here",
68 | "value": "$tenantId"
69 | }
70 | ]
71 | },
72 | {
73 | "App": "client",
74 | "SettingKind": "Replace",
75 | "SettingFile": "\\..\\SPA\\public\\authConfig.js",
76 | "Mappings": [
77 | {
78 | "key": "Enter_the_Application_Id_Here",
79 | "value": ".AppId"
80 | },
81 | {
82 | "key": "Enter_the_Tenant_Info_Here",
83 | "value": "$tenantId"
84 | },
85 | {
86 | "key": "Enter_the_Web_Api_Application_Id_Here",
87 | "value": "service.AppId"
88 | }
89 | ]
90 | }
91 | ]
92 | }
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/ReadmeFiles/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/1-call-api/ReadmeFiles/screenshot.png
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/ReadmeFiles/topology_callapi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/1-call-api/ReadmeFiles/topology_callapi.png
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ciam-sign-in-javascript",
3 | "version": "1.0.0",
4 | "description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users against Azure AD Customer Identity Access Management (Azure AD for Customers)",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "test": "jest --forceExit"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@azure/msal-browser": "^2.37.0",
14 | "express": "^4.18.2",
15 | "morgan": "^1.10.0"
16 | },
17 | "devDependencies": {
18 | "jest": "^29.5.0",
19 | "jest-environment-jsdom": "^29.5.0",
20 | "supertest": "^6.3.3"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/authConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration object to be passed to MSAL instance on creation.
3 | * For a full list of MSAL.js configuration parameters, visit:
4 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
5 | */
6 | const msalConfig = {
7 | auth: {
8 | clientId: 'Enter_the_Application_Id_Here', // This is the ONLY mandatory field that you need to supply.
9 | authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Info_Here', // Replace the placeholder with your tenant name
10 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.href e.g. http://localhost:3000/,
11 | postLogoutRedirectUri: '/', // Indicates the page to navigate after logout.
12 | },
13 | cache: {
14 | cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO.
15 | storeAuthStateInCookie: false, // set this to true if you have to support IE
16 | },
17 | system: {
18 | loggerOptions: {
19 | loggerCallback: (level, message, containsPii) => {
20 | if (containsPii) {
21 | return;
22 | }
23 | switch (level) {
24 | case msal.LogLevel.Error:
25 | console.error(message);
26 | return;
27 | case msal.LogLevel.Info:
28 | console.info(message);
29 | return;
30 | case msal.LogLevel.Verbose:
31 | console.debug(message);
32 | return;
33 | case msal.LogLevel.Warning:
34 | console.warn(message);
35 | return;
36 | default:
37 | return;
38 | }
39 | },
40 | },
41 | },
42 | };
43 |
44 | /**
45 | * Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
46 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
47 | */
48 | const protectedResources = {
49 | todolistApi: {
50 | endpoint: 'http://localhost:5000/api/todolist',
51 | scopes: {
52 | read: ['api://Enter_the_Web_Api_Application_Id_Here/Todolist.Read'],
53 | write: ['api://Enter_the_Web_Api_Application_Id_Here/Todolist.ReadWrite'],
54 | },
55 | },
56 | };
57 |
58 | /**
59 | * Scopes you add here will be prompted for user consent during sign-in.
60 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
61 | * For more information about OIDC scopes, visit:
62 | * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
63 | */
64 | const loginRequest = {
65 | scopes: [...protectedResources.todolistApi.scopes.read, ...protectedResources.todolistApi.scopes.write],
66 | };
67 |
68 | /**
69 | * An optional silentRequest object can be used to achieve silent SSO
70 | * between applications by providing a "login_hint" property.
71 | */
72 |
73 | // const silentRequest = {
74 | // scopes: ["openid", "profile"],
75 | // loginHint: "example@domain.net"
76 | // };
77 |
78 | // exporting config object for jest
79 | if (typeof exports !== 'undefined') {
80 | module.exports = {
81 | msalConfig,
82 | loginRequest,
83 | protectedResources,
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/authPopup.js:
--------------------------------------------------------------------------------
1 | // Create the main myMSALObj instance
2 | // configuration parameters are located at authConfig.js
3 | const myMSALObj = new msal.PublicClientApplication(msalConfig);
4 |
5 | let username = '';
6 |
7 | function selectAccount() {
8 | /**
9 | * See here for more info on account retrieval:
10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
11 | */
12 |
13 | const currentAccounts = myMSALObj.getAllAccounts();
14 | if (!currentAccounts || currentAccounts.length < 1) {
15 | return;
16 | } else if (currentAccounts.length > 1) {
17 | // Add your account choosing logic here
18 | console.warn('Multiple accounts detected.');
19 | } else if (currentAccounts.length === 1) {
20 | username = currentAccounts[0].username;
21 | welcomeUser(username);
22 | updateTable(currentAccounts[0]);
23 | }
24 | }
25 |
26 | function handleResponse(response) {
27 | /**
28 | * To see the full list of response object properties, visit:
29 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response
30 | */
31 |
32 | if (response !== null) {
33 | username = response.account.username;
34 | welcomeUser(username);
35 | updateTable(response.account);
36 | } else {
37 | selectAccount();
38 | }
39 | }
40 |
41 | function signIn() {
42 | /**
43 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
44 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
45 | */
46 |
47 | myMSALObj
48 | .loginPopup({
49 | ...loginRequest,
50 | redirectUri: '/redirect',
51 | })
52 | .then(handleResponse)
53 | .catch((error) => {
54 | console.log(error);
55 | });
56 | }
57 |
58 |
59 | function getTokenPopup(request) {
60 | /**
61 | * See here for more information on account retrieval:
62 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
63 | */
64 | request.account = myMSALObj.getAccountByUsername(username);
65 | return myMSALObj.acquireTokenSilent(request).catch((error) => {
66 | console.warn(error);
67 | console.warn('silent token acquisition fails. acquiring token using popup');
68 | if (error instanceof msal.InteractionRequiredAuthError) {
69 | // fallback to interaction when silent call fails
70 | return myMSALObj
71 | .acquireTokenPopup(request)
72 | .then((response) => {
73 | return response;
74 | })
75 | .catch((error) => {
76 | console.error(error);
77 | });
78 | } else {
79 | console.warn(error);
80 | }
81 | });
82 | }
83 |
84 | function signOut() {
85 | /**
86 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
87 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
88 | */
89 |
90 | // Choose which account to logout from by passing a username.
91 | const logoutRequest = {
92 | account: myMSALObj.getAccountByUsername(username),
93 | };
94 | myMSALObj.logoutPopup(logoutRequest).then(() => {
95 | window.location.reload();
96 | });
97 | }
98 |
99 | selectAccount();
100 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/authRedirect.js:
--------------------------------------------------------------------------------
1 | // Create the main myMSALObj instance
2 | // configuration parameters are located at authConfig.js
3 | const myMSALObj = new msal.PublicClientApplication(msalConfig);
4 |
5 | let username = '';
6 |
7 | /**
8 | * A promise handler needs to be registered for handling the
9 | * response returned from redirect flow. For more information, visit:
10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirect-apis
11 | */
12 | myMSALObj
13 | .handleRedirectPromise()
14 | .then(handleResponse)
15 | .catch((error) => {
16 | console.error(error);
17 | });
18 |
19 | function selectAccount() {
20 | /**
21 | * See here for more info on account retrieval:
22 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
23 | */
24 |
25 | const currentAccounts = myMSALObj.getAllAccounts();
26 |
27 | if (!currentAccounts) {
28 | return;
29 | } else if (currentAccounts.length > 1) {
30 | // Add your account choosing logic here
31 | console.warn('Multiple accounts detected.');
32 | } else if (currentAccounts.length === 1) {
33 | username = currentAccounts[0].username;
34 | welcomeUser(username);
35 | updateTable(currentAccounts[0]);
36 | }
37 | }
38 |
39 | function handleResponse(response) {
40 | /**
41 | * To see the full list of response object properties, visit:
42 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response
43 | */
44 |
45 | if (response !== null) {
46 | username = response.account.username;
47 | welcomeUser(username);
48 | updateTable(response.account);
49 | } else {
50 | selectAccount();
51 | }
52 | }
53 |
54 | function signIn() {
55 | /**
56 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
57 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
58 | */
59 |
60 | myMSALObj.loginRedirect(loginRequest);
61 | }
62 |
63 | function getTokenRedirect(request) {
64 | /**
65 | * See here for more info on account retrieval:
66 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
67 | */
68 | request.account = myMSALObj.getAccountByUsername(username);
69 | return myMSALObj.acquireTokenSilent(request).catch((error) => {
70 | console.error(error);
71 | console.warn('silent token acquisition fails. acquiring token using popup');
72 | if (error instanceof msal.InteractionRequiredAuthError) {
73 | // fallback to interaction when silent call fails
74 | return myMSALObj.acquireTokenRedirect(request);
75 | } else {
76 | console.error(error);
77 | }
78 | });
79 | }
80 |
81 | function signOut() {
82 | /**
83 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
84 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
85 | */
86 |
87 | // Choose which account to logout from by passing a username.
88 | const logoutRequest = {
89 | account: myMSALObj.getAccountByUsername(username),
90 | };
91 |
92 | myMSALObj.logoutRedirect(logoutRequest);
93 | }
94 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/fetch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Execute a fetch request with the given options
3 | * @param {string} method: GET, POST, PUT, DELETE
4 | * @param {String} endpoint: The endpoint to call
5 | * @param {Object} data: The data to send to the endpoint, if any
6 | * @returns response
7 | */
8 | function callApi(method, endpoint, token, data = null) {
9 | const headers = new Headers();
10 | const bearer = `Bearer ${token}`;
11 |
12 | headers.append('Authorization', bearer);
13 |
14 | if (data) {
15 | headers.append('Content-Type', 'application/json');
16 | }
17 |
18 | const options = {
19 | method: method,
20 | headers: headers,
21 | body: data ? JSON.stringify(data) : null,
22 | };
23 |
24 | return fetch(endpoint, options)
25 | .then((response) => {
26 | const contentType = response.headers.get("content-type");
27 |
28 | if (contentType && contentType.indexOf("application/json") !== -1) {
29 | return response.json();
30 | } else {
31 | return response;
32 | }
33 | });
34 | }
35 |
36 |
37 | /**
38 | * Handles todolist actions
39 | * @param {Object} task
40 | * @param {string} method
41 | * @param {string} endpoint
42 | */
43 | async function handleToDoListActions(task, method, endpoint) {
44 | let listData;
45 |
46 | try {
47 | const accessToken = await getToken();
48 | const data = await callApi(method, endpoint, accessToken, task);
49 |
50 | switch (method) {
51 | case 'POST':
52 | listData = JSON.parse(localStorage.getItem('todolist'));
53 | listData = [data, ...listData];
54 | localStorage.setItem('todolist', JSON.stringify(listData));
55 | AddTaskToToDoList(data);
56 | break;
57 | case 'DELETE':
58 | listData = JSON.parse(localStorage.getItem('todolist'));
59 | const index = listData.findIndex((todoItem) => todoItem.id === task.id);
60 | localStorage.setItem('todolist', JSON.stringify([...listData.splice(index, 1)]));
61 | showToDoListItems(listData);
62 | break;
63 | default:
64 | console.log('Unrecognized method.')
65 | break;
66 | }
67 | } catch (error) {
68 | console.error(error);
69 | }
70 | }
71 |
72 | /**
73 | * Handles todolist action GET action.
74 | */
75 | async function getToDos() {
76 | try {
77 | const accessToken = await getToken();
78 |
79 | const data = await callApi(
80 | 'GET',
81 | protectedResources.todolistApi.endpoint,
82 | accessToken
83 | );
84 |
85 | if (data) {
86 | localStorage.setItem('todolist', JSON.stringify(data));
87 | showToDoListItems(data);
88 | }
89 | } catch (error) {
90 | console.error(error);
91 | }
92 | }
93 |
94 | /**
95 | * Retrieves an access token.
96 | */
97 | async function getToken() {
98 | let tokenResponse;
99 |
100 | if (typeof getTokenPopup === 'function') {
101 | tokenResponse = await getTokenPopup({
102 | scopes: [...protectedResources.todolistApi.scopes.read],
103 | redirectUri: '/redirect'
104 | });
105 | } else {
106 | tokenResponse = await getTokenRedirect({
107 | scopes: [...protectedResources.todolistApi.scopes.read],
108 | });
109 | }
110 |
111 | if (!tokenResponse) {
112 | return null;
113 | }
114 |
115 | return tokenResponse.accessToken;
116 | }
117 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Microsoft identity platform
8 |
9 |
10 |
11 |
14 |
15 |
17 |
18 |
19 |
20 |
32 |
33 |
Vanilla JavaScript single-page application secured with MSAL.js
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Claim Type
42 |
Value
43 |
Description
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
67 |
70 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/redirect.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/styles.css:
--------------------------------------------------------------------------------
1 | .navbarStyle {
2 | padding: .5rem 1rem !important;
3 | }
4 |
5 | .table-responsive-ms {
6 | max-height: 39rem !important;
7 | padding-left: 10%;
8 | padding-right: 10%;
9 | }
10 |
11 | form, .group-div {
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | }
16 |
17 | .input-group, ul {
18 | width: 50% !important;
19 | }
20 |
21 |
22 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/public/ui.js:
--------------------------------------------------------------------------------
1 | // Select DOM elements to work with
2 | const signInButton = document.getElementById('signIn');
3 | const signOutButton = document.getElementById('signOut');
4 | const titleDiv = document.getElementById('title-div');
5 | const welcomeDiv = document.getElementById('welcome-div');
6 | const tableDiv = document.getElementById('table-div');
7 | const tableBody = document.getElementById('table-body-div');
8 | const toDoListLink = document.getElementById('toDoListLink');
9 | const toDoForm = document.getElementById('form');
10 | const textInput = document.getElementById('textInput');
11 | const toDoListDiv = document.getElementById('groupDiv');
12 | const todoListItems = document.getElementById('toDoListItems');
13 |
14 | toDoForm.addEventListener('submit', (e) => {
15 | e.preventDefault();
16 | let task = { description: textInput.value };
17 | handleToDoListActions(task, 'POST', protectedResources.todolistApi.endpoint);
18 | toDoForm.reset();
19 | });
20 |
21 | function welcomeUser(username) {
22 | signInButton.classList.add('d-none');
23 | signOutButton.classList.remove('d-none');
24 | toDoListLink.classList.remove('d-none');
25 | titleDiv.classList.add('d-none');
26 | welcomeDiv.classList.remove('d-none');
27 | welcomeDiv.innerHTML = `Welcome ${username}!`;
28 | }
29 |
30 | function updateTable(account) {
31 | tableDiv.classList.remove('d-none');
32 | const tokenClaims = createClaimsTable(account.idTokenClaims);
33 |
34 | Object.keys(tokenClaims).forEach((key) => {
35 | let row = tableBody.insertRow(0);
36 | let cell1 = row.insertCell(0);
37 | let cell2 = row.insertCell(1);
38 | let cell3 = row.insertCell(2);
39 | cell1.innerHTML = tokenClaims[key][0];
40 | cell2.innerHTML = tokenClaims[key][1];
41 | cell3.innerHTML = tokenClaims[key][2];
42 | });
43 | }
44 |
45 | function showToDoListItems(response) {
46 | todoListItems.replaceChildren();
47 | tableDiv.classList.add('d-none');
48 | toDoForm.classList.remove('d-none');
49 | toDoListDiv.classList.remove('d-none');
50 | if (!!response.length) {
51 | response.forEach((task) => {
52 | AddTaskToToDoList(task);
53 | });
54 | }
55 | }
56 |
57 | function AddTaskToToDoList(task) {
58 | let li = document.createElement('li');
59 | let button = document.createElement('button');
60 | button.innerHTML = 'Delete';
61 | button.classList.add('btn', 'btn-danger');
62 | button.addEventListener('click', () => {
63 | handleToDoListActions(task, 'DELETE', protectedResources.todolistApi.endpoint + `/${task.id}`);
64 | });
65 | li.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center');
66 | li.innerHTML = task.description;
67 | li.appendChild(button);
68 | todoListItems.appendChild(li);
69 | }
70 |
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/sample.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 |
5 | const request = require('supertest');
6 | const path = require('path');
7 | const fs = require('fs');
8 |
9 | const app = require('./server.js');
10 |
11 | jest.dontMock('fs');
12 |
13 | const html = fs.readFileSync(path.resolve(__dirname, './public/index.html'), 'utf8');
14 |
15 | describe('Sanitize index page', () => {
16 | beforeAll(async() => {
17 | global.document.documentElement.innerHTML = html.toString();
18 | });
19 |
20 | it('should have valid cdn link', () => {
21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser");
22 | });
23 | });
24 |
25 | describe('Sanitize configuration object', () => {
26 | beforeAll(() => {
27 | global.msalConfig = require('./public/authConfig.js').msalConfig;
28 | });
29 |
30 | it('should define the config object', () => {
31 | expect(msalConfig).toBeDefined();
32 | });
33 |
34 | it('should not contain credentials', () => {
35 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(false);
37 | });
38 |
39 | it('should contain authority URI', () => {
40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true);
42 | });
43 | });
44 |
45 | describe('Ensure pages served', () => {
46 |
47 | beforeAll(() => {
48 | process.env.NODE_ENV = 'test';
49 | });
50 |
51 | it('should get index page', async () => {
52 | const res = await request(app)
53 | .get('/');
54 |
55 | const data = await fs.promises.readFile(path.join(__dirname, './public/index.html'), 'utf8');
56 | expect(res.statusCode).toEqual(200);
57 | expect(res.text).toEqual(data);
58 | });
59 | });
--------------------------------------------------------------------------------
/3-Authorization-II/1-call-api/SPA/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const path = require('path');
4 |
5 | const DEFAULT_PORT = process.env.PORT || 3000;
6 |
7 | // initialize express.
8 | const app = express();
9 |
10 | // Configure morgan module to log all requests.
11 | app.use(morgan('dev'));
12 |
13 | // Setup app folders.
14 | app.use(express.static('public'));
15 |
16 | // set up a route for redirect.html
17 | app.get('/redirect', (req, res) => {
18 | res.sendFile(path.join(__dirname + '/public/redirect.html'));
19 | });
20 |
21 | // Set up a route for index.html
22 | app.get('/', (req, res) => {
23 | res.sendFile(path.join(__dirname + '/index.html'));
24 | });
25 |
26 | app.listen(DEFAULT_PORT, () => {
27 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`);
28 | });
29 |
30 | module.exports = app;
31 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const cors = require('cors');
4 |
5 | const rateLimit = require('express-rate-limit');
6 |
7 | const passport = require('passport');
8 | const passportAzureAd = require('passport-azure-ad');
9 |
10 |
11 | const authConfig = require('./authConfig.js');
12 | const router = require('./routes/index');
13 |
14 | const app = express();
15 |
16 | /**
17 | * If your app is behind a proxy, reverse proxy or a load balancer, consider
18 | * letting express know that you are behind that proxy. To do so, uncomment
19 | * the line below.
20 | */
21 |
22 | // app.set('trust proxy', /* numberOfProxies */);
23 |
24 | /**
25 | * HTTP request handlers should not perform expensive operations such as accessing the file system,
26 | * executing an operating system command or interacting with a database without limiting the rate at
27 | * which requests are accepted. Otherwise, the application becomes vulnerable to denial-of-service attacks
28 | * where an attacker can cause the application to crash or become unresponsive by issuing a large number of
29 | * requests at the same time. For more information, visit: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
30 | */
31 | const limiter = rateLimit({
32 | windowMs: 15 * 60 * 1000, // 15 minutes
33 | max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
34 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
35 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers
36 | });
37 |
38 | // Apply the rate limiting middleware to all requests
39 | app.use(limiter);
40 |
41 | app.use(cors());
42 |
43 | app.use(express.json())
44 | app.use(express.urlencoded({ extended: false}));
45 | app.use(morgan('dev'));
46 |
47 | const options = {
48 | identityMetadata: `https://${authConfig.metadata.b2cDomain}/${authConfig.credentials.tenantName}/${authConfig.policies.policyName}/${authConfig.metadata.version}/${authConfig.metadata.discovery}`,
49 | clientID: authConfig.credentials.clientID,
50 | audience: authConfig.credentials.clientID,
51 | policyName: authConfig.policies.policyName,
52 | isB2C: authConfig.settings.isB2C,
53 | validateIssuer: authConfig.settings.validateIssuer,
54 | loggingLevel: authConfig.settings.loggingLevel,
55 | passReqToCallback: authConfig.settings.passReqToCallback,
56 | loggingNoPII: authConfig.settings.loggingNoPII, // set this to true in the authConfig.js if you want to enable logging and debugging
57 | };
58 |
59 | const bearerStrategy = new passportAzureAd.BearerStrategy(options, (req,token, done) => {
60 | /**
61 | * Below you can do extended token validation and check for additional claims, such as:
62 | * - check if the delegated permissions in the 'scp' are the same as the ones declared in the application registration.
63 | *
64 | * Bear in mind that you can do any of the above checks within the individual routes and/or controllers as well.
65 | * For more information, visit: https://learn.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview
66 | */
67 |
68 | /**
69 | * Lines below verifies if the caller's client ID is in the list of allowed clients.
70 | * This ensures only the applications with the right client ID can access this API.
71 | * To do so, we use "azp" claim in the access token. Uncomment the lines below to enable this check.
72 | */
73 | // if (!myAllowedClientsList.includes(token.azp)) {
74 | // return done(new Error('Unauthorized'), {}, "Client not allowed");
75 | // }
76 |
77 | // const myAllowedClientsList = [
78 | // /* add here the client IDs of the applications that are allowed to call this API */
79 | // ]
80 |
81 | /**
82 | * Access tokens that have no 'scp' (for delegated permissions).
83 | */
84 | if (!token.hasOwnProperty('scp')) {
85 | return done(new Error('Unauthorized'), null, 'No delegated permissions found');
86 | }
87 |
88 | done(null, {}, token);
89 | });
90 |
91 |
92 | app.use(passport.initialize());
93 |
94 | passport.use(bearerStrategy);
95 |
96 | app.use(
97 | '/api',
98 | (req, res, next) => {
99 | passport.authenticate(
100 | 'oauth-bearer',
101 | {
102 | session: false,
103 | },
104 | (err, user, info) => {
105 | if (err) {
106 | /**
107 | * An error occurred during authorization. Either pass the error to the next function
108 | * for Express error handler to handle, or send a response with the appropriate status code.
109 | */
110 | return res.status(401).json({ error: err.message });
111 | }
112 |
113 | if (!user) {
114 | // If no user object found, send a 401 response.
115 | return res.status(401).json({ error: 'Unauthorized' });
116 | }
117 |
118 | if (info) {
119 | // access token payload will be available in req.authInfo downstream
120 | req.authInfo = info;
121 | return next();
122 | }
123 | }
124 | )(req, res, next);
125 | },
126 | router, // the router with all the routes
127 | (err, req, res, next) => {
128 | /**
129 | * Add your custom error handling logic here. For more information, see:
130 | * http://expressjs.com/en/guide/error-handling.html
131 | */
132 |
133 | // set locals, only providing error in development
134 | res.locals.message = err.message;
135 | res.locals.error = req.app.get('env') === 'development' ? err : {};
136 |
137 | // send error response
138 | res.status(err.status || 500).send(err);
139 | }
140 | );
141 |
142 | const port = process.env.PORT || 5000;
143 |
144 | app.listen(port, () => {
145 | console.log('Listening on port ' + port);
146 | });
147 |
148 | module.exports = app;
149 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/auth/permissionUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Ensures that the access token has the specified delegated permissions.
3 | * @param {Object} accessTokenPayload: Parsed access token payload
4 | * @param {Array} requiredPermission: list of required permissions
5 | * @returns {boolean}
6 | */
7 | const hasRequiredDelegatedPermissions = (accessTokenPayload, requiredPermission) => {
8 | const normalizedRequiredPermissions = requiredPermission.map((permission) => permission.toUpperCase());
9 |
10 | if (accessTokenPayload.hasOwnProperty('scp') && accessTokenPayload.scp.split(' ')
11 | .some(claim => normalizedRequiredPermissions.includes(claim.toUpperCase()))) {
12 | return true;
13 | }
14 | return false;
15 |
16 | };
17 |
18 | module.exports = {
19 | hasRequiredDelegatedPermissions
20 | };
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/authConfig.js:
--------------------------------------------------------------------------------
1 | const passportConfig = {
2 | credentials: {
3 | tenantName: 'fabrikamb2c.onmicrosoft.com',
4 | clientID: 'e29ac359-6a90-4f9e-b31c-8f64e1ac20cb',
5 | },
6 | policies: {
7 | policyName: 'B2C_1_susi_v2',
8 | },
9 | metadata: {
10 | b2cDomain: 'fabrikamb2c.b2clogin.com',
11 | authority: 'login.microsoftonline.com',
12 | discovery: '.well-known/openid-configuration',
13 | version: 'v2.0',
14 | },
15 | settings: {
16 | isB2C: true,
17 | validateIssuer: false,
18 | passReqToCallback: true,
19 | loggingLevel: 'info',
20 | loggingNoPII: false,
21 | },
22 | protectedRoutes: {
23 | todolist: {
24 | endpoint: '/api/todolist',
25 | delegatedPermissions: {
26 | read: ['ToDoList.Read', 'ToDoList.ReadWrite'],
27 | write: ['ToDoList.ReadWrite'],
28 | },
29 | },
30 | },
31 | };
32 |
33 | module.exports = passportConfig;
34 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/controllers/todolist.js:
--------------------------------------------------------------------------------
1 | const lowdb = require('lowdb');
2 | const FileSync = require('lowdb/adapters/FileSync');
3 | const adapter = new FileSync('./data/db.json');
4 | const db = lowdb(adapter);
5 | const { v4: uuidv4 } = require('uuid');
6 |
7 | const {
8 | hasRequiredDelegatedPermissions,
9 | } = require('../auth/permissionUtils');
10 |
11 | const authConfig = require('../authConfig');
12 |
13 | exports.getTodo = (req, res, next) => {
14 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) {
15 | try {
16 | /**
17 | * The 'oid' (object id) is the only claim that should be used to uniquely identify
18 | * a user in an Azure AD tenant. The token might have one or more of the following claim,
19 | * that might seem like a unique identifier, but is not and should not be used as such,
20 | * especially for systems which act as system of record (SOR):
21 | *
22 | * - upn (user principal name): might be unique amongst the active set of users in a tenant but
23 | * tend to get reassigned to new employees as employees leave the organization and
24 | * others take their place or might change to reflect a personal change like marriage.
25 | *
26 | * - email: might be unique amongst the active set of users in a tenant but tend to get
27 | * reassigned to new employees as employees leave the organization and others take their place.
28 | */
29 | const owner = req.authInfo['oid'];
30 | const id = req.params.id;
31 |
32 | const todo = db.get('todos')
33 | .filter({ owner: owner })
34 | .find({ id: id })
35 | .value();
36 |
37 | res.status(200).send(todo);
38 | } catch (error) {
39 | next(error);
40 | }
41 | } else {
42 | next(new Error('User does not have the required permissions'))
43 | }
44 | }
45 |
46 | exports.getTodos = (req, res, next) => {
47 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) {
48 | try {
49 | const owner = req.authInfo['oid'];
50 |
51 | const todos = db.get('todos')
52 | .filter({ owner: owner })
53 | .value();
54 |
55 | res.status(200).send(todos);
56 | } catch (error) {
57 | next(error);
58 | }
59 | } else {
60 | next(new Error('User does not have the required permissions'))
61 | }
62 | }
63 |
64 | exports.postTodo = (req, res, next) => {
65 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.write)) {
66 | try {
67 | const todo = {
68 | description: req.body.description,
69 | id: uuidv4(),
70 | owner: req.authInfo['oid'] // oid is the only claim that should be used to uniquely identify a user in an Azure AD tenant
71 | };
72 |
73 | db.get('todos').push(todo).write();
74 |
75 | res.status(200).json(todo);
76 | } catch (error) {
77 | next(error);
78 | }
79 | } else (
80 | next(new Error('User or application does not have the required permissions'))
81 | )
82 | }
83 |
84 | exports.deleteTodo = (req, res, next) => {
85 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.write)) {
86 | try {
87 | const id = req.params.id;
88 | const owner = req.authInfo['oid'];
89 |
90 | db.get('todos')
91 | .remove({ owner: owner, id: id })
92 | .write();
93 |
94 | res.status(200).json({ message: "success" });
95 | } catch (error) {
96 | next(error);
97 | }
98 | } else {
99 | next(new Error('User does not have the required permissions'))
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/data/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "todos": []
3 | }
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ms-identity-react-c3s1",
3 | "version": "1.0.0",
4 | "description": "A Node.js & Express web API accepting authorized calls with Azure Active Directory",
5 | "author": "derisen",
6 | "scripts": {
7 | "start": "node app.js",
8 | "dev": "nodemon app.js",
9 | "test": "jest --forceExit"
10 | },
11 | "dependencies": {
12 | "cors": "^2.8.5",
13 | "express": "^4.18.1",
14 | "express-rate-limit": "^6.5.2",
15 | "lowdb": "^1.0.0",
16 | "morgan": "^1.10.0",
17 | "passport": "^0.6.0",
18 | "passport-azure-ad": "^4.3.3",
19 | "uuid": "^9.0.0"
20 | },
21 | "main": "app.js",
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial.git"
25 | },
26 | "keywords": [
27 | "azure-ad",
28 | "ms-identity",
29 | "node",
30 | "api"
31 | ],
32 | "bugs": {
33 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/issues"
34 | },
35 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial#readme",
36 | "devDependencies": {
37 | "jest": "^28.1.1",
38 | "nodemon": "^2.0.16",
39 | "supertest": "^6.2.3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const todolist = require('../controllers/todolist');
4 |
5 | // initialize router
6 | const router = express.Router();
7 |
8 | router.get('/todolist', todolist.getTodos);
9 |
10 | router.get('/todolist/:id', todolist.getTodo);
11 |
12 | router.post('/todolist', todolist.postTodo);
13 |
14 | router.delete('/todolist/:id', todolist.deleteTodo);
15 |
16 | module.exports = router;
17 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/API/sample.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 |
3 | const app = require('./app.js');
4 |
5 | describe('Sanitize configuration object', () => {
6 | beforeAll(() => {
7 | global.config = require('./authConfig.js');
8 | });
9 |
10 | it('should define the config object', () => {
11 | expect(config).toBeDefined();
12 | });
13 |
14 | it('should contain client Id', () => {
15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
16 | expect(regexGuid.test(config.credentials.clientID)).toBe(true);
17 | });
18 | });
19 |
20 | describe('Ensure routes served', () => {
21 |
22 | beforeAll(() => {
23 | process.env.NODE_ENV = 'test';
24 | });
25 |
26 | it('should protect todolist endpoint', async () => {
27 | const res = await request(app)
28 | .get('/api');
29 |
30 | expect(res.statusCode).toEqual(401);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/AppCreationScripts/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "Sample": {
3 | "Author": "salman90",
4 | "Title": "A JavaScript single-page application using MSAL Browser to authorize users for calling a protected web API on Azure AD B2C",
5 | "Level": 200,
6 | "Client": "JavaScript SPA",
7 | "Service": "Node.js web API",
8 | "RepositoryUrl": "ms-identity-javascript-react-tutorial",
9 | "Endpoint": "AAD v2.0",
10 | "Description": "A JavaScript single-page application using MSAL Browser to authorize users for calling a protected web API on Azure AD B2C",
11 | "Languages": [
12 | "javascript",
13 | "nodejs"
14 | ],
15 | "Products": [
16 | "azure-active-directory-b2c",
17 | "msal-js",
18 | "passport-azure-ad"
19 | ],
20 | "Platform": "JavaScript",
21 | "Provider": "B2C"
22 | },
23 | "AADApps": [
24 | {
25 | "Id": "service",
26 | "Name": "msal-node-api",
27 | "Kind": "WebApi",
28 | "Audience": "AzureADandPersonalMicrosoftAccount",
29 | "SDK": "MsalNode",
30 | "Scopes": [
31 | "ToDoList.Read",
32 | "ToDoList.ReadWrite"
33 | ],
34 | "SampleSubPath": "3-Authorization-II\\1-call-api-b2c\\API"
35 | },
36 | {
37 | "Id": "client",
38 | "Name": "msal-javascript-spa",
39 | "Kind": "SinglePageApplication",
40 | "Audience": "AzureADandPersonalMicrosoftAccount",
41 | "HomePage": "http://localhost:3000",
42 | "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect",
43 | "SampleSubPath": "3-Authorization-II\\1-call-api-b2c\\SPA",
44 | "SDK": "MsalJs",
45 | "RequiredResourcesAccess": [
46 | {
47 | "Resource": "service",
48 | "DelegatedPermissions": [
49 | "ToDoList.Read",
50 | "ToDoList.ReadWrite"
51 | ]
52 | }
53 | ]
54 | }
55 | ],
56 | "CodeConfiguration": [
57 | {
58 | "App": "service",
59 | "SettingKind": "JSON",
60 | "SettingFile": "\\..\\API\\authConfig.json",
61 | "Mappings": [
62 | {
63 | "key": "clientID",
64 | "value": ".AppId"
65 | },
66 | {
67 | "key": "tenantID",
68 | "value": "$tenantId"
69 | },
70 | {
71 | "key": "policyName",
72 | "value": "Enter_The_Your_policy_Name"
73 | }
74 | ]
75 | },
76 | {
77 | "App": "client",
78 | "SettingKind": "Replace",
79 | "SettingFile": "\\..\\SPA\\src\\authConfig.js",
80 | "Mappings": [
81 | {
82 | "key": "Enter_the_Application_Id_Here",
83 | "value": ".AppId"
84 | },
85 | {
86 | "key": "Enter_the_Tenant_Info_Here",
87 | "value": "$tenantId"
88 | },
89 | {
90 | "key": "Enter_the_Web_Api_Scope_Here",
91 | "value": "service.Scope"
92 | },
93 | {
94 | "key": "policyName",
95 | "value": "Enter_The_Your_policy_Name"
96 | },
97 | {
98 | "key": "b2cDomain",
99 | "value": "Enter_The_Tenant_Domain_name"
100 | }
101 | ]
102 | }
103 | ]
104 | }
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/ReadmeFiles/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/2-call-api-b2c/ReadmeFiles/screenshot.png
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/ReadmeFiles/topology_b2c_callapi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/2-call-api-b2c/ReadmeFiles/topology_b2c_callapi.png
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ciam-sign-in-javascript",
3 | "version": "1.0.0",
4 | "description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users against Azure AD Customer Identity Access Management (Azure AD for Customers)",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "test": "jest --forceExit"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@azure/msal-browser": "^2.37.0",
14 | "express": "^4.18.2",
15 | "morgan": "^1.10.0"
16 | },
17 | "devDependencies": {
18 | "jest": "^29.5.0",
19 | "jest-environment-jsdom": "^29.5.0",
20 | "supertest": "^6.3.3"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/authConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Enter here the user flows and custom policies for your B2C application
3 | * To learn more about user flows, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview
4 | * To learn more about custom policies, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-overview
5 | */
6 | const b2cPolicies = {
7 | names: {
8 | signUpSignIn: 'B2C_1_susi_v2',
9 | forgotPassword: 'B2C_1_reset_v3',
10 | editProfile: 'B2C_1_edit_profile_v2',
11 | },
12 | authorities: {
13 | signUpSignIn: {
14 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_susi_v2',
15 | },
16 | forgotPassword: {
17 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_reset_v3',
18 | },
19 | editProfile: {
20 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_edit_profile_v2',
21 | },
22 | },
23 | authorityDomain: 'fabrikamb2c.b2clogin.com',
24 | };
25 |
26 | /**
27 | * Configuration object to be passed to MSAL instance on creation.
28 | * For a full list of MSAL.js configuration parameters, visit:
29 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
30 | * For more details on MSAL.js and Azure AD B2C, visit:
31 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/working-with-b2c.md
32 | */
33 |
34 | const msalConfig = {
35 | auth: {
36 | clientId: '2fdd06f3-7b34-49a3-a78b-0cf1dd87878e', // Replace with your AppID/ClientID obtained from Azure Portal.
37 | authority: b2cPolicies.authorities.signUpSignIn.authority, // Choose sign-up/sign-in user-flow as your default.
38 | knownAuthorities: [b2cPolicies.authorityDomain], // You must identify your tenant's domain as a known authority.
39 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to "window.location.href".
40 | postLogoutRedirectUri: '/signout', // Simply remove this line if you would like navigate to index page after logout.
41 | navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
42 | },
43 | cache: {
44 | cacheLocation: 'localStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO.
45 | storeAuthStateInCookie: false, // If you wish to store cache items in cookies as well as browser cache, set this to "true".
46 | },
47 | system: {
48 | loggerOptions: {
49 | loggerCallback: (level, message, containsPii) => {
50 | if (containsPii) {
51 | return;
52 | }
53 | switch (level) {
54 | case msal.LogLevel.Error:
55 | console.error(message);
56 | return;
57 | case msal.LogLevel.Info:
58 | console.info(message);
59 | return;
60 | case msal.LogLevel.Verbose:
61 | console.debug(message);
62 | return;
63 | case msal.LogLevel.Warning:
64 | console.warn(message);
65 | return;
66 | }
67 | },
68 | },
69 | },
70 | };
71 |
72 | /**
73 | * Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
74 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
75 | */
76 | const protectedResources = {
77 | todolistApi: {
78 | endpoint: 'http://localhost:5000/api/todolist',
79 | scopes: {
80 | read: ['https://fabrikamb2c.onmicrosoft.com/ToDoList.Read'],
81 | write: ['https://fabrikamb2c.onmicrosoft.com/ToDoList.ReadWrite'],
82 | },
83 | },
84 | };
85 |
86 | /**
87 | * Scopes you add here will be prompted for user consent during sign-in.
88 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
89 | * For more information about OIDC scopes, visit:
90 | * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
91 | */
92 | const loginRequest = {
93 | scopes: [...protectedResources.todolistApi.scopes.read, ...protectedResources.todolistApi.scopes.write],
94 | };
95 |
96 | /**
97 | * An optional silentRequest object can be used to achieve silent SSO
98 | * between applications by providing a "login_hint" property.
99 | */
100 |
101 | // const silentRequest = {
102 | // scopes: ["openid", "profile"],
103 | // loginHint: "example@domain.net"
104 | // };
105 |
106 | // exporting config object for jest
107 | if (typeof exports !== 'undefined') {
108 | module.exports = {
109 | msalConfig,
110 | loginRequest,
111 | protectedResources,
112 | };
113 | }
114 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/authPopup.js:
--------------------------------------------------------------------------------
1 | // Create the main myMSALObj instance
2 | // configuration parameters are located at authConfig.js
3 | const myMSALObj = new msal.PublicClientApplication(msalConfig);
4 |
5 | let username = '';
6 |
7 | function selectAccount() {
8 | /**
9 | * See here for more info on account retrieval:
10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
11 | */
12 |
13 | const currentAccounts = myMSALObj.getAllAccounts();
14 | if (!currentAccounts || currentAccounts.length < 1) {
15 | return;
16 | } else if (currentAccounts.length > 1) {
17 | // Add your account choosing logic here
18 | console.warn('Multiple accounts detected.');
19 | } else if (currentAccounts.length === 1) {
20 | username = currentAccounts[0].username;
21 | welcomeUser(username);
22 | updateTable(currentAccounts[0]);
23 | }
24 | }
25 |
26 | function handleResponse(response) {
27 | /**
28 | * To see the full list of response object properties, visit:
29 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response
30 | */
31 |
32 | if (response !== null) {
33 | username = response.account.username;
34 | welcomeUser(username);
35 | updateTable(response.account);
36 | } else {
37 | selectAccount();
38 | }
39 | }
40 |
41 | function signIn() {
42 | /**
43 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
44 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
45 | */
46 |
47 | myMSALObj
48 | .loginPopup({
49 | ...loginRequest,
50 | redirectUri: '/redirect',
51 | })
52 | .then(handleResponse)
53 | .catch((error) => {
54 | console.log(error);
55 | // Error handling
56 | if (error.errorMessage) {
57 | // Check for forgot password error
58 | // Learn more about AAD error codes at https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes
59 | if (error.errorMessage.indexOf('AADB2C90118') > -1) {
60 | myMSALObj.loginPopup(b2cPolicies.authorities.forgotPassword);
61 | }
62 | }
63 | });
64 | }
65 |
66 |
67 | function getTokenPopup(request) {
68 | /**
69 | * See here for more information on account retrieval:
70 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
71 | */
72 | request.account = myMSALObj.getAccountByUsername(username);
73 | return myMSALObj.acquireTokenSilent(request).catch((error) => {
74 | console.warn(error);
75 | console.warn('silent token acquisition fails. acquiring token using popup');
76 | if (error instanceof msal.InteractionRequiredAuthError) {
77 | // fallback to interaction when silent call fails
78 | return myMSALObj
79 | .acquireTokenPopup(request)
80 | .then((response) => {
81 | return response;
82 | })
83 | .catch((error) => {
84 | console.error(error);
85 | });
86 | } else {
87 | console.warn(error);
88 | }
89 | });
90 | }
91 |
92 | function signOut() {
93 | /**
94 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
95 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
96 | */
97 |
98 | // Choose which account to logout from by passing a username.
99 | const logoutRequest = {
100 | account: myMSALObj.getAccountByUsername(username),
101 | };
102 | myMSALObj.logoutPopup(logoutRequest).then(() => {
103 | window.location.reload();
104 | });
105 | }
106 |
107 | function editProfile() {
108 | myMSALObj.loginPopup({
109 | ...b2cPolicies.authorities.editProfile,
110 | });
111 | }
112 |
113 | selectAccount();
114 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/authRedirect.js:
--------------------------------------------------------------------------------
1 | // Create the main myMSALObj instance
2 | // configuration parameters are located at authConfig.js
3 | const myMSALObj = new msal.PublicClientApplication(msalConfig);
4 |
5 | let username = '';
6 |
7 | /**
8 | * A promise handler needs to be registered for handling the
9 | * response returned from redirect flow. For more information, visit:
10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirect-apis
11 | */
12 | myMSALObj
13 | .handleRedirectPromise()
14 | .then(handleResponse)
15 | .catch((error) => {
16 | console.log(error);
17 |
18 | // Check for forgot password error
19 | // Learn more about AAD error codes at https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes
20 | if (error.errorMessage.indexOf('AADB2C90118') > -1) {
21 | try {
22 | myMSALObj.loginRedirect(b2cPolicies.authorities.forgotPassword);
23 | } catch (err) {
24 | console.log(err);
25 | }
26 | }
27 | });
28 |
29 | function selectAccount() {
30 | /**
31 | * See here for more info on account retrieval:
32 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
33 | */
34 |
35 | const currentAccounts = myMSALObj.getAllAccounts();
36 |
37 | if (!currentAccounts) {
38 | return;
39 | } else if (currentAccounts.length > 1) {
40 | // Add your account choosing logic here
41 | console.warn('Multiple accounts detected.');
42 | } else if (currentAccounts.length === 1) {
43 | username = currentAccounts[0].username;
44 | welcomeUser(username);
45 | updateTable(currentAccounts[0]);
46 | }
47 | }
48 |
49 | function handleResponse(response) {
50 | /**
51 | * To see the full list of response object properties, visit:
52 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response
53 | */
54 |
55 | if (response !== null) {
56 | username = response.account.username;
57 | welcomeUser(username);
58 | updateTable(response.account);
59 | } else {
60 | selectAccount();
61 | }
62 | }
63 |
64 | function signIn() {
65 | /**
66 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
67 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
68 | */
69 |
70 | myMSALObj.loginRedirect(loginRequest);
71 | }
72 |
73 | function getTokenRedirect(request) {
74 | /**
75 | * See here for more info on account retrieval:
76 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
77 | */
78 | request.account = myMSALObj.getAccountByUsername(username);
79 | return myMSALObj.acquireTokenSilent(request).catch((error) => {
80 | console.error(error);
81 | console.warn('silent token acquisition fails. acquiring token using popup');
82 | if (error instanceof msal.InteractionRequiredAuthError) {
83 | // fallback to interaction when silent call fails
84 | return myMSALObj.acquireTokenRedirect(request);
85 | } else {
86 | console.error(error);
87 | }
88 | });
89 | }
90 |
91 | function signOut() {
92 | /**
93 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
94 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
95 | */
96 |
97 | // Choose which account to logout from by passing a username.
98 | const logoutRequest = {
99 | account: myMSALObj.getAccountByUsername(username),
100 | };
101 |
102 | myMSALObj.logoutRedirect(logoutRequest);
103 | }
104 |
105 | function editProfile() {
106 | myMSALObj.loginRedirect(b2cPolicies.authorities.editProfile);
107 | }
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/fetch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Execute a fetch request with the given options
3 | * @param {string} method: GET, POST, PUT, DELETE
4 | * @param {String} endpoint: The endpoint to call
5 | * @param {Object} data: The data to send to the endpoint, if any
6 | * @returns response
7 | */
8 | function callApi(method, endpoint, token, data = null) {
9 | const headers = new Headers();
10 | const bearer = `Bearer ${token}`;
11 |
12 | headers.append('Authorization', bearer);
13 |
14 | if (data) {
15 | headers.append('Content-Type', 'application/json');
16 | }
17 |
18 | const options = {
19 | method: method,
20 | headers: headers,
21 | body: data ? JSON.stringify(data) : null,
22 | };
23 |
24 | return fetch(endpoint, options)
25 | .then((response) => {
26 | const contentType = response.headers.get("content-type");
27 |
28 | if (contentType && contentType.indexOf("application/json") !== -1) {
29 | return response.json();
30 | } else {
31 | return response;
32 | }
33 | });
34 | }
35 |
36 |
37 | /**
38 | * Handles todolist actions
39 | * @param {Object} task
40 | * @param {string} method
41 | * @param {string} endpoint
42 | */
43 | async function handleToDoListActions(task, method, endpoint) {
44 | let listData;
45 |
46 | try {
47 | const accessToken = await getToken();
48 | const data = await callApi(method, endpoint, accessToken, task);
49 |
50 | switch (method) {
51 | case 'POST':
52 | listData = JSON.parse(localStorage.getItem('todolist'));
53 | listData = [data, ...listData];
54 | localStorage.setItem('todolist', JSON.stringify(listData));
55 | AddTaskToToDoList(data);
56 | break;
57 | case 'DELETE':
58 | listData = JSON.parse(localStorage.getItem('todolist'));
59 | const index = listData.findIndex((todoItem) => todoItem.id === task.id);
60 | localStorage.setItem('todolist', JSON.stringify([...listData.splice(index, 1)]));
61 | showToDoListItems(listData);
62 | break;
63 | default:
64 | console.log('Unrecognized method.')
65 | break;
66 | }
67 | } catch (error) {
68 | console.error(error);
69 | }
70 | }
71 |
72 | /**
73 | * Handles todolist action GET action.
74 | */
75 | async function getToDos() {
76 | try {
77 | const accessToken = await getToken();
78 |
79 | const data = await callApi(
80 | 'GET',
81 | protectedResources.todolistApi.endpoint,
82 | accessToken
83 | );
84 |
85 | if (data) {
86 | localStorage.setItem('todolist', JSON.stringify(data));
87 | showToDoListItems(data);
88 | }
89 | } catch (error) {
90 | console.error(error);
91 | }
92 | }
93 |
94 | /**
95 | * Retrieves an access token.
96 | */
97 | async function getToken() {
98 | let tokenResponse;
99 |
100 | if (typeof getTokenPopup === 'function') {
101 | tokenResponse = await getTokenPopup({
102 | scopes: [...protectedResources.todolistApi.scopes.read],
103 | redirectUri: '/redirect'
104 | });
105 | } else {
106 | tokenResponse = await getTokenRedirect({
107 | scopes: [...protectedResources.todolistApi.scopes.read],
108 | });
109 | }
110 |
111 | if (!tokenResponse) {
112 | return null;
113 | }
114 |
115 | return tokenResponse.accessToken;
116 | }
117 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Azure AD B2C
8 |
9 |
10 |
11 |
14 |
15 |
17 |
18 |
19 |
20 |
34 |
35 |
Vanilla JavaScript single-page application secured with MSAL.js
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
Claim Type
44 |
Value
45 |
Description
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
69 |
72 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/redirect.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/styles.css:
--------------------------------------------------------------------------------
1 | .navbarStyle {
2 | padding: .5rem 1rem !important;
3 | }
4 |
5 | .table-responsive-ms {
6 | max-height: 39rem !important;
7 | padding-left: 10%;
8 | padding-right: 10%;
9 | }
10 |
11 | form, .group-div {
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | }
16 |
17 | .input-group, ul {
18 | width: 50% !important;
19 | }
20 |
21 |
22 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/public/ui.js:
--------------------------------------------------------------------------------
1 | // Select DOM elements to work with
2 | const signInButton = document.getElementById('signIn');
3 | const signOutButton = document.getElementById('signOut');
4 | const titleDiv = document.getElementById('title-div');
5 | const welcomeDiv = document.getElementById('welcome-div');
6 | const tableDiv = document.getElementById('table-div');
7 | const tableBody = document.getElementById('table-body-div');
8 | const toDoListLink = document.getElementById('toDoListLink');
9 | const toDoForm = document.getElementById('form');
10 | const textInput = document.getElementById('textInput');
11 | const toDoListDiv = document.getElementById('groupDiv');
12 | const todoListItems = document.getElementById('toDoListItems');
13 | const editProfileButton = document.getElementById('editProfileButton');
14 |
15 | toDoForm.addEventListener('submit', (e) => {
16 | e.preventDefault();
17 | let task = { description: textInput.value };
18 | handleToDoListActions(task, 'POST', protectedResources.todolistApi.endpoint);
19 | toDoForm.reset();
20 | });
21 |
22 | function welcomeUser(username) {
23 | signInButton.classList.add('d-none');
24 | signOutButton.classList.remove('d-none');
25 | toDoListLink.classList.remove('d-none');
26 | editProfileButton.classList.remove('d-none');
27 | titleDiv.classList.add('d-none');
28 | welcomeDiv.classList.remove('d-none');
29 | welcomeDiv.innerHTML = `Welcome ${username}!`;
30 | }
31 |
32 | function updateTable(account) {
33 | tableDiv.classList.remove('d-none');
34 | const tokenClaims = createClaimsTable(account.idTokenClaims);
35 |
36 | Object.keys(tokenClaims).forEach((key) => {
37 | let row = tableBody.insertRow(0);
38 | let cell1 = row.insertCell(0);
39 | let cell2 = row.insertCell(1);
40 | let cell3 = row.insertCell(2);
41 | cell1.innerHTML = tokenClaims[key][0];
42 | cell2.innerHTML = tokenClaims[key][1];
43 | cell3.innerHTML = tokenClaims[key][2];
44 | });
45 | }
46 |
47 | function showToDoListItems(response) {
48 | todoListItems.replaceChildren();
49 | tableDiv.classList.add('d-none');
50 | toDoForm.classList.remove('d-none');
51 | toDoListDiv.classList.remove('d-none');
52 | if (!!response.length) {
53 | response.forEach((task) => {
54 | AddTaskToToDoList(task);
55 | });
56 | }
57 | }
58 |
59 | function AddTaskToToDoList(task) {
60 | let li = document.createElement('li');
61 | let button = document.createElement('button');
62 | button.innerHTML = 'Delete';
63 | button.classList.add('btn', 'btn-danger');
64 | button.addEventListener('click', () => {
65 | handleToDoListActions(task, 'DELETE', protectedResources.todolistApi.endpoint + `/${task.id}`);
66 | });
67 | li.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center');
68 | li.innerHTML = task.description;
69 | li.appendChild(button);
70 | todoListItems.appendChild(li);
71 | }
72 |
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/sample.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 |
5 | const request = require('supertest');
6 | const path = require('path');
7 | const fs = require('fs');
8 |
9 | const app = require('./server.js');
10 |
11 | jest.dontMock('fs');
12 |
13 | const html = fs.readFileSync(path.resolve(__dirname, './public/index.html'), 'utf8');
14 |
15 | describe('Sanitize index page', () => {
16 | beforeAll(async() => {
17 | global.document.documentElement.innerHTML = html.toString();
18 | });
19 |
20 | it('should have valid cdn link', () => {
21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser");
22 | });
23 | });
24 |
25 | describe('Sanitize configuration object', () => {
26 | beforeAll(() => {
27 | global.msalConfig = require('./public/authConfig.js').msalConfig;
28 | });
29 |
30 | it('should define the config object', () => {
31 | expect(msalConfig).toBeDefined();
32 | });
33 |
34 | it('should contain credentials', () => {
35 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(true);
37 | });
38 |
39 | it('should contain authority URI', () => {
40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true);
42 | });
43 | });
44 |
45 | describe('Ensure pages served', () => {
46 |
47 | beforeAll(() => {
48 | process.env.NODE_ENV = 'test';
49 | });
50 |
51 | it('should get index page', async () => {
52 | const res = await request(app)
53 | .get('/');
54 |
55 | const data = await fs.promises.readFile(path.join(__dirname, './public/index.html'), 'utf8');
56 | expect(res.statusCode).toEqual(200);
57 | expect(res.text).toEqual(data);
58 | });
59 | });
--------------------------------------------------------------------------------
/3-Authorization-II/2-call-api-b2c/SPA/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const path = require('path');
4 |
5 | const DEFAULT_PORT = process.env.PORT || 6420;
6 |
7 | // initialize express.
8 | const app = express();
9 |
10 | // Configure morgan module to log all requests.
11 | app.use(morgan('dev'));
12 |
13 | // Setup app folders.
14 | app.use(express.static('public'));
15 |
16 | // set up a route for redirect.html
17 | app.get('/redirect', (req, res) => {
18 | res.sendFile(path.join(__dirname + '/public/redirect.html'));
19 | });
20 |
21 | // Set up a route for index.html
22 | app.get('/', (req, res) => {
23 | res.sendFile(path.join(__dirname + '/index.html'));
24 | });
25 |
26 | app.listen(DEFAULT_PORT, () => {
27 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`);
28 | });
29 |
30 | module.exports = app;
31 |
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/api_step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/api_step1.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/api_step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/api_step2.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/api_step3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/api_step3.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/disable_easy_auth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/disable_easy_auth.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/enable_cors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/enable_cors.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/screenshot.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/spa_step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step1.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/spa_step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step2.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/spa_step3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step3.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/spa_step4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step4.png
--------------------------------------------------------------------------------
/4-Deployment/ReadmeFiles/topology_dep.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/topology_dep.png
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## 01/04/2021
4 |
5 | * New chapter (4-2) added.
6 |
7 | ## 12/07/2020
8 |
9 | * Updated MSAL.js to 2.7.0
10 | * B2C null access token issue resolved.
11 | * UI update code removed from auth methods.
12 | * Folder structure and naming revised.
13 |
14 | ## 10/20/2020
15 |
16 | * Initial sample.
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to [project-title]
2 |
3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
6 |
7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
9 | provided by the bot. You will only need to do this once across all repos using our CLA.
10 |
11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
14 |
15 | - [Code of Conduct](#coc)
16 | - [Issues and Bugs](#issue)
17 | - [Feature Requests](#feature)
18 | - [Submission Guidelines](#submit)
19 |
20 | ## Code of Conduct
21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
22 |
23 | ## Found an Issue?
24 | If you find a bug in the source code or a mistake in the documentation, you can help us by
25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can
26 | [submit a Pull Request](#submit-pr) with a fix.
27 |
28 | ## Want a Feature?
29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub
30 | Repository. If you would like to *implement* a new feature, please submit an issue with
31 | a proposal for your work first, to be sure that we can use it.
32 |
33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
34 |
35 | ## Submission Guidelines
36 |
37 | ### Submitting an Issue
38 | Before you submit an issue, search the archive, maybe your question was already answered.
39 |
40 | If your issue appears to be a bug, and hasn't been reported, open a new issue.
41 | Help us to maximize the effort we can spend fixing issues and adding new
42 | features, by not reporting duplicate issues. Providing the following information will increase the
43 | chances of your issue being dealt with quickly:
44 |
45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
46 | * **Version** - what version is affected (e.g. 0.1.2)
47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
48 | * **Browsers and Operating System** - is this a problem with all browsers?
49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps
50 | * **Related Issues** - has a similar issue been reported before?
51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
52 | causing the problem (line of code or commit)
53 |
54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new].
55 |
56 | ### Submitting a Pull Request (PR)
57 | Before you submit your Pull Request (PR) consider the following guidelines:
58 |
59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR
60 | that relates to your submission. You don't want to duplicate effort.
61 |
62 | * Make your changes in a new git fork:
63 |
64 | * Commit your changes using a descriptive commit message
65 | * Push your fork to GitHub:
66 | * In GitHub, create a pull request
67 | * If we suggest changes then:
68 | * Make the required updates.
69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request):
70 |
71 | ```shell
72 | git rebase master -i
73 | git push -f
74 | ```
75 |
76 | That's it! Thank you for your contribution!
77 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------