Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/msal-browser/src/error/BrowserAuthErrorCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const hashDoesNotContainKnownProperties =
"hash_does_not_contain_known_properties";
export const unableToParseState = "unable_to_parse_state";
export const stateInteractionTypeMismatch = "state_interaction_type_mismatch";
export const noBroadcastChannelNameInState =
"no_broadcast_channel_name_in_state";
export const interactionInProgress = "interaction_in_progress";
export const popupWindowError = "popup_window_error";
export const emptyWindowError = "empty_window_error";
Expand Down
34 changes: 29 additions & 5 deletions lib/msal-browser/src/interaction_client/PopupClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvid
import { generateEarKey } from "../crypto/BrowserCrypto.js";
import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js";
import { validateRequestMethod } from "../request/RequestHelpers.js";
import { extractBrowserRequestState } from "../utils/BrowserProtocolUtils.js";

export type PopupParams = {
popup?: Window | null;
Expand Down Expand Up @@ -335,7 +336,8 @@ export class PopupClient extends StandardInteractionClient {
// Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds.
const responseString = await this.monitorPopupForHash(
popupWindow,
popupParams.popupWindowParent
popupParams.popupWindowParent,
request.state
);

const serverParams = invoke(
Expand Down Expand Up @@ -437,7 +439,7 @@ export class PopupClient extends StandardInteractionClient {
this.logger,
this.performanceClient,
correlationId
)(popupWindow, popupParams.popupWindowParent);
)(popupWindow, popupParams.popupWindowParent, request.state);

const serverParams = invoke(
ResponseHandler.deserializeResponse,
Expand Down Expand Up @@ -514,7 +516,7 @@ export class PopupClient extends StandardInteractionClient {
this.logger,
this.performanceClient,
correlationId
)(popupWindow, popupParams.popupWindowParent);
)(popupWindow, popupParams.popupWindowParent, request.state);

const serverParams = invoke(
ResponseHandler.deserializeResponse,
Expand Down Expand Up @@ -653,7 +655,8 @@ export class PopupClient extends StandardInteractionClient {

await this.monitorPopupForHash(
popupWindow,
popupParams.popupWindowParent
popupParams.popupWindowParent,
validRequest.state || ""
).catch(() => {
// Swallow any errors related to monitoring the window. Server logout is best effort
});
Expand Down Expand Up @@ -735,8 +738,29 @@ export class PopupClient extends StandardInteractionClient {
*/
monitorPopupForHash(
popupWindow: Window,
popupWindowParent: Window
popupWindowParent: Window,
state: string
): Promise<string> {
const parsedState = extractBrowserRequestState(
this.browserCrypto,
state
);
if (!parsedState) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToParseState
);
}
if (!parsedState.broadcastChannelName) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.noBroadcastChannelNameInState
);
}
const authBroadcastChannel = new BroadcastChannel(
parsedState.broadcastChannelName
);

authBroadcastChannel.addEventListener("message");

return new Promise<string>((resolve, reject) => {
this.logger.verbose(
"PopupHandler.monitorPopupForHash - polling started"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ export abstract class StandardInteractionClient extends BaseInteractionClient {
const redirectUri = this.getRedirectUri(request.redirectUri);
const browserState: BrowserStateObject = {
interactionType: interactionType,
broadcastChannelName:
interactionType === InteractionType.Popup
? "msal.broadcast." + this.browserCrypto.createNewGuid()
: "",
};
const state = ProtocolUtils.setRequestState(
this.browserCrypto,
Expand Down
1 change: 1 addition & 0 deletions lib/msal-browser/src/utils/BrowserProtocolUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {

export type BrowserStateObject = {
interactionType: InteractionType;
broadcastChannelName: string;
};

/**
Expand Down
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Config object to be passed to Msal on creation
const msalConfig = {
auth: {
clientId: "b5c2e510-4a17-4feb-b219-e55aa5b74144",
clientId: "8015f5e0-0370-427c-9b0d-d834189ffdd0",
authority:
"https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47",
},
Expand Down
60 changes: 60 additions & 0 deletions samples/msal-browser-samples/popup-coop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# MSAL Browser - Popup with Cross-Origin-Opener-Policy Sample

This sample demonstrates how to use MSAL Browser with popup authentication while implementing the `Cross-Origin-Opener-Policy` (COOP) header to enhance security. The COOP header helps protect against cross-origin attacks by isolating the browsing context.

## Setup

### Step 1: Clone or download this repository

From your shell or command line:

```bash
git clone https://github.com/AzureAD/microsoft-authentication-library-for-js.git
cd microsoft-authentication-library-for-js/samples/msal-browser-samples/popup-coop
```

### Step 2: Install dependencies

```bash
npm install
```

### Step 3: Register the sample application in Azure portal

1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure AD** service.
2. Select the **App Registrations** blade on the left, then select **New registration**.
3. In the **Register an application page** that appears, enter your application's registration information:
- In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-browser-popup-coop`.
- Under **Supported account types**, select **Accounts in this organizational directory only**.
- In the **Redirect URI (optional)** section, select **Single-page application** in the combo-box and enter the following redirect URI: `http://localhost:30662/redirect`.
4. Select **Register** to create the application.
5. In the app's registration screen, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code.

### Step 4: Configure the sample

1. Open the `app/authConfig.js` file.
2. Find the key `clientId` and replace the existing value with the application ID (clientId) of the application copied from the Azure portal.
3. Find the key `authority` and replace the existing value with your tenant ID if you want to sign in users from your specific tenant only.

## Running the sample

1. Start the web server:

```bash
npm start
```

2. Open your browser and navigate to `http://localhost:30662`.

3. **Testing different COOP headers**: You can experiment with different Cross-Origin-Opener-Policy values by modifying the header in `server.js`:

```javascript
res.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
```

**To verify the COOP header is set correctly:**

- Open Developer Tools (F12)
- Go to the **Application** tab
- Scroll down to the **top** section
- Look for `Cross-Origin-Opener-Policy` to confirm the header value
124 changes: 124 additions & 0 deletions samples/msal-browser-samples/popup-coop/app/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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);

myMSALObj.initialize().then(() => {
// 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;
myMSALObj.setActiveAccount(resp.account);
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) {
const activeAccount = currentAccounts[0];
myMSALObj.setActiveAccount(activeAccount);
accountId = activeAccount.homeAccountId;
showWelcomeMessage(activeAccount);
}
}
}

async function signIn(method) {
signInType = isIE ? "redirect" : method;
if (signInType === "popup") {
return myMSALObj.loginPopup({
...loginRequest,
// redirectUri: "http://localhost:30662"
}).then(handleResponse).catch(function (error) {
console.log(error);
});
} else if (signInType === "redirect") {
return myMSALObj.loginRedirect(loginRequest)
}
}

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

if (interactionType === "popup") {
myMSALObj.logoutPopup(logoutRequest).then(() => {
window.location.reload();
});
} else {
myMSALObj.logoutRedirect(logoutRequest);
}
}

async function getTokenPopup(request, account) {
request.redirectUri = "http://localhost:30662"
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) {
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);
}
});
}

function openPopupLmso(){
// const popupWindow = window.open("http://localhost:30662", "popup", "width=600,height=600");
const popupWindow = window.open("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", "popup", "width=600,height=600");
monitorLmsoPopup(popupWindow);
}

function monitorLmsoPopup(popupWindow){
console.log("PopupHandler.monitorPopupLmso - monitoring popup");
const intervalId = setInterval(() => {
// Window is closed
if (popupWindow.closed) {
console.log(
"PopupHandler.monitorPopupLmso - window closed"
);
clearInterval(intervalId);
}
}, 100000);
popupWindow.location = "http://localhost:30662";
popupWindow.close();
};
Loading
Loading