Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

msal-browser testing sample using msal-node to obtain tokens #2771

Merged
merged 5 commits into from Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,8 @@
{
"type": "prerelease",
"comment": "Add getKVStore to tokenCache (#2771)",
"packageName": "@azure/msal-node",
"email": "thomas.norling@microsoft.com",
"dependentChangeType": "patch",
"date": "2020-12-17T00:25:06.891Z"
}
6 changes: 5 additions & 1 deletion lib/msal-node/src/cache/TokenCache.ts
Expand Up @@ -5,7 +5,7 @@

import { Storage } from "./Storage";
import { StringUtils, AccountEntity, AccountInfo, Logger, ISerializableTokenCache, ICachePlugin, TokenCacheContext } from "@azure/msal-common";
import { InMemoryCache, JsonCache, SerializedAccountEntity, SerializedAccessTokenEntity, SerializedRefreshTokenEntity, SerializedIdTokenEntity, SerializedAppMetadataEntity } from "./serializer/SerializerTypes";
import { InMemoryCache, JsonCache, SerializedAccountEntity, SerializedAccessTokenEntity, SerializedRefreshTokenEntity, SerializedIdTokenEntity, SerializedAppMetadataEntity, CacheKVStore } from "./serializer/SerializerTypes";
import { Deserializer } from "./serializer/Deserializer";
import { Serializer } from "./serializer/Serializer";

Expand Down Expand Up @@ -88,6 +88,10 @@ export class TokenCache implements ISerializableTokenCache {
}
}

getKVStore(): CacheKVStore {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should add tests for this?

return this.storage.getCache();
}

/**
* API that retrieves all accounts currently in cache to the user
*/
Expand Down
1 change: 1 addition & 0 deletions lib/msal-node/src/index.ts
Expand Up @@ -10,6 +10,7 @@ export { Storage } from "./cache/Storage";
export { Serializer } from "./cache/serializer/Serializer";
export { Deserializer } from "./cache/serializer/Deserializer";
export { TokenCache } from "./cache/TokenCache";
export { CacheKVStore } from "./cache/serializer/SerializerTypes";

// crypto
export { CryptoProvider } from "./crypto/CryptoProvider";
Expand Down
15 changes: 15 additions & 0 deletions samples/msal-browser-samples/TestingSample/.babelrc
@@ -0,0 +1,15 @@
{
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env",
{
"modules": "commonjs"
}
]
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}
3 changes: 3 additions & 0 deletions samples/msal-browser-samples/TestingSample/.beachballrc
@@ -0,0 +1,3 @@
{
"shouldPublish": false
}
24 changes: 24 additions & 0 deletions samples/msal-browser-samples/TestingSample/Readme.md
@@ -0,0 +1,24 @@
# MSAL.js Jest/Puppeteer Testing Example

⚠️ Warning: The ROPC Flow in msal-node should only be used for testing and is not suitable for authenticating users outside of a testing environment ⚠️

## About this sample

This sample demonstrates how you can run e2e tests against an application that uses msal-browser to obtain tokens and sign users in.
Using the [ROPC flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) in [msal-node](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node) you can pre-populate local or session storage with tokens without requiring your test to navigate through the AAD sign-in pages. This allows you to test your application with a real user and real tokens without testing 3rd party sites.

## Pre-requisites

- Ensure the clientId and authority in `test/browser-test.spec.ts` (msal-node configuration) match what is set in `app/authConfig.js` (msal-browser configuration)
- You must use a tenanted authority to use the ROPC flow
- Implement a function to get a username and password for the test account
- Ensure the `usernamePasswordRequest` request contains the same scopes your SPA needs tokens for, making several requests if needed.

## Run the test

```javascript
// Install dependencies
npm install
// Run tests using jest and puppeteer
npm test
```
88 changes: 88 additions & 0 deletions samples/msal-browser-samples/TestingSample/app/auth.js
@@ -0,0 +1,88 @@
// 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;
let accountId = "";

// Create the main myMSALObj instance
// configuration parameters are located at authConfig.js
const myMSALObj = new msal.PublicClientApplication(msalConfig);

// Redirect: once login is successful and redirects with tokens, call Graph API
myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => {
console.error(err);
});

function handleResponse(resp) {
if (resp !== null) {
accountId = resp.account.homeAccountId;
showWelcomeMessage(resp.account);
} else {
// need to call getAccount here?
const currentAccounts = myMSALObj.getAllAccounts();
if (!currentAccounts || currentAccounts.length < 1) {
return;
} else if (currentAccounts.length > 1) {
// Add choose account code here
} else if (currentAccounts.length === 1) {
accountId = currentAccounts[0].homeAccountId;
showWelcomeMessage(currentAccounts[0]);
}
}
}

async function signIn(method) {
signInType = isIE ? "loginRedirect" : method;
if (signInType === "loginPopup") {
return myMSALObj.loginPopup(loginRequest).then(handleResponse).catch(function (error) {
console.log(error);
});
} else if (signInType === "loginRedirect") {
return myMSALObj.loginRedirect(loginRequest)
}
}

function signOut() {
const logoutRequest = {
account: myMSALObj.getAccountByHomeId(accountId)
};

myMSALObj.logout(logoutRequest);
}

async function getTokenPopup(request, account) {
request.account = account;
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof msal.InteractionRequiredAuthError) {
console.log("acquiring token using popup");
return myMSALObj.acquireTokenPopup(request).catch(error => {
console.error(error);
});
} else {
console.error(error);
}
});
}

// This function can be removed if you do not need to support IE
async function getTokenRedirect(request, account) {
request.account = account;
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
console.log("acquiring token using redirect");
myMSALObj.acquireTokenRedirect(request);
} else {
console.error(error);
}
});
}
55 changes: 55 additions & 0 deletions samples/msal-browser-samples/TestingSample/app/authConfig.js
@@ -0,0 +1,55 @@
// Config object to be passed to Msal on creation
const msalConfig = {
auth: {
clientId: "client_id_here",
authority: "tenanted_authority_here"
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case msal.LogLevel.Error:
console.error(message);
return;
case msal.LogLevel.Info:
console.info(message);
return;
case msal.LogLevel.Verbose:
console.debug(message);
return;
case msal.LogLevel.Warning:
console.warn(message);
return;
}
}
}
}
};

// Add here scopes for id token to be used at MS Identity Platform endpoints.
const loginRequest = {
scopes: ["User.Read"]
};

// Add here the endpoints for MS Graph API services you would like to use.
const graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
graphMailEndpoint: "https://graph.microsoft.com/v1.0/me/messages"
};

// Add here scopes for access token to be used at MS Graph API endpoints.
const tokenRequest = {
scopes: ["Mail.Read"],
forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
};

const silentRequest = {
scopes: ["openid", "profile", "User.Read", "Mail.Read"]
};
64 changes: 64 additions & 0 deletions samples/msal-browser-samples/TestingSample/app/graph.js
@@ -0,0 +1,64 @@
// Helper function to call MS Graph API endpoint
// using authorization bearer token scheme
function callMSGraph(endpoint, accessToken, callback) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;

headers.append("Authorization", bearer);

const options = {
method: "GET",
headers: headers
};

console.log('request made to Graph API at: ' + new Date().toString());

fetch(endpoint, options)
.then(response => response.json())
.then(response => callback(response, endpoint))
.catch(error => console.log(error));
}

async function seeProfile() {
const currentAcc = myMSALObj.getAccountByHomeId(accountId);
if (currentAcc) {
const response = await getTokenPopup(loginRequest, currentAcc).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
profileButton.style.display = 'none';
}
}

async function readMail() {
const currentAcc = myMSALObj.getAccountByHomeId(accountId);
if (currentAcc) {
const response = await getTokenPopup(tokenRequest, currentAcc).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
mailButton.style.display = 'none';
}
}

async function seeProfileRedirect() {
const currentAcc = myMSALObj.getAccountByHomeId(accountId);
if (currentAcc) {
const response = await getTokenRedirect(loginRequest, currentAcc).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
profileButton.style.display = 'none';
}
}

async function readMailRedirect() {
const currentAcc = myMSALObj.getAccountByHomeId(accountId);
if (currentAcc) {
const response = await getTokenRedirect(tokenRequest, currentAcc).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
mailButton.style.display = 'none';
}
}
70 changes: 70 additions & 0 deletions samples/msal-browser-samples/TestingSample/app/index.html
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Quickstart | MSAL.JS Vanilla JavaScript SPA</title>

<script type="text/javascript" src="https://alcdn.msauth.net/browser/2.8.0/js/msal-browser.min.js"></script>

<!-- adding Bootstrap 4 for UI components -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="SHORTCUT ICON" href="https://c.s-microsoft.com/favicon.ico?v2" type="image/x-icon">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="/">MS Identity Platform</a>
<div class="btn-group ml-auto dropleft">
<button type="button" id="SignIn" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Sign In
</button>
<div class="dropdown-menu">
<button class="dropdown-item" id="loginPopup" onclick="signIn(this.id)">Sign in using Popup</button>
<button class="dropdown-item" id="loginRedirect" onclick="signIn(this.id)">Sign in using Redirect</button>
</div>
</div>
</nav>
<br>
<h5 class="card-header text-center">Vanilla JavaScript SPA calling MS Graph API with MSAL.JS</h5>
<br>
<div class="row" style="margin:auto" >
<div id="card-div" class="col-md-3" style="display:none">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" id="WelcomeMessage">Please sign-in to see your profile and read your mails</h5>
<div id="profile-div"></div>
<br>
<br>
<button class="btn btn-primary" id="seeProfile" onclick="seeProfile()">See Profile</button>
<br>
<br>
<button class="btn btn-primary" id="readMail" onclick="readMail()">Read Mails</button>
</div>
</div>
</div>
<br>
<br>
<div class="col-md-4">
<div class="list-group" id="list-tab" role="tablist">
</div>
</div>
<div class="col-md-5">
<div class="tab-content" id="nav-tabContent">
</div>
</div>
</div>
<br>
<br>

<!-- importing bootstrap.js and supporting js libraries -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

<!-- importing app scripts | load order is important -->
<script type="text/javascript" src="./authConfig.js"></script>
<script type="text/javascript" src="./ui.js"></script>
<script type="text/javascript" src="./auth.js"></script>
<script type="text/javascript" src="./graph.js"></script>
</body>
</html>