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

[Feature] Export credential #4269

Closed
nicoandmee opened this issue Oct 28, 2020 · 3 comments · Fixed by #4412
Closed

[Feature] Export credential #4269

nicoandmee opened this issue Oct 28, 2020 · 3 comments · Fixed by #4412

Comments

@nicoandmee
Copy link

Original Issue on puppeteer

  • Currently, there is no reliable way to clone a browser (all session data/cookies/user data dir structure) without persisting a user data directory on disk. This makes running tests that require persistent authentication (like MFA prompts) somewhat tricky.

  • Proposing adding functions to API to serve this purpose: exportCredential(), importCredential(str). Export simply returns a base64-encoded string. You then provide this string when instantiating the browser, which unpacks the session data and sets the cookies. Sort of like freezing the browser in time, and then reactivating it.

  • I am using node-tar to gzip the userDataDir, the implementation looks something like this:

const stream = await tar.x({
      cwd: browser.userDataDir,
      gzip: true,
    });
    stream.end(Buffer.isBuffer(browser.parsedCredential.chromeUserData) ? browser.parsedCredential.chromeUserData : Buffer.from(browser.parsedCredential.chromeUserData, 'base64'));

Import function looks like:

async importCredential(zbuf) {
    await new Promise((resolve, reject) => {
      zlib.gunzip(Buffer.from(zbuf, 'base64'), (err, buf) => {
        if (err) return reject(err);
        browser.parsedCredential = JSON.parse(buf.toString());
        return resolve();
      });
    });

    await this.loadCredentials();
  }

parsedCredential is composed of:

  • all cookies (including third party): from ._client.send('Network.getAllCookies')
  • cookies: from page.cookies()
  • chromeUserData: from:

async function streamToBuffer(stream) {
  return new Promise((resolve, reject) => {
    const buffers = [];
    stream.on('error', reject);
    stream.on('data', data => buffers.push(data));
    stream.on('end', () => resolve(Buffer.concat(buffers)));
  });
}

 const buf = await streamToBuffer(await tar.c({
        gzip: true,
        cwd: browser.userDataDir,
        filter(path) {
          if (/BrowserMetrics-active\.pma$/i.test(path)) return false;
          if (/\/Favicons(-journal)?$/.test(path)) return false;
          if (/\/History(-journal)?$/.test(path)) return false;
          if (/\/Top Sites(-journal)?$/.test(path)) return false;
          if (/\/Last Session$/.test(path)) return false;
          if (/\/Last Tabs$/.test(path)) return false;
          if (/\/Visited Links$/.test(path)) return false;
          if (/\/ShaderCache\//.test(path)) return false;
          if (/\/ShaderCache$/.test(path)) return false;
          if (/\/GPUCache\//.test(path)) return false;
          if (/\/GPUCache$/.test(path)) return false;
          return true;
        },
      }, ['.']));

Is this something that would be appropriate to add to the project itself, or is it better in a utility module? I think this is a very common use-case.

@pavelfeldman
Copy link
Member

What do you think is needed on top of the cookies and storage? A user-land code for those can be found below.

const cookies = await (await page.context().cookies()).filter(c => c.value !== '');
const storage = await page.evaluate(() => ({ sessionStorage, localStorage }));
await context.addCookies(loggedInState.cookies);
  await context.addInitScript(loggedInState => {
    if (new URL(location.href).origin.endsWith('microsoft.com')) {
      for (const name of Object.keys(loggedInState.session))
        sessionStorage[name] = loggedInState.sessionStorage[name];
      for (const name of Object.keys(loggedInState.local))
        localStorage[name] = loggedInState.localStorage[name];
    }
  }, loggedInState);

@nicoandmee
Copy link
Author

nicoandmee commented Nov 9, 2020

I think there is other state information in the user-data-dir that some sites use to validate a session. I will attempt to produce an example. I also wonder:

  • does the context.cookies() return ALL (including third party) cookie? I could not find a clear answer in the docs. I know for the puppeteer, this is not the case.
  • Would the code sample you shared work for setting local/session storage for all origins (not just ending with microsoft.com)?

@nicoandmee
Copy link
Author

@pavelfeldman that was fast! #4412 💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants