Cypress: unable to signup using keycloak through cypress

Created on 10 Jan 2019  路  11Comments  路  Source: cypress-io/cypress

Current behavior:

In my application, user management is done through Keycloak. Manual signup works fine but while testing my application end to end through cypress, I come across an issue. When I sign up user through it, it gives the following error:

We're sorry. An error has occurred, please login again through your application.

I have noticed that in test case cypress is appending session_code to the request url after I click on submit button. While doing manual test I don't get session code. This can be cause of this issue. Please share your views if you think of any other reason for this issue. Below is the url generated through cypress

.../login-actions/registration?session_code=LsZbmsVVLwEH9s-xwFJ2JdDtaCu1_xzqAGOQCpjxGJI&execution=06fac3bb-fb19-474b-8659-2572586ae371&client_id=web_app&tab_id=PSlmfgdv0ls

Where as manually generated url is like following

.../login-actions/registration?client_id=web_app&tab_id=PSlmfgdv0ls

My application backend is Spring boot and front-end is in React and next.js

It would be really helpful if anyone from your team could guide us for this issue. Please let me know if you need more information about our application.

Desired behavior:

Desired behavior is to able to signup through cypress tests

Steps to reproduce: (app code and test code)

You can use this test case

{
  "baseUrl": "",
  "pageLoadTimeout": 180000,
  "defaultCommandTimeout": 50000,
  "env":{
    "ENVIROMENT":"mock"
  },
  "reporter": "mochawesome",
  "reporterOptions": {
    "reportDir": "reports",
    "reportFilename": "test-report",
    "reportTitle": "Carbook Test Run",
    "overWrite": true,
    "saveHtml": true
  }
}
describe("Carbook Registration Page", () => {
  before(() => {
    cy.visit("/")
  })

  it("Verify that user is access registration page", () => {
    cy.get("#nav-sign-in").should('be.visible')
    cy.get("#nav-sign-in").click()
    cy.wait(5000)
    cy.get("a").contains("Register").should('be.visible')
    cy.get("a").contains("Register").click()
  })

  it("Verify that user able to go back to login page", () => {
    cy.get("#kc-form-options").should('be.visible')
    cy.get("#kc-form-options").click()
    cy.get("#kc-page-title").should('be.visible')
  })

  it("Verify that user is able to register for carbook", () => {
    cy.get("#firstName").type("Test")
    cy.get("#lastName").type("Test")
    cy.get("#email").type("[email protected]")
    cy.get("#password").type("test@123")
    cy.get("#password-confirm").type("test@123")
    cy.get('input[name="user.attributes.phone"]').type("12341234567")
    cy.get('input[name="user.attributes.cnic"]').type("1234512345678")
    cy.get('input[value="Register"]').click()
    cy.wait(10000)
  })
})

Most helpful comment

@ZijlkerVR I have followed steps 1 to 3 using this Blog Post of @vrockai, still can't figure out why it won't work.

It might be because of step 4 and 5 of your guide, can you provide more detailed information about that?

context('Logged in user', () => {
  beforeEach(() => {
    cy.kcLogin('testuser', 'testuserpass');
  });

  afterEach(() => {
    cy.kcLogout();
  });

  it('Should render logged user name somewhere on the page', () => {
    cy.visit('http://SAMPLE.com'); // still not logged in, although beforeEach is successful.
    cy.get('#username').should('contain', 'testuser');
  });
});

All 11 comments

Hey @wajeeha09, we are unable to run the tests provided without the application code or a url to visit in the tests. From the provided test code and settings - everything looks set up properly.

Could you provide screenshots, application code to visit replicating the error, anything else?
Unfortunately we'll have to close this issue if no reproducible example is provided.

If you're interested in dedicated email support, which gives you one-on-one help from our team, you can sign up for one of our paid plans then email us at [email protected].

@jennifer-shehane This zip file contains output video of this test. You can use this as you reference
register.e2e-spec.ts.mp4.zip

@jennifer-shehane Thank you for responding. Application is deployed. Base url is: https://carbook-client-public.carbook-mock.gocarbook.com. Using this url you should be able to reproduce this issue. Let me know if any other information is required.

Hi @jennifer-shehane is there any update on that issue?

I'm able to reproduce a failing test.

Cypress is not appending a session_code to the url. This is coming from keycloak. Likely there is something within the Cypress environment that makes keycloak behave differently and append this session_code (authorization plugins often don't play well with automated testing).

I'm unfamiliar with keycloaks api, but likely you want to skip over manually registering and stub/mock what is necessary for keycloak to recognize a registered user. Maybe after they register, you save their session id in localStorage, for example. Just set the values needed in localStorage within cypress. We do this with our own tests. There should be no need to test the particulars of keycloaks implementation. Only test that the methods in your application are working properly when they send what is expected.

Also, arbitrary waits are very very rarely necessary and can cause flake. I updated your tests to remove the wait times, instead waiting for the url to change.

describe('Carbook Registration Page', () => {
  before(() => {
    cy.visit('https://carbook-client-public.carbook-mock.gocarbook.com')
  })

  it('Verify that user is access registration page', () => {
    cy.get('#nav-sign-in').should('be.visible')
    cy.get('#nav-sign-in').click()
    cy.url().should('include', 'https://keycloak.carbook-dev.gocarbook.com')
    cy.get('a').contains('Register').should('be.visible')
    cy.get('a').contains('Register').click()
  })

  it('Verify that user able to go back to login page', () => {
    cy.get('#kc-form-options').should('be.visible')
    cy.get('#kc-form-options').click()
    cy.get('#kc-page-title').should('be.visible')
  })

  it('Verify that user is able to register for carbook', () => {
    cy.get('#firstName').type('Test')
    cy.get('#lastName').type('Test')
    cy.get('#email').type('[email protected]')
    cy.get('#password').type('test@123')
    cy.get('#password-confirm').type('test@123')
    cy.get('input[name="user.attributes.phone"]').type('12341234567')
    cy.get('input[name="user.attributes.cnic"]').type('1234512345678')
    cy.get('input[value="Register"]').click()
    cy.url().should('include', 'https://keycloak.carbook-dev.gocarbook.com')
    cy.contains('We\'re sorry').should('not.exist')
  })
})

@wajeeha09 Any update on this? Were you able to resolve this issue? I'd like to close it if so.

@wajeeha09 Have the same problem in my application using keycloak tested by cypress, but I do not recall seeing any extra parameters in my URL.

Do you have the issue when you login right away as first step of your first test? I'm only getting it whenever I try to login in test after some other actions/tests.

Workaround for now is changing before to beforeEach so it revisits the login page every test. I'm going straight to login page. Only doing this in a specific login.spec which has only 3 tests so impact is minimal.

Now trying to create a cy.request to skip the UI login altogether for all other tests as recommended by Cypress team. Having a hard time with that though. Not familiar with the API. Might be easier to just stub the authentications.

I've succeeded in automating our (proxied) Keycloak login without UI using a chain of 5 requests.

  1. GET request to retrieve actionUrl plus query string parameters from login page:
    /auth/realms/{realm}/protocol/openid-connect/auth?scope=name%2Cemail&response_type=code&approval_prompt=auto&redirect_uri={site}%2Fuserauth&client_id=account
    Used this regex to retrieve the url from the HTMLstring object and fix it (using &):
    resp.body.match(/action\=\"(.*)\" /)[1].replace(/&/g, '&');

  2. POST form request to the actionUrl with user/pass in body:
    /auth/realms/{realm}/login-actions/authenticate?session_code={x}&execution={x}&client_id=account&tab_id={x}
    Keycloak cookies are now set.

  3. Request to our login page which will determine 'state' and redirect to authenticate because of presence keycloak cookies

  4. Redirected to the same "auth" of step 1 but with state:
    /auth/realms/{realm}/protocol/openid-connect/auth?state={x}&scope=name%2Cemail&response_type=code&approval_prompt=auto&redirect_uri={site}%2Fuserauth&client_id=account

  5. Redirected to "userauth" eventually leading to our application setting our final login cookies:
    /userauth?state={x}&session_state={x}&code={x}

I've disabled followRedirect on all calls and handled them myself instead. Main reason is it didn't result in an authentication cookies for some reason. Second reason is we use Keycloak server on 1 environment for several other test environments. Catching the redirect enables us to go to particular environment.

Normally I would expect it to be possible to handle this in 2 - 3 steps when the redirecting is followed properly by both Cypress and website. Not sure where it failed. We have a custom keycloak proxy layer so just followed that one. Going through UI entirely will result in even more requests/redirects (8-10).

I realize this is not the most ideal way, but it was the quickest one and still decent result. Login in 2.5s instead of 6-10s through UI. Call 1 and 2 are done in 0.4 seconds. Step 3 is slowing it down caused by lazy acceptance server.

@ZijlkerVR I have followed steps 1 to 3 using this Blog Post of @vrockai, still can't figure out why it won't work.

It might be because of step 4 and 5 of your guide, can you provide more detailed information about that?

context('Logged in user', () => {
  beforeEach(() => {
    cy.kcLogin('testuser', 'testuserpass');
  });

  afterEach(() => {
    cy.kcLogout();
  });

  it('Should render logged user name somewhere on the page', () => {
    cy.visit('http://SAMPLE.com'); // still not logged in, although beforeEach is successful.
    cy.get('#username').should('contain', 'testuser');
  });
});

@pouriaMaleki Interesting blog post. His method is comparable to my first 2 steps. He has a cleaner way of retrieving the ActionURL though. He creates a UUID to fill state in step 1 but, atleast in my case, it's optional so I just skipped it and get it later. Although creating a state UUID beforehand might actually decrease the number of steps I need in total so gotta look into that. Not sure if it's the case.

I noticed he stops after his POST. In my case it would only give me KC cookies, but not site specific login cookies. I really need to follow through to atleast 3 more requests untill our site specific cookie will be set. Again.. it might be specifically related to our implementation since they have build quite some code around Keycloak here. It's been build by external supplier so lacking the knowledge on site.

Notice that my step 3 is a specific call back to the login page. Now that KC cookies are available it triggers our site to process and eventually end up with site specific auth cookies.

If I would follow redirect after step 2 POST I would get stuck. Might also be your problem.

Unfortunately I don't understand why steps 4 and 5 are needed. I would expect followRedirect=true in step 3 to work, but I had to disable followRedirect and manually catch and explicitly request them in order to get a successfull login cookie. Could be a Cypress bug.

My advice would be to go through the requests step by step by catching en requesting manually like I did. Will give more control and insight. When we login through UI there is even more than these 5 requests/redirects, but I cut it off as soon as I get the cookie.

My code might help (simplified a bit by removing our environment specific condition) :

Cypress.Commands.add("login", (user, pass) => {
    Cypress.log({ name: 'Login' })
    const userName = (user != undefined) ? user : Cypress.env('user')
    const passWord = (pass != undefined) ? pass : Cypress.env('pass')
    cy.clearCookies()

    // Non UI login - awesomely fast
    // 1. Get login params from auth
    // 2. Post form to authenticate
    // 3. Get back to /login
    // 4. Follow redirect to auth
    // 5. Follow redirect to origin

    const getStartBody = {
        url: 'Cypress.config('baseUrl')' + '/auth/realms/*REALM*/protocol/openid-connect/auth',
        followRedirect: false,
        qs: {
            scope: 'name,email',
            response_type: 'code',
            approval_prompt: 'auto',
            redirect_uri: Cypress.config('baseUrl') + '/userauth',
            client_id: 'account'
        }
    }
    // Step 1
    cy.request(getStartBody).then((getStartResp) => {

        const actionUrl = getStartResp.body.match(/action\=\"(.*)\" /)[1].replace(/&/g, '&');
        const postLoginBody = {
            method: 'POST',
            url: actionUrl,
            followRedirect: false,
            form: true,
            body: { username: userName, password: passWord }
        }
        // Step 2
        cy.request(postLoginBody)
        // Keycloak cookies now set

    }).then(() => {
            // Step 3
            cy.request({
                url: '/login?redirect=/',
                followRedirect: false
            }).then((redirectResp1) => {
                // Step 4
                cy.request({
                    url: redirectResp1.redirectedToUrl,
                    followRedirect: false
                })
            }).then((redirectResp2) => {
                // Step 5
                cy.request({
                    url: redirectResp2.redirectedToUrl,
                    followRedirect: false
                })
            }).then(() => {
                // Finally got the site cookie
                cy.getCookie('cookie').should('exist')
            })
    })

Closing as resolved. Please comment if you are still having this issue and we will consider reopening.

Was this page helpful?
0 / 5 - 0 ratings