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

[Question] Azure AD login for a Web App #18390

Closed
bheemreddy181 opened this issue Oct 28, 2022 · 12 comments
Closed

[Question] Azure AD login for a Web App #18390

bheemreddy181 opened this issue Oct 28, 2022 · 12 comments

Comments

@bheemreddy181
Copy link

Is there a way we can simulate a similar functionality in playwright https://medium.com/version-1/using-cypress-to-test-azure-active-directory-protected-spas-47d04f5add9 ?

@mxschmitt
Copy link
Member

A similar thing will work for Playwright, if you use page.request.post instead of cy.request it should work.

@bheemreddy181
Copy link
Author

bheemreddy181 commented Oct 28, 2022

@mxschmitt Can you elaborate more on this ? Do you mean to do a page.request.post on before test ? don't we need to store the decoded token in the local storage similar to how they did in the above post ?

@bheemreddy181
Copy link
Author

bheemreddy181 commented Oct 28, 2022

is this what you meant to say ?

import { test, expect } from '@playwright/test';
import authSettings from "./authsettings.json";

test.beforeEach(async ({ page }, testInfo) => {
  console.log(`Running ${testInfo.title}`);
  const {
    clientId,
    clientSecret,
    apiScopes,
    username,
    password,
  } = authSettings;
  await page.request.post('https://login.microsoftonline.com//<tenant_id>/oauth2/v2.0/token', {
    form: {
      'grant_type': 'password',
      'client_id': clientId,
      'scope': ["openid profile offline_access"].concat(apiScopes).join(" "),
      'client_secret': clientSecret,
      "username": username,
      "password": password
    },
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    ignoreHTTPSErrors: true,
    timeout: 0,
  })
});

test.describe('when you ARE logged in', () => {
  test("Home page should have the correct title", async ({page}) => {
    await page.goto("<url>");
    expect(await page.title()).toBe("All posts | Building SPAs");
  })
});

@mxschmitt
Copy link
Member

mxschmitt commented Oct 31, 2022

Yes, you need to rewrite injectTokens to Playwright. So you can do something like:

await page.evaluate(yourToken => {
  window.localStorage.setItem("key", yourToken);
}, yourToken)

See here for evaluating JavaScript: https://playwright.dev/docs/evaluating

and you can do the "heavy request" in globalSetup so its done a single time and set stuff always in beforeEach, or use storage states: https://playwright.dev/docs/auth

@bheemreddy181
Copy link
Author

How does storage states work with localstorage ?

@bheemreddy181
Copy link
Author

As most of these keys which we need to set on the localstorage are dynamic like here

    const accountKey = `${homeAccountId}-${environment}-${realm}`;

if i do something like you mentioned above

   await page.evaluate(accessTokenKey => {
      window.localStorage.setItem(accountKey, JSON.stringify(accessTokenKey));
    }, accessTokenKey)

i see below error
Screen Shot 2022-10-30 at 9 08 49 PM

@mxschmitt
Copy link
Member

See in https://playwright.dev/docs/evaluating that you need to pass the variables over as an argument which you want to use inside the callback (which runs inside the browser).

Local Storage gets serialized as well with storage states. I recommend reading through https://playwright.dev/docs/evaluating and https://playwright.dev/docs/auth

@bheemreddy181
Copy link
Author

Quick Question as i am very new to playwright

 let localStorage = await page.evaluate(() => window.localStorage);
    writeFileSync('storageState.json', JSON.stringify(localStorage))
    // Save signed-in state to 'storageState.json'.
    await page.context().storageState({ path: 'storageState.json' });
    await page.request.dispose();

and in the test

import { test, expect } from '@playwright/test';
import globalSetup from "./global-setup";

test.beforeEach(async ({ page }, testInfo) => {
  console.log(`Running ${testInfo.title}`);
  await page.goto("url");
  await globalSetup(page);
  await page.reload
});

test.describe('when you ARE logged in', () => {
  test.use({storageState: 'storageState.json'});
  test("Home page should have the correct title", async ({page}) => {
    await page.pause();
    expect(await page.title()).toBe("All posts | Building SPAs");
  })
});

does the above makes sense ?

@bheemreddy181
Copy link
Author

@mxschmitt i am pretty confused with the approach here this is what i did as for now i feel this only limited to beforeeach setup , give we need to use the same page object for running tests - i don't think the global setup would work here

here is what i did but that doesn't seem to be working

    await page.evaluate(
      ({ accountKey, accountEntity }) => window.localStorage.setItem(accountKey, JSON.stringify(accountEntity)),
      { accountKey, accountEntity });
    await page.evaluate(
      ({ idTokenKey, idTokenEntity }) => window.localStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity)),
      { idTokenKey, idTokenEntity });
    await page.evaluate(
      ({ accessTokenKey, accessTokenEntity }) => window.localStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity)),
      { accessTokenKey, accessTokenEntity });
    await page.evaluate(
      ({ refreshTokenKey, refreshTokenEntity }) => window.localStorage.setItem(refreshTokenKey, JSON.stringify(refreshTokenEntity)),
      { refreshTokenKey, refreshTokenEntity });

  
    await page.goto(url, { waitUntil: 'load'})

if i just load the token to the local storage and run page.goto that doesn't seem to be working for me , do we need to store the localstorage to storage state ?

@bheemreddy181
Copy link
Author

I made some progress setting up the global setup but this is not working for me as expected , can you correct me if any of my setups are wrong here

import { chromium } from '@playwright/test';
import { decode } from "jsonwebtoken";
import authSettings from "./authsettings.json";
import { writeFileSync } from "fs";

async function globalSetup() {
  const {
    clientId,
    clientSecret,
    apiScopes,
    loginUsername,
    password,
  } = authSettings;

  const url = <HostUrl>;
  const browser = await chromium.launch({channel: 'chrome'});
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'domcontentloaded'})
  await page.waitForLoadState('load');

  const tokenResponse = await page.request.post('https://login.microsoftonline.com//<tenant>/oauth2/v2.0/token', {
    form: {
      'grant_type': 'password',
      'client_id': clientId,
      'scope': ["openid profile offline_access"].concat(apiScopes).join(" "),
      'client_secret': clientSecret,
      "username": loginUsername,
      "password": password
    },
    ignoreHTTPSErrors: true,
    timeout: 0,
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded',
    },
  }).then((response) => response.json())
  console.log(tokenResponse, " : response data ")

    const environment = "login.windows.net";
    const idToken = decode(tokenResponse.id_token);
    const localAccountId = idToken.oid || idToken.sid;
    const realm = idToken.tid;
    const homeAccountId = `${localAccountId}.${realm}`;
    const username = idToken.preferred_username;
    const name = idToken.name;
    const idTokenPayload = idToken;

    const buildAccountEntity = (
      homeAccountId,
      realm,
      localAccountId,
      username,
      name,
      idTokenPayload
    ) => {
      return {
        authorityType: "MSSTS",
        // This value does not seem to get used, so we can leave it out.
        clientInfo: "",
        homeAccountId: homeAccountId,
        environment: environment,
        realm: realm,
        localAccountId: localAccountId,
        username: username,
        name: name,
        idTokenClaims: idTokenPayload
      };
    };

    const buildIdTokenEntity = (
      homeAccountId,
      idToken,
      realm
    ) => {
      return {
        credentialType: "IdToken",
        homeAccountId: homeAccountId,
        environment: environment,
        clientId: clientId,
        secret: idToken,
        realm: realm,
      };
    };

    const buildAccessTokenEntity = (
      homeAccountId,
      accessToken,
      expiresIn,
      extExpiresIn,
      realm,
      scopes
    ) => {
      const now = Math.floor(Date.now() / 1000);
      return {
        homeAccountId: homeAccountId,
        credentialType: "AccessToken",
        secret: accessToken,
        cachedAt: now.toString(),
        expiresOn: (now + expiresIn).toString(),
        extendedExpiresOn: (now + extExpiresIn).toString(),
        environment: environment,
        clientId: clientId,
        realm: realm,
        target: scopes.map((s) => s.toLowerCase()).join(" "),
        // Scopes _must_ be lowercase or the token won't be found
      };
    };

    const buildRefreshTokenEntity = (
      clientId,
      environment,
      homeAccountId,
      refreshToken
    ) => {
      return {
        clientId: clientId,
        credentialType: "RefreshToken",
        environment: environment,
        homeAccountId: homeAccountId,
        secret: refreshToken
      };
    }

    const accountKey = `${homeAccountId}-${environment}-${realm}`;
    const accountEntity = buildAccountEntity(
      homeAccountId,
      realm,
      localAccountId,
      username,
      name,
      idTokenPayload
    );

    const idTokenKey = `${homeAccountId}-${environment}-idtoken-${clientId}-${realm}-`;
    const idTokenEntity = buildIdTokenEntity(
      homeAccountId,
      tokenResponse.id_token,
      realm
    );

    const accessTokenKey = `${homeAccountId}-${environment}-accesstoken-${clientId}-${realm}-${apiScopes.join(
      " "
    )}`;
    const accessTokenEntity = buildAccessTokenEntity(
      homeAccountId,
      tokenResponse.access_token,
      tokenResponse.expires_in,
      tokenResponse.ext_expires_in,
      realm,
      apiScopes
    );

    const refreshTokenKey = `${homeAccountId}-${environment}-refreshtoken-${clientId}--`;
    const refreshTokenEntity = buildRefreshTokenEntity(
      clientId,
      environment,
      homeAccountId,
      tokenResponse.refresh_token
    );

    await page.evaluate(
      ({ accountKey, accountEntity }) => window.localStorage.setItem(accountKey, JSON.stringify(accountEntity)),
      { accountKey, accountEntity });
    await page.evaluate(
      ({ idTokenKey, idTokenEntity }) => window.localStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity)),
      { idTokenKey, idTokenEntity });
    await page.evaluate(
      ({ accessTokenKey, accessTokenEntity }) => window.localStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity)),
      { accessTokenKey, accessTokenEntity });
    await page.evaluate(
      ({ refreshTokenKey, refreshTokenEntity }) => window.localStorage.setItem(refreshTokenKey, JSON.stringify(refreshTokenEntity)),
      { refreshTokenKey, refreshTokenEntity });

    // let localStorage = await page.evaluate(() => window.localStorage);
    // writeFileSync('storageState.json', JSON.stringify(localStorage))
    // Save signed-in state to 'storageState.json'.*/
    await page.context().storageState({ path: 'storageState.json' });
  }

export default globalSetup;

Every time this setup runs, I keep getting this error

browserContext.storageState: net::ERR_ABORTED; maybe frame was detached?

   at endtoend/global-setup.ts:177

  175 |     // writeFileSync('storageState.json', JSON.stringify(localStorage))
  176 |     // Save signed-in state to 'storageState.json'.*/
> 177 |     await page.context().storageState({ path: 'storageState.json' });
      |                          ^
  178 |   }
  179 |
  180 | export default globalSetup;

@bheemreddy181
Copy link
Author

i either get the above error or Execution context was destroyed, most likely because of a navigation

@aslushnikov
Copy link
Collaborator

@bheemreddy181 I'll have to close this since this becomes more of a general programming discussion rather then a playwright-specific discussion.

Please ask this question on other forums like StackOverflow, or possible on https://aka.ms/playwright/slack!

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

No branches or pull requests

3 participants