Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Azure AD Single Sign On with Cypress
// This goes in cypress/plugins/index.js
const AzureAdSingleSignOn = require('./azure-ad-sso/plugin').AzureAdSingleSignOn
module.exports = (on, config) => {
on('task', {AzureAdSingleSignOn:AzureAdSingleSignOn})
}
// This is an example of how you might use the plugin in your tests
describe('My spec', function() {
Cypress.Commands.add('setCookies', function () {
const options = {
username: Cypress.env('username'),
password: Cypress.env('password'),
loginUrl: Cypress.env('appUrl'),
postLoginSelector: '#myselector',
headless: true,
logs: false
}
cy.task('AzureAdSingleSignOn', options).then(result => {
cy.clearCookies()
result.cookies.forEach(cookie => {
cy.setCookie(cookie.name, cookie.value, {
domain: cookie.domain,
expiry: cookie.expires,
httpOnly: cookie.httpOnly,
path: cookie.path,
secure: cookie.secure
})
Cypress.Cookies.preserveOnce(cookie.name)
})
})
})
before(function() {
cy.setCookies();
})
it('Visits the site as logged in user', function() {
cy.visit(Cypress.env('appUrl'));
cy.contains(`Hello, ${Cypress.env('username')}!`)
})
})
// I put this in cypress/plugins/azure-ad-sso directory
'use strict'
const puppeteer = require('puppeteer')
/**
*
* @param {options.username} string username
* @param {options.password} string password
* @param {options.loginUrl} string password
* @param {options.postLoginSelector} string a selector on the app's post-login return page to assert that login is successful
* @param {options.headless} boolean launch puppeteer in headless more or not
* @param {options.logs} boolean whether to log cookies and other metadata to console
* @param {options.getAllBrowserCookies} boolean whether to get all browser cookies instead of just for the loginUrl
*/
module.exports.AzureAdSingleSignOn = async function AzureAdSingleSignOn(options = {}) {
validateOptions(options)
const browser = await puppeteer.launch({ headless: !!options.headless })
const page = await browser.newPage()
await page.goto(options.loginUrl)
await typeUsername({ page, options })
await typePassword({ page, options })
const cookies = await getCookies({ page, options })
await finalizeSession({ page, browser, options })
return {
cookies
}
}
function validateOptions(options) {
if (!options.username || !options.password) {
throw new Error('Username or Password missing for login')
}
if (!options.loginUrl) {
throw new Error('Login Url missing')
}
if (!options.postLoginSelector) {
throw new Error('Post login selector missing')
}
}
async function typeUsername({ page, options } = {}) {
await page.waitForSelector('input[name=loginfmt]:not(.moveOffScreen)', { visible: true, delay: 10000 })
await page.type('input[name=loginfmt]', options.username, { delay: 50 })
await page.click('input[type=submit]')
}
async function typePassword({ page, options } = {}) {
await page.waitForSelector('input[name=Password]:not(.moveOffScreen),input[name=passwd]:not(.moveOffScreen)', { visible: true, delay: 10000 })
await page.type('input[name=passwd]', options.password, { delay: 50 })
await page.click('input[type=submit]')
}
async function getCookies({ page, options } = {}) {
await page.waitForSelector(options.postLoginSelector, { visible: true, delay: 10000 })
const cookies = options.getAllBrowserCookies
? await getCookiesForAllDomains(page)
: await page.cookies(options.loginUrl)
if (options.logs) {
console.log(cookies)
}
return cookies
}
async function getCookiesForAllDomains(page) {
const cookies = await page._client.send('Network.getAllCookies', {})
return cookies.cookies
}
async function finalizeSession({ page, browser, options } = {}) {
await browser.close()
}
@csuzw

This comment has been minimized.

Copy link
Owner Author

csuzw commented Nov 7, 2019

This was heavily influenced by the following:

This could be improved by:

  • Delays could be removed - I don't think they're required.
  • Add all the additional steps that might occur in the single sign on flow (like account selection and remember me options). These are all accounted for in aws-azure-login.
  • Convert this to a proper npm package like cypress-social-logins
@ElvisLives

This comment has been minimized.

Copy link

ElvisLives commented Nov 7, 2019

Nice work, we are having some early success using this! Thanks for putting it together.

@Kpizzle

This comment has been minimized.

Copy link

Kpizzle commented Nov 13, 2019

Hey @csuzw,

Thanks for this. Has really helped me out.

I'm having one issue though, currently this is really unstable for me.

I'm able to make all the right calls and get the token but then after 2 or 3 tests cypress starts to act like the cookies are no longer there and hits the Azure AD issue again.

I tried to extract this to a before hook in my cypress/support/index.js file so as I don't have the code in each test file.

Have you experienced this at all?

@csuzw

This comment has been minimized.

Copy link
Owner Author

csuzw commented Nov 14, 2019

@Kpizzle : No I've not experienced this but I've also not used this in anger yet. I have 2 thoughts, firstly the cookies are just expiring and you need to run this more often or increase cookie lifetime. I believe Cypress is meant to clear cookies down between specs but I also think there may be a bug related to this at the moment that is causing them not to be cleared, so not exactly sure what behaviour you'd see here.
Alternatively this might just be an issue with the login, in which case my suggestion would be to change the headless parameter to false and see what is going on. This will cause the login process to display in a browser instead of happening silently. If the problem is here I would guess that you're encountering an extra step in the login process, perhaps account selection or remind me popups, that is causing the login to get stuck. If this is the case then you could take a look at aws-azure-login (which I linked to in my first comment) as this has selectors and code for each of these additional popups and steps.

@ddregalo

This comment has been minimized.

Copy link

ddregalo commented Nov 21, 2019

@csuzw - I appreciate you for getting this up as its the closest I've been to solving this yet! (and the work of @pieterdv, @saschwarz, and @jkosters you mentioned that inspired this) :)

I'm experiencing the following behaviour after the plugin runs and sets the cookies and passes back to Cypress in the test function on cy.visit('myurl'): the page starts loading but almost immediately tries to visit login.mircosoftonline...[...] again for authentication which then fails the test producing the following error: chrome-error://chromewebdata/ - this is with chromeWebSecurity: false

If I set I get the following cross-origin error:
SecurityError: Blocked a frame with origin "https://app-test.mydomain.com" from accessing a cross-origin frame.

I am wondering if there is something I've done incorrectly that is forcing the cy.visit() in the test to redirect to AD login even with the cookies set? Have you experience this redirect at all? Any ideas why this is happening and how to address it?

Note - I have added an extra step in puppeteer to select Stay Logged In => Yes in the extra AD login process step to handle that which is working correctly.

@Kpizzle

This comment has been minimized.

Copy link

Kpizzle commented Nov 21, 2019

Hey @ddregalo,

I was having a very similar issue in that cypress wasn't keeping/setting the cookies and it would fail over to the AD login after 2 or 3 tests.

I got round this by storing the cookies locally in the root dir and then for each test I have a before hook func that loads the cookies before each test.

Cypress clears cookies after each tests for a clean state. So far it's work perfectly.

I'm on mobile and not near my code, but can provide an example if needed.

@csuzw

This comment has been minimized.

Copy link
Owner Author

csuzw commented Nov 21, 2019

@ddregalo Just in case @Kpizzle 's solution doesn't solve your issue, I wonder if it's related to headers. If you look at the solutions that inspired mine, they don't set cookies at all, instead they set a bunch of headers, related I believe to their use of the MSAL/ADAL libraries. This didn't work for me but it might be dependant on exactly how your site integrates with Azure AD. I think if I was investigating this I'd manually log in the site and then check cookies and headers to see if there is anything the puppeteer login is not doing.

@ddregalo

This comment has been minimized.

Copy link

ddregalo commented Nov 21, 2019

@Kpizzle , @csuzw - really appreciate the quick replies! Had to jump on something else for a second but will re-visit this very shortly and let you know what worked (fingers crossed!)

@ddregalo

This comment has been minimized.

Copy link

ddregalo commented Nov 22, 2019

Hey @csuzw @Kpizzle - thanks a ton for your suggestions, huge help in debugging where I was at and what needed to be done. I took the long way but I GOT THERE IN THE END!

The key fix was observing what happens on manual login as you suggested and this proved I didn't have the same cookies as when I ran the puppeteer plugin. During the login process different cookies are set at different stages and I was only capturing 2-3 cookies after clicking Stay Signed In => Yes. I then hardcoded all cookies I got from manually logging-in into my test with cy.SetCookie() and everything ran perfectly.....then returning to double check this repo I realized that I must have accidentally deleted my getAllBrowserCookies option which when re-enabled (true), captured all the cookies I needed and logged in exactly as if I had manually set them...BAM!

Super happy about this, I've been trying to get away from Selenium Web-Driver to a more modern (and more friendly) front-end testing framework and I've been really loving Cypress but was super stuck when I hit the AD wall - thanks again for this, really appreciate it!

*I don't actually have a bunch of tests yet to flex the robustness of running continuously back-to-back tests but now that I got the motivation I'll reach back and let you know how it goes...

@Kpizzle

This comment has been minimized.

Copy link

Kpizzle commented Nov 22, 2019

Awesome work @ddregalo,

Always feels good when you get your code working.

Yeah, I'd starting getting a nice collection of tests going and see how that works. My issue started (the instability) once I started having multiple separate spec files, and the cookies were being cleared between them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.