cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.87k stars 3.17k forks source link

Login AD Azure #17433

Closed dnlopesoftware closed 1 year ago

dnlopesoftware commented 3 years ago

Current behavior

I'm having a problem using azure AD authentication. I'm using the same model provided by Joonas Westlin(https://github.com/juunas11/AzureAdUiTestAutomation); All keys and locastorages are being generated as expected by my application, but after cy.reload I am redirected to the screen "https://login.microsoftonline.com/" to inform the username and password.

Does cypress already have another solution for interacting with AD Azure?

image

image

Desired behavior

No response

Test code to reproduce

auth.js: ///

import { decode } from "jsonwebtoken"; import { jwt } from 'express-jwt'; import authSettings from "../fixtures/hlg/authSettings.json";

const { authority, clientId, clientSecret, apiScopes, username, password, } = authSettings; const environment = "login.windows.net";

const buildAccountEntity = ( homeAccountId, realm, localAccountId, username, name ) => { return { authorityType: "MSSTS", // This could be filled in but it involves a bit of custom base64 encoding // and would make this sample more complicated. // This value does not seem to get used, so we can leave it out. clientInfo: "", homeAccountId, environment, realm, localAccountId, username, name, }; };

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

const buildAccessTokenEntity = ( homeAccountId, accessToken, expiresIn, extExpiresIn, realm, scopes, tokentype ) => { const now = Math.floor(Date.now() / 1000); return { homeAccountId, credentialType: "AccessToken", secret: accessToken, cachedAt: now.toString(), expiresOn: (now + expiresIn).toString(), extendedExpiresOn: (now + extExpiresIn).toString(), environment, clientId, realm, target: "user.read.all, user.read, openid, profile, email", // Scopes must be lowercase or the token won't be found tokentype }; };

const buildAuthTokenEntity = ( aio, aud, email, exp, iat, iss, name, nbf, nonce, localAccountId, onpremisessamaccountname, username, rh, sub, realm, uti, ver ) =>{ return { aud: aud, aio: aio, email: email, exp: exp, iat: iat, iss: iss, name: name, nbf: nbf, nonce: nonce, oid: localAccountId, onpremisessamaccountname: onpremisessamaccountname, preferred_username: username, rh: rh, sub: sub, tid: realm, uti: uti, ver: ver } }

const buildpermProve = (access_token2, token_type2, expires_in2, scope2, grupos2, cargo2, jti2) =>{ return { access_token: access_token2, token_type: token_type2, expires_in: expires_in2, scope: scope2, grupos: grupos2, nivel: Object.keys(grupos2), cargo: cargo2, jti: jti2 } }

const injectTokens = (tokenResponse) => { const idToken = decode(tokenResponse.access_token); console.log(idToken) const localAccountId = idToken.oid || idToken.sid; const realm = idToken.tid; const homeAccountId = ${localAccountId}.${realm}; const username = idToken.preferred_username; const name = idToken.name; const tokentype = tokenResponse.token_type const aio = idToken.aio const aud = idToken.aud const email = idToken.upn const exp = idToken.exp const iat = idToken.iat const iss = idToken.iss const nbf = idToken.nbf const crypto = require('crypto');

function generateNonce() { return crypto.randomBytes(16).toString("hex"); } const nonce = generateNonce() const onpremisessamaccountname = "7700608017" const rh = idToken.rh const sub = idToken.sub const uti = idToken.uti const ver = idToken.ver

const proveAuthenticationTokenEntity = buildAuthTokenEntity( aio, aud, email, exp, iat, iss, name, nbf, nonce, localAccountId, onpremisessamaccountname, username, rh, sub, realm, uti, ver );

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

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

const accessTokenKey = ${homeAccountId}-${environment}-accesstoken-${clientId}-${realm}-${apiScopes};

const accessTokenEntity = buildAccessTokenEntity( homeAccountId, tokenResponse.access_token, tokenResponse.expires_in, tokenResponse.ext_expires_in, realm, apiScopes, tokentype );

localStorage.setItem(accountKey, JSON.stringify(accountEntity)); localStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity)); localStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity)); localStorage.setItem("proveAuthenticationToken", JSON.stringify(proveAuthenticationTokenEntity)); };

const injectTokensProve = (tokenResponse) => { const access_token2 = tokenResponse.access_token; const token_type2 = tokenResponse.token_type; const expires_in2 = tokenResponse.expires_in; const scope2 = tokenResponse.scope; const grupos2 = tokenResponse.grupos; const cargo2 = tokenResponse.cargo; const jti2 = tokenResponse.jti;

const provePermissaoToken = buildpermProve( access_token2, token_type2, expires_in2, scope2, grupos2, cargo2, jti2);

localStorage.setItem("provePermissaoToken",JSON.stringify(provePermissaoToken));

}

export const login = (cachedTokenResponse) => { let tokenResponse1 = null; let tokenResponse2 = null; let chainable = cy.visit("/"); //let chainableProve = cy.visit("/");

if (!cachedTokenResponse) { chainable = chainable.request({ url: authority + "/oauth2/v2.0/token", method: "POST", body: { grant_type: "password", client_id: clientId, client_secret: clientSecret, scope: apiScopes, username: username, password: password, }, form: true, }); } else { chainable = chainable.then(() => { return { body: cachedTokenResponse, }; }); }

chainable.then((response) => { console.log(response.body) injectTokens(response.body); tokenResponse1 = response.body; }).then(() => {
let chainable = null; });

if(chainable){ chainable = chainable.request({ url: "https://xxxx/oauth/token", method: "POST", auth: { username: "", password: "" }, form: true, body: { grant_type: "password", scope: "read+write", username: "", password: "" } }) }

chainable.then((response2) => { console.log(response2.body) injectTokensProve(response2.body); tokenResponse2 = response2.body; }).then(() => {
return tokenResponse1; });

return chainable; };

Commands.js: import { login } from "./auth";

let cachedTokenExpiryTime = new Date().getTime(); let cachedTokenResponse = null;

Cypress.Commands.add( 'loginProve', (enderecoAcesso) => { // Clear our cache if tokens are expired if (cachedTokenExpiryTime <= new Date().getTime()) { cachedTokenResponse = null; }

login(cachedTokenResponse).then((tokenResponse) => {
  cachedTokenResponse = tokenResponse;
  // Set expiry time to 50 minutes from now
  cachedTokenExpiryTime = new Date().getTime() + (50 * 60 * 1000);
});

cy.reload()

} )

Cypress Version

7.6.0

Other

No response

DVGY commented 3 years ago

@dnlopesoftware Does your code have silent auth enabled or checkAuth enabled ?

dnlopesoftware commented 3 years ago

@DVGY There is not. I'm doing the token generation, using jwt decode and inserting it by locaStorage.

DVGY commented 3 years ago

@dnlopesoftware I need to see the msal code + auth code. Also make sure that you have access to the APP or API in azure ad

dnlopesoftware commented 3 years ago

I have access. The code is attachment in section. But follow the keys generated with post send with success; image image

DVGY commented 3 years ago

@dnlopesoftware what does the msal code look like in you js files or jsx files?

1.check wether the keys stays in local storage.

  1. Also what is need of cy.reload. i think oncw you have the proper keys msal redirects to base url automatically.
dnlopesoftware commented 3 years ago

Hi @DVGY , I'm using the file auth.js auth.js: ///

import { decode } from "jsonwebtoken"; import { jwt } from 'express-jwt'; import authSettings from "../fixtures/hlg/authSettings.json";

const { authority, clientId, clientSecret, apiScopes, username, password, } = authSettings; const environment = "login.windows.net";

const buildAccountEntity = ( homeAccountId, realm, localAccountId, username, name ) => { return { authorityType: "MSSTS", // This could be filled in but it involves a bit of custom base64 encoding // and would make this sample more complicated. // This value does not seem to get used, so we can leave it out. clientInfo: "", homeAccountId, environment, realm, localAccountId, username, name, }; };

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

const buildAccessTokenEntity = ( homeAccountId, accessToken, expiresIn, extExpiresIn, realm, scopes, tokentype ) => { const now = Math.floor(Date.now() / 1000); return { homeAccountId, credentialType: "AccessToken", secret: accessToken, cachedAt: now.toString(), expiresOn: (now + expiresIn).toString(), extendedExpiresOn: (now + extExpiresIn).toString(), environment, clientId, realm, target: "user.read.all, user.read, openid, profile, email", // Scopes must be lowercase or the token won't be found tokentype }; };

const buildAuthTokenEntity = ( aio, aud, email, exp, iat, iss, name, nbf, nonce, localAccountId, onpremisessamaccountname, username, rh, sub, realm, uti, ver ) =>{ return { aud: aud, aio: aio, email: email, exp: exp, iat: iat, iss: iss, name: name, nbf: nbf, nonce: nonce, oid: localAccountId, onpremisessamaccountname: onpremisessamaccountname, preferred_username: username, rh: rh, sub: sub, tid: realm, uti: uti, ver: ver } }

const buildpermProve = (access_token2, token_type2, expires_in2, scope2, grupos2, cargo2, jti2) =>{ return { access_token: access_token2, token_type: token_type2, expires_in: expires_in2, scope: scope2, grupos: grupos2, nivel: Object.keys(grupos2), cargo: cargo2, jti: jti2 } }

const injectTokens = (tokenResponse) => { const idToken = decode(tokenResponse.access_token); console.log(idToken) const localAccountId = idToken.oid || idToken.sid; const realm = idToken.tid; const homeAccountId = ${localAccountId}.${realm}; const username = idToken.preferred_username; const name = idToken.name; const tokentype = tokenResponse.token_type const aio = idToken.aio const aud = idToken.aud const email = idToken.upn const exp = idToken.exp const iat = idToken.iat const iss = idToken.iss const nbf = idToken.nbf const crypto = require('crypto');

function generateNonce() { return crypto.randomBytes(16).toString("hex"); } const nonce = generateNonce() const onpremisessamaccountname = "7700608017" const rh = idToken.rh const sub = idToken.sub const uti = idToken.uti const ver = idToken.ver

const proveAuthenticationTokenEntity = buildAuthTokenEntity( aio, aud, email, exp, iat, iss, name, nbf, nonce, localAccountId, onpremisessamaccountname, username, rh, sub, realm, uti, ver );

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

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

const accessTokenKey = ${homeAccountId}-${environment}-accesstoken-${clientId}-${realm}-${apiScopes};

const accessTokenEntity = buildAccessTokenEntity( homeAccountId, tokenResponse.access_token, tokenResponse.expires_in, tokenResponse.ext_expires_in, realm, apiScopes, tokentype );

localStorage.setItem(accountKey, JSON.stringify(accountEntity)); localStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity)); localStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity)); localStorage.setItem("proveAuthenticationToken", JSON.stringify(proveAuthenticationTokenEntity)); };

const injectTokensProve = (tokenResponse) => { const access_token2 = tokenResponse.access_token; const token_type2 = tokenResponse.token_type; const expires_in2 = tokenResponse.expires_in; const scope2 = tokenResponse.scope; const grupos2 = tokenResponse.grupos; const cargo2 = tokenResponse.cargo; const jti2 = tokenResponse.jti;

const provePermissaoToken = buildpermProve( access_token2, token_type2, expires_in2, scope2, grupos2, cargo2, jti2);

localStorage.setItem("provePermissaoToken",JSON.stringify(provePermissaoToken)); }

export const login = (cachedTokenResponse) => { let tokenResponse1 = null; let tokenResponse2 = null; let chainable = cy.visit("/"); //let chainableProve = cy.visit("/");

if (!cachedTokenResponse) { chainable = chainable.request({ url: authority + "/oauth2/v2.0/token", method: "POST", body: { grant_type: "password", client_id: clientId, client_secret: clientSecret, scope: apiScopes, username: username, password: password, }, form: true, }); } else { chainable = chainable.then(() => { return { body: cachedTokenResponse, }; }); }

chainable.then((response) => { console.log(response.body) injectTokens(response.body); tokenResponse1 = response.body; }).then(() => { let chainable = null; });

if(chainable){ chainable = chainable.request({ url: "https://xxxx/oauth/token", method: "POST", auth: { username: "", password: "" }, form: true, body: { grant_type: "password", scope: "read+write", username: "", password: "" } }) }

chainable.then((response2) => { console.log(response2.body) injectTokensProve(response2.body); tokenResponse2 = response2.body; }).then(() => { return tokenResponse1; });

return chainable; };

DVGY commented 3 years ago

@dnlopesoftware This is test code, I want to see what really happens in the front end when someone logs in, Instead of doing reload try visiting some URL.

kauppfbi commented 2 years ago

If the issue still persist, you can check out my blog post: https://dev.to/kauppfbi_96/test-msal-based-spas-with-cypress-4goe

It also follows the same approach, but maybe you find the missing piece.

Also MSAL triggers a Redirect when you initially acquired a token without requesting all necessary scopes. If you later try to use a API with a specific scope required, the login flow is triggered again.

mikhailhathey commented 2 years ago

I'm also experiencing this with Azure B2C Login, I've tried with Origin and Session but still get the same issue constantly.

cypress-app-bot commented 1 year ago

This issue has not had any activity in 180 days. Cypress evolves quickly and the reported behavior should be tested on the latest version of Cypress to verify the behavior is still occurring. It will be closed in 14 days if no updates are provided.

cypress-app-bot commented 1 year ago

This issue has been closed due to inactivity.