Testing
June 12, 2026 ยท View on GitHub
The loadExternalTokens() API
The loadExternalTokens() API allows the loading of id, access and refresh tokens to the MSAL cache, which can then be fetched using acquireTokenSilent().
Note: This is an advanced feature that is intended for testing purposes in the browser environment only. We do not recommend using this in a production app. For E2E testing recommendations and a working sample, please refer to our TestingSample instead.
The loadExternalTokens() API is a public API facilitating apps to custom load tokens to msal js cache.
await loadExternalTokens(
config,
silentRequest,
serverResponse,
loadTokenOptions,
);
loadExternalTokens() takes in a request of type SilentRequest, a response of type ExternalTokenResponse, and options of type LoadTokenOptions.
See the type definitions for each, which can be imported from @azure/msal-browser:
E2E Testing with loadExternalTokens
Because loadExternalTokens() requires a browser environment (it stores tokens in sessionStorage or localStorage using the correct browser-side cache key schema), it must be called inside the browser rather than in your test runner process.
The recommended approach is to:
- Obtain a raw token server response outside the browser (e.g. via msal-node's ROPC flow, using hardcoded test tokens, or by calling your own auth service).
- Pass the response into the browser and call
loadExternalTokens()there, using whatever mechanism your test framework provides. - Reload the page so the application reads the freshly-populated cache and recognises the user as signed in.
Playwright
Use page.evaluate to execute code in the browser context. The script injected into the browser needs a way to call loadExternalTokens โ one common approach is to expose your MSAL instance (or the package namespace) on window, but any mechanism that makes the function reachable from an injected script works.
import { test, expect, type Page } from "@playwright/test";
import { PublicClientApplication } from "@azure/msal-node";
const tenantId = "your-tenant-id";
const msalConfig = {
auth: {
clientId: "your-client-id",
authority: `https://login.microsoftonline.com/${tenantId}`,
},
cache: { cacheLocation: "sessionStorage" },
};
const scopes = ["User.Read"];
/**
* Obtain tokens using msal-node's ROPC flow.
* This runs in Node.js (test runner) and does NOT require a browser.
*
* The returned object is mapped to the ExternalTokenResponse format, which
* can be passed directly to loadExternalTokens.
*
* WARNING: The ROPC flow should only be used for testing purposes.
*/
async function getServerTokenResponse(
username: string,
password: string
): Promise<Record<string, unknown>> {
const pca = new PublicClientApplication({ auth: msalConfig.auth });
const result = await pca.acquireTokenByUsernamePassword({
scopes,
username,
password,
});
if (!result) {
throw new Error("Failed to acquire token via ROPC");
}
const now = Math.floor(Date.now() / 1000);
// Default to 3600 seconds (1 hour) if expiresOn is not available
const expiresIn = result.expiresOn
? Math.floor(result.expiresOn.getTime() / 1000) - now
: 3600;
return {
token_type: result.tokenType || "Bearer",
scope: result.scopes.join(" "),
expires_in: expiresIn,
access_token: result.accessToken,
id_token: result.idToken,
};
}
async function loadTokensInBrowser(
page: Page,
serverResponse: Record<string, unknown>
): Promise<void> {
await page.evaluate(
async ([config, request, response]) => {
// Access loadExternalTokens via whatever mechanism your app exposes it
// (e.g. window.msal, a dedicated testbed helper, etc.).
// The fourth argument is LoadTokenOptions (empty object uses defaults)
await (window as any).msal.loadExternalTokens(config, request, response, {});
},
[msalConfig, { scopes, authority: msalConfig.auth.authority }, serverResponse]
);
}
let serverResponse: Record<string, unknown>;
/**
* Reads test credentials from environment variables and throws if either
* value is missing.
*/
function getCredentials(): [string, string] {
const username = process.env.TEST_USERNAME;
const password = process.env.TEST_PASSWORD;
if (!username || !password) {
throw new Error(
"TEST_USERNAME and TEST_PASSWORD environment variables must be set before running tests."
);
}
return [username, password];
}
test.beforeAll(async () => {
const [username, password] = getCredentials();
serverResponse = await getServerTokenResponse(username, password);
});
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:3000/");
await loadTokensInBrowser(page, serverResponse);
await page.reload();
});
Puppeteer
Use page.evaluate in the same way as Playwright. The pattern is identical.
import puppeteer, { type Page } from "puppeteer";
const msalConfig = {
auth: {
clientId: "your-client-id",
authority: "https://login.microsoftonline.com/your-tenant-id",
},
cache: { cacheLocation: "sessionStorage" },
};
const scopes = ["User.Read"];
// Obtain serverResponse using the same ROPC helper shown in the Playwright
// section above, then:
async function loadTokensInBrowser(
page: Page,
serverResponse: Record<string, unknown>
): Promise<void> {
const config = msalConfig;
const request = { scopes, authority: msalConfig.auth.authority };
await page.evaluate(
async (config, request, response) => {
// Access loadExternalTokens via whatever mechanism your app exposes it
// (e.g. window.msal, a dedicated testbed helper, etc.).
// The fourth argument is LoadTokenOptions (empty object uses defaults)
await (window as any).msal.loadExternalTokens(config, request, response, {});
},
config,
request,
serverResponse
);
}
Cypress
In Cypress, use cy.window() to access the browser's window object and call loadExternalTokens directly. Token acquisition and cache hydration can be placed in a custom command or in a before/beforeEach hook.
// cypress/support/commands.ts
import type { SilentRequest, ExternalTokenResponse } from "@azure/msal-browser";
declare global {
namespace Cypress {
interface Chainable {
loadMsalTokens(
config: object,
request: SilentRequest,
response: ExternalTokenResponse
): Chainable<void>;
}
}
}
Cypress.Commands.add("loadMsalTokens", (config, request, response) => {
cy.window().then(async (win) => {
// Access loadExternalTokens via whatever mechanism your app exposes it
// (e.g. win.msal, a dedicated testbed helper, etc.).
// The fourth argument is LoadTokenOptions (empty object uses defaults)
await (win as any).msal.loadExternalTokens(config, request, response, {});
});
});
// cypress/e2e/app.cy.ts
const msalConfig = {
auth: {
clientId: "your-client-id",
authority: "https://login.microsoftonline.com/your-tenant-id",
},
cache: { cacheLocation: "sessionStorage" },
};
beforeEach(() => {
cy.visit("http://localhost:3000/");
// Obtain a server response (see ROPC helper in the Playwright section),
// or use pre-obtained / hardcoded test tokens.
// NOTE: The values below are illustrative placeholders only. Replace them
// with tokens obtained via the ROPC helper or another token acquisition
// method before running real tests.
const serverResponse: ExternalTokenResponse = {
token_type: "Bearer",
scope: "User.Read",
expires_in: 3600,
access_token: "test-access-token",
id_token: "test-id-token",
client_info: "test-client-info",
};
cy.loadMsalTokens(
msalConfig,
{ scopes: ["User.Read"], authority: msalConfig.auth.authority },
serverResponse
);
cy.reload();
});
Loading tokens
You can provide any combination of id, access and refresh tokens for caching but at a minimum the loadExternalTokens API requires one of the following sets of input parameters to identify token associations and cache appropriately:
- A
SilentRequestobject with account information, OR - A
SilentRequestobject with the authority AND aLoadTokenOptionsobject withclientInfo, OR - A
SilentRequestobject with the authority AND a server response object withclient_info - A
SilentRequestobject with the authority AND a server response object withid_token
The examples below show loading tokens individually, however, you may provide any 1, 2 or all 3 in a single request.
Loading id tokens
In addition to the parameters listed above provide the following to load an id token:
- A server response with the id_token field
An account will also be set in the cache based on the information provided above.
See the code examples below:
const config: Configuration = {
auth: { clientId: "your-client-id" },
};
const silentRequest: SilentRequest = {
account: {
homeAccountId: "your-home-account-id",
environment: "login.microsoftonline.com",
tenantId: "your-tenant-id",
username: "test@contoso.com",
localAccountId: "your-local-account-id",
},
};
const serverResponse: ExternalTokenResponse = {
id_token: "id-token-here",
};
const loadTokenOptions: LoadTokenOptions = {};
const pca = new PublicClientApplication(config);
await loadExternalTokens(
config,
silentRequest,
serverResponse,
loadTokenOptions
);
// OR
const config: Configuration = {
auth: { clientId: "your-client-id" },
};
const silentRequest: SilentRequest = {
scopes: [],
authority: "https://login.microsoftonline.com/your-tenant-id",
};
const serverResponse: ExternalTokenResponse = {
id_token: "id-token-here",
};
const loadTokenOptions: LoadTokenOptions = {
clientInfo: "client-info-here",
};
const pca = new PublicClientApplication(config);
await loadExternalTokens(
config,
silentRequest,
serverResponse,
loadTokenOptions
);
// OR
const config: Configuration = {
auth: { clientId: "your-client-id" },
};
const silentRequest: SilentRequest = {
scopes: [],
authority: "https://login.microsoftonline.com/your-tenant-id",
};
const serverResponse: ExternalTokenResponse = {
id_token: "id-token-here",
client_info: "client-info-here",
};
const loadTokenOptions: LoadTokenOptions = {};
const pca = new PublicClientApplication(config);
await loadExternalTokens(
config,
silentRequest,
serverResponse,
loadTokenOptions
);
Loading access tokens
In addition to the parameters listed above provide the following to load an access token:
- A server response with an
access_token,expires_in,token_type, andscope
See the code examples below:
const config: Configuration = {
auth: { clientId: "your-client-id" },
};
const silentRequest: SilentRequest = {
scopes: ["User.Read", "email"],
account: {
homeAccountId: "your-home-account-id",
environment: "login.microsoftonline.com",
tenantId: "your-tenant-id",
username: "test@contoso.com",
localAccountId: "your-local-account-id",
},
};
const serverResponse: ExternalTokenResponse = {
token_type: AuthenticationScheme.BEARER, // "Bearer"
scope: "User.Read email",
expires_in: 3599,
access_token: "access-token-here",
};
const loadTokenOptions: LoadTokenOptions = {
extendedExpiresOn: 6599,
};
const pca = new PublicClientApplication(config);
await loadExternalTokens(
config,
silentRequest,
serverResponse,
loadTokenOptions
);
Loading refresh tokens
In addition to the parameters listed above provide the following to load a refresh token:
- A server response with a
refresh_tokenand optionallyrefresh_token_expires_in
See the code examples below:
const config: Configuration = {
auth: { clientId: "your-client-id" },
};
const silentRequest: SilentRequest = {
scopes: [],
account: {
homeAccountId: "your-home-account-id",
environment: "login.microsoftonline.com",
tenantId: "your-tenant-id",
username: "test@contoso.com",
localAccountId: "your-local-account-id",
},
};
const serverResponse: ExternalTokenResponse = {
refresh_token: "refresh-token-here",
refresh_token_expires_in: "86399",
};
const loadTokenOptions: LoadTokenOptions = {};
const pca = new PublicClientApplication(config);
await loadExternalTokens(
config,
silentRequest,
serverResponse,
loadTokenOptions
);