Cypress automation when using the SPA SDK

Hi there,

Trying to keep it relatively high level, the blog post about automating the interaction with Auth0 from Cypress does not seem to work when using the SPA SDK.

It all goes well until handling the request back from the Cypress.env('auth_url'). Even when you configure the tenant correctly and get a 200 back with a valid token, etc the part where you fabricate a call back URL and force Cypress to follow it as indicated:

const callbackUrl = `/callback#access_token=${access_token}&scope=openid&id_token=${id_token}&expires_in=${expires_in}&token_type=Bearer&state=${auth0State.state}`;
cy.visit(callbackUrl, () => {
  // cookie setting, etc
});

…does not work. As I understand the SPA SDK has different expectations. There’s code flying around (Github) that suggests decoding the token and using local storage (but not everyone uses local storage). That sample code makes things even muddier by using cy.setLocalStorage() which is not part of the official Cypress API, it’s a plugin!

All in all, it would be a w e s o m e, if the blog post could be updated with official advice on how to handle Auth0 SPA SDK logins with Cypress.

Thanks a bunch!!!

1 Like

Hi @jdelgado!

Welcome to the Community!

I don’t know if you saw my response on your tweet (from @Auth0Community), but I submitted a request for this to the team who handles the blog. I’ll add this thread to the request if there are any questions from them.

Thanks!
Dan

Hey @dan.woda is there an update to this? Even if not for a “fix” (documentation update), but at least when you think someone will take a look at it. So at least we can plan around it.

Thanks.

Juan

Hi @jdelgado, we are having some back and forth on it, should have an update shortly. Thanks for your patience! In the meantime, have you looked at the repo mentioned in this thread?

Thanks Dan.

Yeah, looked at both the repo and the code in that Github issue and neither worked for us. The implementation is very different too.

After getting a valid response from the auth URL one uses cookies, the other one localStorage. One decodes the token before saving it, the other forces a call to (presumably the callback URL?) with code and state like so: /?code=test-code&state=${stateId}.

Thanks.

@jdelgado,

Here is the update I got from our team who creates this content:

We are collaborating with Cypress on new guidance but we don’t have an ETA yet.

Thanks Dan.

I’m really happy that you are working with the Cypress team (truly!) but I don’t think this has to do with them. My feeling is that you’d have the same issue with any other driver / automation tool.

For me the issue is not having clear documentation about the SPA’s SDK expectations when coming back from the API call (cookie vs local storage, what to store in them, what URLs need to be called…).

Thanks,

Juan

If this is a documentation problem then I can suggest an update fairly quickly.

Can you elaborate on this? We have documented the storage options here. Are you saying its not clear where the tokens are stored?

No, I’m not saying that.

I’m saying it’s not clear a) what to store (regardless of cookies vs local storage) and b) what URLs to call and with which parameters, if any.

To be specific, neither the code in this Github comment nor this repo work or match what I see in my local environment.

The code in the Github repo comes closer but:

  • The key for the local storage proposed differs from the one I see being set by the SPA SDK in my app under test.
  • What the SPA SDK puts inside local storage has way more information than what the code in that Github comment proposes. If some of that extra information is optional, I don’t know.
  • Still not clear to me whether after setting the correct local storage object I can redirect anywhere in my app that requires credentials OR should I hit a specific local URL (and if so, with which parameters).

Thanks,

J

1 Like

Thanks for elaborating. Hopefully this info will be useful when they address the blog post.

As for right now, our team knows this is something that needs updated in our blogs/docs and are actively working on it.

Your best avenue is probably either the repo issue about it, or the other topic I linked where the user was able to get a solution. I’ll leave this thread open in case anyone has some input.

Ok, the plot thickens. Or maybe clarifies. But I need to provide more context.

I’m running this whole thing using Docker and Docker Compose. This is important because I don’t run my frontend app in localhost, but using its own service name as defined in the Docker Compose file (frontend).

I also run Cypress in a different container, so when I docker-compose up I need to tell Cypress that the application under test is running in http://frontend:3000. As you might have guess by now, this is no bueno, I’m getting a secure origin error as per this note in the documentation.

Because the origin frontend is not considered secure, the SPA SDK is throwing an error. This took a while to find out because running Cypress inside Docker is not particularly straightforward, so digging out relevant logs took a long time.

At this point I’m not sure how to proceed. Not sure if both Cypress and Auth0 are working as expected and I should find some other way to run the frontend application in a “secure” location (localhost) or if there’s anything I can do on my end to figure things out.

Thanks,

Juan

1 Like

Is there any update on this? We’re in the same boat as @jdelgado and so far have been unable to leverage Cypress to automate our UI tests exactly as outlined above. The fact that the whole article was written without using the official SPA SDK is crazy to me - no enterprise implementation would be rolling their own auth management when we are paying Auth0 who provides SDKs/APIs.

1 Like

We would also like to update our app to use auth0-angular. But there does not seem to be a documented way on how to use cypress together with it. Is there any news on this front?

1 Like

Hey there!

Unfortunately we don’t have combined content on angular + cypress. What I would encourage you to do is to provide such feedback in a form of a topic in our Feedback category here, providing all the needed context:

Actually, you do have content like that: End-to-End Testing with Cypress and Auth0
But it is not up to date.

Yep that’s pretty much the reason why I haven’t pointed you there. I would really encourage you to leave your feedback in order to advocate for such update.

The following worked for me, and I hope it will be helpful to someone else as well:

  1. Setup a route which is publicly available (this is key for this to work) and void of both functionality and content (if you already have a public route, you can use this).
/** Dummy component required for E2E testing */
@Component({ selector: 'app-public', template: `<!-- Test component -->` })
export class PublicComponent {}

/** My app module **/
@NgModule({
  declarations: [AppComponent, PublicComponent],
  imports: [
    RouterModule.forRoot(
      [
        ...
        { path: 'hidden', component: PublicComponent }, // This route is not in use by anything other than E2E testing
      ],
    ),
    ....
})
export class AppModule {}
  1. Create the custom login command:
Cypress.Commands.add('login', (email, password) => {
  Cypress.log({ name: 'login via Auth0' });
  
  // Direct browser to only route in my application which is not bound by an AuthGuard. 
  // The reason for doing this is so that we can access localStorage/cookies on the correct domain
  cy.visit('/hidden');

  const client_id = Cypress.env.CLIENT_ID;
  const secret = Cypress.env.CLIENT_SECRET;
  const audience = Cypress.env.AUDIENCE; // https://${tenantDomain}/api/v2/.
  const rscope = 'openid profile email offline_access';
  return cy
    .request({
      url: Cypress.env.AUTH_URL, // https://${tenantDomain}/oauth/token
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: {
        grant_type: 'password',
        username: email,
        password: password,
        audience: audience,
        scope: rscope,
        client_id: client_id,
        client_secret: secret,
      },
    })
    .then(response => {
      const { scope, access_token, id_token, token_type, expires_in, refresh_token } = response.body;

      cy.window().then(win => {
        const payload = JSON.parse(Buffer.from(id_token.split('.')[1], 'base64').toString('ascii'));
        win.localStorage.setItem(
          `@@auth0spajs@@::${client_id}::${audience}::${rscope}`,
          JSON.stringify({
            body: {
              client_id,
              access_token,
              id_token,
              scope,
              expires_in,
              token_type,
              refresh_token,
              decodedToken: {
                claims: Object.assign({}, payload, { __raw: id_token }),
                encoded: {
                  header: id_token.split('.')[0],
                  payload: id_token.split('.')[1],
                  signature: id_token.split('.')[2],
                },
                header: JSON.parse(Buffer.from(id_token.split('.')[0], 'base64').toString('ascii')),
                user: {
                  email: payload.email,
                  email_verified: payload.email_verified,
                  name: payload.name,
                  nickname: payload.nickname,
                  picture: payload.picture,
                  sub: payload.sub,
                  updated_at: payload.updated_at,
                },
              },
              audience,
            },
            expiresAt: Math.floor(Date.now() / 1000) + expires_in,
          })
        );
      });
      cy.setCookie('auth0.is.authenticated', 'true');
    });
});

  1. Now create a test:
    it('should show home page', () => {
      cy.login(Cypress.env.USER, Cypress.env.PASSWORD).then(() => {
        cy.visit('/');
        cy.url().should('contain', '/home/check-in');
      });
    });

The client_id and client_secret must match what the application is configured for. I could not make this work using a new Auth0 Application for this (which other tutorials specify). The reason for this, is that the auth-spa-js checks localStorage by key, and the key contains the client_id. If you use a different client_id for your tests, the application will not pick this up.

You also have to add “Password” grant in Auth0 → Applications → {your application} → Advanced (which Auth0 advices against, but as previously stated I cannot setup a dedicated Auth0 application for this)

2 Likes

Thanks for sharing that with the rest of community!

1 Like

I guess you figured it out a long time ago how to do it, but if anyone else having the same issue, I can provide an easier way without exposing your test environment onto a public https address.

For us the easiest way to achieve this was to proxy http://frontend:3000 to localhost:80 (inside the cypress container) which is a secure location for chrome, as you already stated.

e.g. something like this before executing cypress tests.

const http = require("http");
const httpProxy = require("http-proxy");

httpProxy.createProxyServer({ target: "http://frontend:3000" }).listen(80);
1 Like