diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/b2c/auth.js b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/auth.js new file mode 100644 index 0000000000..13d6323c85 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/auth.js @@ -0,0 +1,93 @@ +// Browser check variables +// If you support IE, our recommendation is that you sign-in using Redirect APIs +// If you as a developer are testing using Edge InPrivate mode, please add "isEdge" to the if check +const ua = window.navigator.userAgent; +const msie = ua.indexOf("MSIE "); +const msie11 = ua.indexOf("Trident/"); +const msedge = ua.indexOf("Edge/"); +const isIE = msie > 0 || msie11 > 0; +const isEdge = msedge > 0; + +let signInType; + +// Create the main myMSALObj instance +// configuration parameters are located at authConfig.js +const myMSALObj = new Msal.UserAgentApplication(msalConfig); + +// Register Callbacks for Redirect flow +myMSALObj.handleRedirectCallback(authRedirectCallBack); + +function authRedirectCallBack(error, response) { + if (error) { + console.log(error); + } else { + if (response.tokenType === "id_token" && myMSALObj.getAccount() && !myMSALObj.isCallback(window.location.hash)) { + console.log('id_token acquired at: ' + new Date().toString()); + showWelcomeMessage(myMSALObj.getAccount()); + } else if (response.tokenType === "access_token") { + console.log('access_token acquired at: ' + new Date().toString()); + updateUI(response); + accessTokenButtonPopup.style.display = 'none'; + accessTokenButtonRedirect.style.display = 'none'; + } else { + console.log("token type is:" + response.tokenType); + } + } +} + +// Redirect: once login is successful and redirects with tokens, call Graph API +if (myMSALObj.getAccount() && !myMSALObj.isCallback(window.location.hash)) { + // avoid duplicate code execution on page load in case of iframe and Popup window. + showWelcomeMessage(myMSALObj.getAccount()); +} + +function signIn(method) { + signInType = isIE ? "loginRedirect" : method; + if (signInType === "loginPopup") { + myMSALObj.loginPopup(loginRequest) + .then(loginResponse => { + console.log(loginResponse); + if (myMSALObj.getAccount()) { + showWelcomeMessage(myMSALObj.getAccount()); + } + }).catch(function (error) { + console.log(error); + }); + } else if (signInType === "loginRedirect") { + myMSALObj.loginRedirect(loginRequest) + } +} + +function signOut() { + myMSALObj.logout(); +} + +function getAccessTokenPopup() { + if (myMSALObj.getAccount()) { + myMSALObj.acquireTokenPopup(loginRequest).then(response => { + updateUI(response); + accessTokenButtonPopup.style.display = 'none'; + accessTokenButtonRedirect.style.display = 'none'; + }).catch(error => { + console.log(error); + }); + } +} + +function getAccessTokenRedirect() { + if (myMSALObj.getAccount()) { + myMSALObj.acquireTokenRedirect(loginRequest); + } +} + +function getAccessTokenSilent() { + if (myMSALObj.getAccount()) { + myMSALObj.acquireTokenSilent(loginRequest).then(response => { + updateUI(response); + accessTokenButtonPopup.style.display = 'none'; + accessTokenButtonRedirect.style.display = 'none'; + }).catch(error => { + console.log(error); + }) + } +} \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/b2c/authConfig.js b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/authConfig.js new file mode 100644 index 0000000000..915a3dcda7 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/authConfig.js @@ -0,0 +1,18 @@ +// Config object to be passed to Msal on creation +const msalConfig = { + auth: { + clientId: "e3b9ad76-9763-4827-b088-80c7a7888f79", + authority: "https://login.microsoftonline.com/tfp/msidlabb2c.onmicrosoft.com/B2C_1_SISOPolicy/", + knownAuthorities: ["login.microsoftonline.com"] + }, + cache: { + cacheLocation: "localStorage", // This configures where your cache will be stored + storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge + } +}; + +// Add here scopes for id token to be used at MS Identity Platform endpoints. +const loginRequest = { + scopes: ["https://msidlabb2c.onmicrosoft.com/msidlabb2capi/read"], + forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token +}; \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/b2c/index.html b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/index.html new file mode 100644 index 0000000000..dd569d1470 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/index.html @@ -0,0 +1,73 @@ + + + + + + Quickstart | MSAL.JS Vanilla JavaScript SPA + + + + + + + + + + +
+
Vanilla JavaScript SPA calling MS Graph API with MSAL.JS
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/b2c/test/browser.spec.ts b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/test/browser.spec.ts new file mode 100644 index 0000000000..2bfb6a7a83 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/test/browser.spec.ts @@ -0,0 +1,244 @@ +import * as Mocha from "mocha"; +import puppeteer from "puppeteer"; +import { expect } from "chai"; +import fs from "fs"; +import { LabClient, ILabApiParams } from "../../../e2eTests/LabClient"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots`; +let SCREENSHOT_NUM = 0; +let username = ""; +let accountPwd = ""; + +// Set App Info +const clientId = "e3b9ad76-9763-4827-b088-80c7a7888f79"; +const authority = "https://login.microsoftonline.com/tfp/msidlabb2c.onmicrosoft.com/B2C_1_SISOPolicy/" +const scopes = ["https://msidlabb2c.onmicrosoft.com/msidlabb2capi/read"] +const idTokenCacheKey = "msal." + clientId + ".idtoken" +const clientInfoCacheKey = "msal." + clientId + ".client.info" + +function setupScreenshotDir() { + if (!fs.existsSync(`${SCREENSHOT_BASE_FOLDER_NAME}`)) { + fs.mkdirSync(SCREENSHOT_BASE_FOLDER_NAME); + } +} + +async function setupCredentials() { + const testCreds = new LabClient(); + const userParams: ILabApiParams = {envName: "azurecloud"}; + const envResponse = await testCreds.getUserVarsByCloudEnvironment(userParams); + const testEnv = envResponse[0]; + if (testEnv.upn) { + username = testEnv.upn; + } + + const testPwdSecret = await testCreds.getSecret(testEnv.labName); + + accountPwd = testPwdSecret.value; +} + +async function takeScreenshot(page: puppeteer.Page, testName: string, screenshotName: string): Promise { + const screenshotFolderName = `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + if (!fs.existsSync(`${screenshotFolderName}`)) { + fs.mkdirSync(screenshotFolderName); + } + await page.screenshot({ path: `${screenshotFolderName}/${++SCREENSHOT_NUM}_${screenshotName}.png` }); +} + +async function enterCredentials(page: puppeteer.Page, testName: string): Promise { + await page.waitForNavigation({ waitUntil: "networkidle0"}); + await page.waitForSelector("#i0116"); + await takeScreenshot(page, testName, `loginPage`); + await page.type("#i0116", username); + await page.click("#idSIButton9"); + await page.waitForNavigation({ waitUntil: "networkidle0"}); + await page.waitForSelector("#i0118"); + await takeScreenshot(page, testName, `pwdInputPage`); + await page.type("#i0118", accountPwd); + await page.click("#idSIButton9"); + + // Keep me signed in dialog box + await page.waitForSelector("#idSIButton9"); + await page.click("#idSIButton9"); +} + +async function loginRedirect(page: puppeteer.Page, testName: string): Promise { + // Home Page + await takeScreenshot(page, testName, `samplePageInit`); + // Click Sign In + await page.click("#SignIn"); + await takeScreenshot(page, testName, `signInClicked`); + // Click Sign In With Redirect + await page.click("#loginRedirect"); + await page.waitForSelector("#MSIDLAB4_AzureAD"); + await takeScreenshot(page, testName, "b2cSignInPage"); + // Select Lab Provider + await page.click("#MSIDLAB4_AzureAD"); + + // Enter credentials + await enterCredentials(page, testName); + // Wait for return to page + await page.waitForSelector("#getAccessTokenRedirect"); + await takeScreenshot(page, testName, `samplePageLoggedIn`); +} + +async function loginPopup(page: puppeteer.Page, testName: string): Promise { + // Home Page + await takeScreenshot(page, testName, `samplePageInit`); + // Click Sign In + await page.click("#SignIn"); + await takeScreenshot(page, testName, `signInClicked`); + // Click Sign In With Popup + const newPopupWindowPromise = new Promise(resolve => page.once('popup', resolve)); + await page.click("#loginPopup"); + const popupPage = await newPopupWindowPromise; + const popupWindowClosed = new Promise(resolve => popupPage.once("close", resolve)); + + await popupPage.waitForSelector("#MSIDLAB4_AzureAD"); + await takeScreenshot(popupPage, testName, "b2cSignInPage"); + // Select Lab Provider + await popupPage.click("#MSIDLAB4_AzureAD"); + + // Enter credentials + await enterCredentials(popupPage, testName); + // Wait until popup window closes and see that we are logged in + await popupWindowClosed; + await page.waitForSelector("#getAccessTokenPopup"); + await takeScreenshot(page, testName, `samplePageLoggedIn`); +} + +async function validateAccessTokens(page: puppeteer.Page, localStorage: Storage) { + let accessTokensFound = 0 + let accessTokenMatch: boolean; + + Object.keys(localStorage).forEach(async (key) => { + if (key.includes("authority")) { + let cacheKey = JSON.parse(key); + let cachedScopeList = cacheKey.scopes.split(" "); + + accessTokenMatch = cacheKey.authority === authority.toLowerCase() && + cacheKey.clientId.toLowerCase() === clientId.toLowerCase() && + scopes.every(scope => cachedScopeList.includes(scope)); + + if (accessTokenMatch) { + accessTokensFound += 1; + await page.evaluate((key) => window.localStorage.removeItem(key)) + } + } + }); + + return accessTokensFound; +} + +describe("Browser tests", function () { + this.timeout(8000); + this.retries(1); + + let browser: puppeteer.Browser; + before(async () => { + setupScreenshotDir(); + setupCredentials(); + browser = await puppeteer.launch({ + headless: true, + ignoreDefaultArgs: ['--no-sandbox', '–disable-setuid-sandbox'] + }); + }); + + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + + after(async () => { + await context.close(); + await browser.close(); + }); + + describe("Test Login functions", async () => { + beforeEach(async () => { + SCREENSHOT_NUM = 0; + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + await page.goto('http://localhost:30662/'); + }); + + afterEach(async () => { + await page.close(); + }); + + it("Performs loginRedirect", async () => { + const testName = "redirectBaseCase"; + await loginRedirect(page, testName); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + }); + + it("Performs loginPopup", async () => { + const testName = "popupBaseCase"; + await loginPopup(page, testName); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + }); + }); + + describe("Test AcquireToken functions", async () => { + const testName = "acquireTokenBaseCase"; + + before(async () => { + SCREENSHOT_NUM = 0; + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + await page.goto('http://localhost:30662/'); + await loginPopup(page, testName); + }); + + after(async () => { + await page.close(); + }); + + afterEach(async () => { + await page.reload(); + }); + + it("Test acquireTokenRedirect", async () => { + await page.click("#getAccessTokenRedirect"); + await page.waitForSelector("#access-token-info"); + await takeScreenshot(page, testName, "accessTokenAcquiredRedirect"); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + + const accessTokensFound = await validateAccessTokens(page, localStorage); + expect(accessTokensFound).to.equal(1); + }); + + it("Test acquireTokenPopup", async () => { + await page.click("#getAccessTokenPopup"); + await page.waitForSelector("#access-token-info"); + await takeScreenshot(page, testName, "accessTokenAcquiredPopup"); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + + const accessTokensFound = await validateAccessTokens(page, localStorage); + expect(accessTokensFound).to.equal(1); + }); + + it("Test acquireTokenSilent", async () => { + await page.click("#getAccessTokenSilent"); + await page.waitForSelector("#access-token-info"); + await takeScreenshot(page, testName, "accessTokenAcquiredSilently"); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + + const accessTokensFound = await validateAccessTokens(page, localStorage); + expect(accessTokensFound).to.equal(1); + }); + }); +}); \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/b2c/ui.js b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/ui.js new file mode 100644 index 0000000000..a6af5fa69f --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/b2c/ui.js @@ -0,0 +1,33 @@ +// Select DOM elements to work with +const welcomeDiv = document.getElementById("WelcomeMessage"); +const signInButton = document.getElementById("SignIn"); +const cardDiv = document.getElementById("card-div"); +const accessTokenButtonRedirect = document.getElementById("getAccessTokenRedirect"); +const accessTokenButtonPopup = document.getElementById("getAccessTokenPopup"); +const accessTokenButtonSilent = document.getElementById("getAccessTokenSilent"); +const profileDiv = document.getElementById("profile-div"); + +function showWelcomeMessage(account) { + // Reconfiguring DOM elements + cardDiv.style.display = 'initial'; + welcomeDiv.innerHTML = `Welcome ${account.name}`; + signInButton.nextElementSibling.style.display = 'none'; + signInButton.setAttribute("onclick", "signOut();"); + signInButton.setAttribute('class', "btn btn-success") + signInButton.innerHTML = "Sign Out"; +} + +function updateUI(response) { + const oldAccessTokenDiv = document.getElementById('access-token-info'); + if (oldAccessTokenDiv) { + oldAccessTokenDiv.remove(); + } + const accessTokenDiv = document.createElement('div'); + accessTokenDiv.id = "access-token-info"; + profileDiv.appendChild(accessTokenDiv); + + const scopes = document.createElement('p'); + scopes.innerHTML = "Access Token Acquired for Scopes: " + response.scopes; + + accessTokenDiv.appendChild(scopes); +} \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/default/test/browser.spec.ts b/samples/msal-core-samples/VanillaJSTestApp/app/default/test/browser.spec.ts index e7236b8ffc..c976da0952 100644 --- a/samples/msal-core-samples/VanillaJSTestApp/app/default/test/browser.spec.ts +++ b/samples/msal-core-samples/VanillaJSTestApp/app/default/test/browser.spec.ts @@ -2,7 +2,7 @@ import * as Mocha from "mocha"; import puppeteer from "puppeteer"; import { expect } from "chai"; import fs from "fs"; -import { LabClient } from "../../../e2eTests/LabClient"; +import { LabClient, ILabApiParams } from "../../../e2eTests/LabClient"; const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots`; let SCREENSHOT_NUM = 0; @@ -17,7 +17,8 @@ function setupScreenshotDir() { async function setupCredentials() { const testCreds = new LabClient(); - const envResponse = await testCreds.getUserVarsByCloudEnvironment("azurecloud"); + const userParams: ILabApiParams = {envName: "azurecloud"}; + const envResponse = await testCreds.getUserVarsByCloudEnvironment(userParams); const testEnv = envResponse[0]; if (testEnv.upn) { username = testEnv.upn; diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/test/browser.spec.ts b/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/test/browser.spec.ts index d2373b5ad0..e793c0f944 100644 --- a/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/test/browser.spec.ts +++ b/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/test/browser.spec.ts @@ -2,7 +2,7 @@ import * as Mocha from "mocha"; import puppeteer from "puppeteer"; import { expect } from "chai"; import fs from "fs"; -import { LabClient } from "../../../e2eTests/LabClient"; +import { LabClient, ILabApiParams } from "../../../e2eTests/LabClient"; const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots`; let SCREENSHOT_NUM = 0; @@ -17,7 +17,8 @@ function setupScreenshotDir() { async function setupCredentials() { const testCreds = new LabClient(); - const envResponse = await testCreds.getUserVarsByCloudEnvironment("azurecloud"); + const userParams: ILabApiParams = {envName: "azurecloud"}; + const envResponse = await testCreds.getUserVarsByCloudEnvironment(userParams); const testEnv = envResponse[0]; if (testEnv.upn) { username = testEnv.upn; diff --git a/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts b/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts index 566625af30..4baa72b9ae 100644 --- a/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts +++ b/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts @@ -2,6 +2,12 @@ import { ClientSecretCredential, AccessToken } from "@azure/identity"; import axios from "axios"; const labApiUri = "https://msidlab.com/api" +export interface ILabApiParams { + envName?: string, + userType?: string, + b2cProvider?: string +}; + export class LabClient { private credentials: ClientSecretCredential; @@ -35,10 +41,26 @@ export class LabClient { return null; } - async getUserVarsByCloudEnvironment(envName: string): Promise { + async getUserVarsByCloudEnvironment(apiParams: ILabApiParams): Promise { const accessToken = await this.getCurrentToken(); + let queryParams: Array = []; + + if (apiParams.envName) { + queryParams.push(`envname=${apiParams.envName}`); + } + if (apiParams.userType) { + queryParams.push(`usertype=${apiParams.userType}`); + } + if (apiParams.b2cProvider) { + queryParams.push(`b2cprovider=${apiParams.b2cProvider}`); + } + + if (queryParams.length <= 0) { + throw "Must provide at least one param to getUserVarsByCloudEnvironment"; + } + const apiUrl = '/user?' + queryParams.join("&"); - return await this.requestLabApi(`/user?azureenvironment=${envName}`, accessToken); + return await this.requestLabApi(apiUrl, accessToken); } async getSecret(secretName: string): Promise {