AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.69k stars 2.65k forks source link

Extension/Manifest v3 Browser detection and use of window #3923

Open sickpuppysoftware opened 3 years ago

sickpuppysoftware commented 3 years ago

Core Library

MSAL.js v2 (@azure/msal-browser)

Wrapper Library

Not Applicable

Description

msal-browser doesn't work as it should when called from a manifest v3 service worker. I've adapted the extension sample in an attempt to get it working under manifest v3. However I have the following issues depending on where I try to use the library. Background service worker - The library's browser detection fails as there is no window object in a service worker. Content script - No access to chrome.identity should library calls fail. I'm also loathed to inject the whole library into every page loaded.

Is there ay way to get the library working with manifest v3?

Source

External (Customer)

jasonnutter commented 3 years ago

@sickpuppysoftware To clarify, this is what you are referring to? https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/

Unfortunately, this isn't something we have tested, but we'll add it to our backlog to investigate. We also welcome PRs if you have ideas for specific changes that can be made.

sickpuppysoftware commented 3 years ago

That is what I'm referring to.

bartlomiejzuber commented 2 years ago

@jasonnutter Is there a chance to support that in future ? Our team have exactly the same problem, we use msal for authentication in chrome browser extension which now seems to be impossible due to window not exist inside of service worker. :(

wolkowsky commented 2 years ago

@jasonnutter +1, it's a blocker for us. Will the problem be solved? Could we know ETA?

nichovski commented 2 years ago

Did anyone find a workaround for this issue?

bartlomiejzuber commented 2 years ago

Did anyone find a workaround for this issue?

Kind of yes although I didn't finish it, as a workaround you can use jsdom and assign it to the global.window, additionally you might need to add polyfills for things like Crypto API(https://developer.mozilla.org/en-US/docs/Web/API/Crypto). Keep in mind that this type of workaround will bloat your background(service worker) script bundle significantly.

Alino commented 2 years ago

hi @bartlomiejzuber , did you finish it? Is your solution working well? I wonder if there are not issues with the localStorage when it's kind of polyfilled from jsdom

Does it actually store the data and load it back when you reopen the browser or when the service worker 'resets'? Or are you using your own caching?

bartlomiejzuber commented 2 years ago

hi @bartlomiejzuber , did you finish it? Is your solution working well? I wonder if there are not issues with the localStorage when it's kind of polyfilled from jsdom

Does it actually store the data and load it back when you reopen the browser or when the service worker 'resets'? Or are you using your own caching?

Hi, yeah it worked. Not everything can be pollyfilled by jsdom though. When it comes to local storage you need to implement it using chrome's storage API.

Alino commented 2 years ago

Thanks, I have tried this, implementing the localStorage using chrome storage API but I think msal cannot access the value of localStorage.getItem() as it is async. And real localStorage is not async.

// a workaround for msal not being able to detect browser environment
const JSDOM = new jsdomModule.JSDOM;
globalThis.window = JSDOM.window;
delete globalThis.window.localStorage;
globalThis.document = globalThis.window.document;
globalThis.window.crypto = globalThis.crypto;

// a workaround for msal not supporting chrome.storage API as a cache option
globalThis.localStorage = {
    setItem: (keyName, keyValue) => {
        console.log(`setting chrome.storage keyName ${keyName} with keyValue ${keyValue}`);
        return chrome.storage.local.set({ [`localStorage-${keyName}`]: keyValue });
    },
    getItem: (keyName) => {
        console.log(`getting chrome.storage keyName ${keyName}`);
        return chrome.storage.local.get([`localStorage-${keyName}`]);
    },
    removeItem: (keyName) => {
        console.log(`removing chrome.storage keyName ${keyName}`);
        return chrome.storage.local.remove([`localStorage-${keyName}`]);
    },
    clear: () => {
        console.log(`clearing chrome.storage`);
        return chrome.storage.local.clear();
    }
}
globalThis.window.localStorage = {};
globalThis.window.localStorage = globalThis.localStorage;

I think it would require changes in msal code itself to be able to await async values.

Did you do it in a different way?

relation with https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/5023

bartlomiejzuber commented 2 years ago

@Alino Hi again, I don't have access to that code anymore but briefly I can explain that I've tried two approaches:

Alino commented 2 years ago

@bartlomiejzuber Hello again, thank you very much for your input. This idea also came across my mind for a while but I kind of ditched it as a dirty hack. But when rethinking it, it is probably the most simple workaround and there isn't anything better without having to refactor the whole MSAL library to use async cache...

I have actually created a sample repository for this. https://github.com/Alino/MSAL_ChromiumExtensionSampleManifestV3

I am polyfilling the window.crypto object with msrCrypto which is also a lib from Microsoft. Did you use also this library or anything else? I have an issue with this one. When trying to signIn, it throws an error with Detail: algorithm If you are aware of some polyfill that works that would be great, thanks :) I have tried few others but none of them worked and this one from MS looks best, yet it still does not work.

image

EDIT: I have asked the question about incompatibility with msrCrypto here -> https://github.com/AzureAD/microsoft-authentication-library-for-js/discussions/5079

Alino commented 2 years ago

I have created a sample repo with OIDC-client-ts lib instead of MSAL https://github.com/Alino/OIDC-client-ts-chromium-sample

It actually works, I can sign in and sign out...

Mkishore7 commented 1 year ago

Hi, is there any known workaround for this issue? Considering chrome has stopped accepting manifest v2 extensions on the chrome store, this will be creating a problem for my team to publish our extension.

chaitanyakale commented 1 year ago

This is a problem when using Blazor Webassembly with Azure AD B2C for Edge and Chrome extensions. Is there any plan to address this with .NET 8 release?

EmLauber commented 1 year ago

The MSAL.js team is tracking on our backlog adding manifest v3 support, but I don't have a timeline to share. We are also monitoring Chrome's review of their support timeline for manifest v2.

@chaitanyakale I don't have insight into the .NET 8 release, but Blazor Webassembly wraps MSAL.js for Azure AD B2C so it will not have support until MSAL.js does.

chaitanyakale commented 1 year ago

Is there any way to publish a new Edge/Chrome extension with manifest v2 that uses MSAL.js?

bondib commented 8 months ago

Any update on this ?

mprystupa commented 4 months ago

Okay, for anyone interested, we've managed to make it (sort of) work with Manifest V3 service worker via use of the offscreen API.

The idea is that you do the login and MSAL initialization through popup and if you need to call the API with access token from MSAL, you send the message to offscreen script running in the background, which can access cached token and refresh it if necessary. So, assuming you have login code already working on the popup side of things, here's how to get token in background service worker via offscreen script.

  1. Add new permission to your manifest:

    "permissions": [
    ...,
    "offscreen"
    ]
  2. Create a new HTML page that is just loading the script. In our case we are using TypeScript, so we loaded the .ts file as a module:

    <!DOCTYPE html>
    <script src="offscreen.ts" type="module"></script>
  3. Create a new script file that will contain one function, responsible for getting the access token from MSAL auth provider:

    const getAccessToken = async () => {
    const authProvider = [get your auth provider however you do in your project];
    const authResponse = await authProvider?.acquireToken();
    
    return authResponse?.accessToken;
    };
  4. Since offscreen can access only chrome.runtime API, we can subscribe to message from background script, and on said message we will fetch the token and pass it back to background service worker. So, below getAccessToken in your offscreen script add following code:

    chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
    const getToken = async () => {
        const token = await getAccessToken();
        sendResponse(token);
    };
    
    if (message === 'getAccessTokenViaOffscreen') {
        getToken();
    
        // This indicates that the response will be sent asynchronously
        return true;
    }
    });
  5. In your background service worker, you'll have to spin the offscreen script first, then trigger the token acquisition via chrome.runtime.sendMessage method and at the end, you might want to close the offscreen:

    
    const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';

// A global promise to avoid concurrency issues let creatingOffscreenDocument;

// Chrome only allows for a single offscreenDocument. This is a helper function // that returns a boolean indicating if a document is already active. async function hasDocument() { // Check all windows controlled by the service worker to see if one // of them is the offscreen document with the given path const matchedClients = await clients.matchAll(); return matchedClients.some( (c) => c.url === chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH) ); }

async function setupOffscreenDocument(path) { // If we do not have a document, we are already setup and can skip if (!(await hasDocument())) { // create offscreen document if (creating) { await creating; } else { creating = chrome.offscreen.createDocument({ url: path, reasons: [ chrome.offscreen.Reason.DOM_SCRAPING ], justification: 'authentication' }); await creating; creating = null; } } }

async function closeOffscreenDocument() { if (!(await hasDocument())) { return; } await chrome.offscreen.closeDocument(); }

async function getAccessToken() { await setupOffscreenDocument(OFFSCREEN_DOCUMENT_PATH); const token = await chrome.runtime.sendMessage('getAccessTokenViaOffscreen'); await closeOffscreenDocument();

return token; }



This solution was based on how Firebase handles this in Manifest V3 extensions, outlined [here](https://firebase.google.com/docs/auth/web/chrome-extension), but since we were using MSAL extensively already, we've just adjusted this method to work with MSAL.

Hopefully that will help some of you!