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

Cannot automate login in cypress since 1.12 using cookie injection #581

Closed
eplatek-safyr opened this issue Sep 18, 2020 · 17 comments
Closed
Labels
bug report This issue reports a suspect bug or issue with the SDK itself

Comments

@eplatek-safyr
Copy link

eplatek-safyr commented Sep 18, 2020

Describe the problem

In order to automate test to a frontend application i'm currently working on, I'm using cypress on a react app.

As it's not possible to navigate to the auth0 login page, I've been using a command that generates the user token using the API POST /oauth/token, and then inject the token in the cookies and redirect the front end as it would occur after a successful login using the state param.

But since version 1.12, the behavior has changed and the auth0 sdk does not seem to accept this way to inject authentication.

Here is the code I'm using to authenticate the browser automation (This code still works using 1.11):

Cypress.Commands.add("login", ({ username, password }, appState = { targetUrl: "/" }) => {
  cy.log(`Log in as ${username}`)
  cy.request({
    method: "POST",
    url: Cypress.env("AUTH0_GET_TOKEN_URL"),
    body: {
      grant_type: "password",
      username: username,
      password,
      audience: Cypress.env("AUTH0_AUDIENCE"),
      scope: "openid profile email",
      client_id: Cypress.env("AUTH0_CLIENTID"),
      client_secret: Cypress.env("AUTH0_CLIENT_SECRET"),
    },
  }).then(({ body }) => {
    const { access_token, expires_in, id_token } = body

    cy.server()
    cy.route({
      url: Cypress.env("AUTH0_GET_TOKEN_URL"),
      method: "POST",
      response: {
        access_token,
        id_token,
        scope: "openid profile email",
        expires_in,
        token_type: "Bearer",
      },
    })

    const stateId = "test"
    cy.setCookie(
      `a0.spajs.txs.${stateId}`,
      encodeURIComponent(
        JSON.stringify({
          appState,
          scope: "openid profile email",
          audience: Cypress.env("AUTH0_AUDIENCE"),
          redirect_uri: "http://localhost:3000",
        })
      )
    )
    cy.visit(`/?code=test-code&state=${stateId}`)
  })
})

Maybe it's a bad thing to do, but it seemed fine to me to do it that way, if you have any other option available I would be glad to hear about how I should do it differently.

What was the expected behavior?

I was expecting that the state handling would be the same after the version upgrade.

Reproduction

  • Allow password authentication on your tenant
  • Generate a token using auth0 api (POST https://{your-tenant}.auth0.com/oauth/token)
  • insert a cookie at a0.spajs.txs.${stateId} as the login page would do
  • mock the auth0 api with the generated token
  • redirect your app to the login callback with the parameters /?code=any-code&state=${stateId}

Can the behavior be reproduced using the SPA SDK Playground?

  • I'm not sure about how to test it with the playground

Environment

Please provide the following:

  • Version of auth0-spa-js used: 1.12.1
  • Which browsers have you tested in? any
  • Which framework are you using, if applicable (Angular, React, etc): react
  • Other modules/plugins/libraries that might be involved: cypress
@eplatek-safyr eplatek-safyr added the bug report This issue reports a suspect bug or issue with the SDK itself label Sep 18, 2020
@eplatek-safyr eplatek-safyr changed the title Cannot automate login in cypress since 1.12 using cookie injection Cannot automate login in cypress since 1.12 using cookie injection with firefox Sep 18, 2020
@eplatek-safyr eplatek-safyr changed the title Cannot automate login in cypress since 1.12 using cookie injection with firefox Cannot automate login in cypress since 1.12 using cookie injection Sep 18, 2020
@stevehobbsdev
Copy link
Contributor

@eliasplatekdkt thanks for raising. In v1.12 we moved from using cookies for the transaction store to session storage, so if you're relying on cookie behaviour your tests will need to be updated.

It does get slightly easier though, in that we no longer include the state value in the key name, so that can be dropped. Otherwise, the data stored inside session storage should be exactly the same as it was for cookies.

Let me know if you're able to get this working as before.

@eplatek-safyr
Copy link
Author

Hi Steve, thanks for pointing me in the right direction, I've made some changes to my code in order to use the session storage.
Now the transaction manager can access the data to get the token.

    const { access_token, expires_in, id_token } = body

    cy.server()
    cy.route({
      url: Cypress.env("AUTH0_GET_TOKEN_URL"),
      method: "POST",
      response: {
        access_token,
        id_token,
        scope: "openid profile email",
        expires_in,
        token_type: "Bearer",
      },
    })
    cy.window().then((win) => {
      win.sessionStorage.setItem(
        "a0.spajs.txs",
        JSON.stringify({
          nonce: "how can i set a valid nonce here ?",
          code_verifier: "code_verifierIn",
          appState: appState,
          scope: "openid profile email",
          audience: Cypress.env("AUTH0_AUDIENCE"),
          redirect_uri: "http://localhost:3000",
        })
      )
    })
    cy.visit("/?code=test-code&state=state")

But I still have a problem that I don't know how to solve, when I'm calling the POST /oauth/token, the id_token field doesn't contain a nonce, but the validation requires it to be present, and I don't know how to make it appear using the API, I don't see any doc about it, the library calls the token endpoint and receives the nonce in it.

Do you have any idea how I can get a nonce in my API call ?

@stevehobbsdev
Copy link
Contributor

Normally would specify the nonce in the call to /authorize with a scope of openid and response type id_token token, which would cause nonce to appear in the resulting ID token. However, I'm not sure how this would have changed for you with the changes you've made (although, I can't see the whole changeset). Can you try sending a nonce value through in your initial call to the token endpoint?

@eplatek-safyr
Copy link
Author

I'm not using the /authorize endpoint. I'm using the same code as in my initial post, except that I set the transaction object in the session storage instead of the cookie.

I wrote this code based on what is still recommended on the official documentation: https://auth0.com/blog/end-to-end-testing-with-cypress-and-auth0/

The limitation of cypress is that I cannot navigate to another domain than the domain under test. So I don't see a way to automate the login without using POST /oauth/token withgrant_type: password.

The initial solution would still work if the library does not try to validate the nonce when it is not defined in the id_token, would that be an acceptable solution ?

@eplatek-safyr
Copy link
Author

@stevehobbsdev I've been playing a bit with the library, and it can be fixed by removing the pre-check on the nonce in handleRedirectCalback:

    // Transaction should have a `code_verifier` to do PKCE and a `nonce` for CSRF protection
    if (!transaction || !transaction.code_verifier || !transaction.nonce) {
      throw new Error('Invalid state');
    }

This check can be removed safely I think because the nonce is revalidated after getting the token in verifyIdToken:

  if (options.nonce) {
    if (!decoded.claims.nonce) {
      throw new Error(
        'Nonce (nonce) claim must be a string present in the ID token'
      );
    }
    if (decoded.claims.nonce !== options.nonce) {
      throw new Error(
        `Nonce (nonce) claim mismatch in the ID token; expected "${options.nonce}", found "${decoded.claims.nonce}"`
      );
    }
  }

If the transaction test only checks for the code_verifier before calling the POST /oauth/token, then you ensure that the code verifier is provided, and when you verify the id_token the nonce is validated as before (it is even declared as an optional parameter).

From my point of view it seems enough to fix it like this, I can provide a PR if it's okay for you

@stevehobbsdev
Copy link
Contributor

@eliasplatekdkt Good spot, this actually looks like a regression as we only started checking nonce there recently. Although I can't understand why your tests are failing just by changing to session storage, as this should have also failed when you were using cookies.

@adamjmcgrath what are your thoughts here? I understand we should only be checking nonce if one was provided in the original authentication request but this check effectively makes it mandatory.

@eplatek-safyr
Copy link
Author

eplatek-safyr commented Sep 22, 2020

The login action started failing after upgrading to 1.12.0, I didn't change anything except upgrading the library. BTW here is the configuration I'm using:

export const auth0Config: Auth0ClientOptions = {
  domain: REACT_APP_AUTH0_DOMAIN!,
  client_id: REACT_APP_AUTH0_CLIENTID!,
  redirect_uri: window.location.origin + "/callback",
  audience: REACT_APP_AUTH0_AUDIENCE,
  useRefreshTokens: true,
}

I cannot use my code anymore because I was setting the transaction data in the cookie as it was originally working. After the upgrade, the transaction data is defined in the session storage as you told me in your comment, so updating the transaction setup in the storage is working fine, here is the complete version of the login command:

Cypress.Commands.add("login", ({ username, password }, appState = { targetUrl: "/" }) => {
  cy.log(`Log in as ${username}`)
  // Call the Auth0 API to get a token for the provided username with
  // with the grant_type password to https://{your-tenant}.auth0.com/oauth/token
  cy.request({
    method: "POST",
    url: Cypress.env("AUTH0_GET_TOKEN_URL"),
    body: {
      grant_type: "password",
      username,
      password,
      audience: Cypress.env("AUTH0_AUDIENCE"),
      scope: "openid profile email",
      client_id: Cypress.env("AUTH0_CLIENTID"),
      client_secret: Cypress.env("AUTH0_CLIENT_SECRET"),
    },
  }).then(({ body }) => {
    // body contains the result of the call to Auth0 API
    const { access_token, expires_in, id_token } = body

    // Mocking the call to the Auth0 API that will be fired
    // by the handleRedirectCallback action in the library,
    // and injecting the token from the previous call
    cy.server()
    cy.route({
      url: Cypress.env("AUTH0_GET_TOKEN_URL"),
      method: "POST",
      response: {
        access_token,
        id_token,
        scope: "openid profile email",
        expires_in,
        token_type: "Bearer",
      },
    })
    // Injecting the transaction object that will be used by the library
    // to setup the call to the Auth0 API (it's supposed to contain the
    // code_verifier from the code query param be it won't be used)
    cy.window().then((win) => {
      win.sessionStorage.setItem(
        "a0.spajs.txs",
        JSON.stringify({
          code_verifier: "any-code",
          appState,
          scope: "openid profile email",
          audience: Cypress.env("AUTH0_AUDIENCE"),
          redirect_uri: "http://localhost:3000",
        })
      )
    })
    cy.visit("/?code=any-code&state=any-state")
  })
})

Although he difference with the library is that I'm using the grant_type: "password" option to the POST /oauth/token instead of grant_type: "authorize", so the id_token that I'm returning back to the lib doesn't contain a nonce.

@adamjmcgrath
Copy link
Contributor

@adamjmcgrath what are your thoughts here? I understand we should only be checking nonce if one was provided in the original authentication request but this check effectively makes it mandatory.

True, you don't need the code_verifier either, if your code hasn't been created with a code_challenge - but we added that check to prevent people logging in to an app with a code they didn't create from the login process.

I'm ok with removing the nonce check if we have to, let's double check with security first though.

@eliasplatekdkt - could you avoid this and just get an access token via the api, then store it in localstorage and use cachelocation: 'localstorage' in cypress mode?

It relies less on our internals, so would be less prone to being broken.
eg:

export const auth0Config: Auth0ClientOptions = {
  domain: REACT_APP_AUTH0_DOMAIN!,
  client_id: REACT_APP_AUTH0_CLIENTID!,
  redirect_uri: window.location.origin + "/callback",
  audience: REACT_APP_AUTH0_AUDIENCE,
  useRefreshTokens: true,
  cacheLocation: window.Cypress ? 'localstorage' : 'memory'
}

and

cy.window().then((win) => {
      win.localStorage.setItem(
        `@@auth0spajs@@::${client_id}::${audience}::${scope}`,
        JSON.stringify({
          access_token,
          ...
        })
      )
    })

similar to: https://sandrino.dev/blog/writing-cypress-e2e-tests-with-auth0#jamstack-and-single-page-applications

@eplatek-safyr
Copy link
Author

@adamjmcgrath thanks for the link I didn't know about this way of logging in.

If I'm understanding correctly the solution using the localStorage is way more complex, because before using the localStorage some cookies checks are performed, and the implemented solution requires:

  • to build a login url using the Auth0Client
  • navigate to the login page using puppeteer
  • extract the cookies and the redirect uri after successful login
  • inject the cookies and redirect to the app with the redirect uri

Even if this solution seems more interesting because I wouldn't even need to change the app code, the setup is really more complex and doesn't seem to be recommended by the official documentation that says that we shouldn't test the login page, and it relies on internals that can change and break the code the same way that I'm struggling today with the cookie management :)

It doesn't seem possible to me to only inject the token in the localStorage, I've tried to log in with my app using for cacheLocation: "localstorage", I can see that a key is added with the format you gave me. There is also a cookie named auth0.is.authenticated=true. Some hidden cookie seem to reactivate the connection, even after clearing the cookies and local storage.

So I tried to inject the cookie and the localStorage key, but the library still tries to redirect to the login page, this time I can't see why.

I'm not sure that using memory or localstorage will change the matter here, because in both solutions there is a need to rely on some internals of the library, with that said, if moving the nonce validation further in the code doesn't occur security issues, it seems a more benefical solution to me.

@adamjmcgrath
Copy link
Contributor

Hey @eliasplatekdkt

If I'm understanding correctly the solution using the localStorage is way more complex, because before using the localStorage some cookies checks are performed

apologies, I just used that as an example of setting localstorage - you don't need to do the cookie stuff

I've put a working example here https://github.com/adamjmcgrath/cypress-spa-example

I think it's simpler than your solution because it doesn't rely on cy.request/cy.server
It has the disadvantage of having to change cacheLocation in cypress mode
But it works with the latest version of spa js and IMO relies less on our internals

@eplatek-safyr
Copy link
Author

@adamjmcgrath this is a very elegant solution and it works seamlessly. I tried the same implementation but some fields were missing which is why it didn't work. Thanks a lot for your time !

I think that would be interesting to update the documentation at https://auth0.com/blog/end-to-end-testing-with-cypress-and-auth0/ which explains quite a different approach, and can be misleading for people searching for cypress + auth0, the localStorage approach requires less setup in cypress. I'm going with this solution in my project.

@camden-brown
Copy link

Been debugging this for a few hours now. Glad someone found a working solution. Thank you!

@camden-brown
Copy link

camden-brown commented Sep 23, 2020

The implementation seems to work fine for logging in initially but my Cypress suite seems to get stuck whenever a redirect occurs within the app. Spent several hours trying to debug but couldn't really find anything helpful.

For now, just reverting back to v1.11.0.

@vinzcelavi
Copy link

@eliasplatekdkt @adamjmcgrath Thank you so much guys ! It helps me a lot to auto login users on my SPA ! 💪

@stevehobbsdev
Copy link
Contributor

Glad you got it working @vinzcelavi!

@StephenCleary
Copy link

One thing that tripped me up was I had useRefreshTokens set to true, so the auth0 library was adding offline_access to my scopes, which changes the localstorage key.

@adamjmcgrath
Copy link
Contributor

Thanks for sharing that @StephenCleary

If anyone else is having trouble setting up #581 (comment) - It's usually because the localStorage key for the credentials you're putting in aren't the same as the key SPA JS is looking for (eg you unexpectedly had offline_access in your scopes)

I added a suggestion and snippet of code that might help you debug situations like this here adamjmcgrath/cypress-spa-example#2 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug report This issue reports a suspect bug or issue with the SDK itself
Projects
None yet
Development

No branches or pull requests

6 participants