3

I am having trouble with testing my oauth-secured application. The problem manifests itself when there is no public page - user is immediately redirected to OAuth server it they are not authenticated.

I managed to reproduce the problem in much simpler setup:

  • fake app running in fake-app domain
  • fake oauth server running in fake-oauth-server domain

Here are respective apps (in Flask):

Fake app

from flask import Flask, redirect, render_template_string

app = Flask(__name__)

app_host="fake-app"
app_port=5000
app_uri=f"http://{app_host}:{app_port}"
oauth_host="fake-oauth-server"
oauth_port=5001
oauth_uri=f"http://{oauth_host}:{oauth_port}"

@app.route('/')
def hello():
    return render_template_string('''<!doctype html>
           <html>
               <body>
                   <p>Hello, World MainApp!</p>
                   <a id="loginButton" href="{{ oauth_uri }}?redirect_uri={{ app_uri }}">Login</a>
               </body>
           </html>
           ''',
           oauth_uri=oauth_uri,
           app_uri=app_uri
    )

@app.route('/goto-oauth')
def goto_oauth():
    return redirect(f"{oauth_uri}?redirect_uri={app_uri}")

if __name__ == '__main__':
    app.run(host=app_host, port=app_port)

Fake oauth server:

from flask import Flask, render_template_string, request

app = Flask(__name__)

oauth_host="fake-oauth-server"
oauth_port=5001

@app.route('/')
def login():
    return render_template_string(
    '''<!doctype html>
      <html>
          <body>
              <p>Please log in</p>
              <label>Username: <label><input id="username" />
              <label>Password: <label><input id="password" />
              <a id="submit-password" href="{{ redirect_uri }}">Submit</a>
          </body>
      </html>
      ''', redirect_uri=request.args.get('redirect_uri'))


if __name__ == '__main__':
    app.run(host=oauth_host, port=oauth_port)

First flow: there is a publicly available page with Login button

This is possible to test with cy.origin:

describe('My Scenarios', () => {
  beforeEach(() => {
    cy.visit('/');
    cy.contains('MainApp');
    cy.get('a#loginButton').click();
    cy.origin('http://fake-oauth-server:5001', () => {
      cy.contains('Please log in');
      cy.get('input#username').type('user1');
      cy.get('input#password').type('password1');
      cy.get('a#submit-password').click()
    });
  });

  it.only('test flask', () => {
    cy.visit('/');
    cy.contains('MainApp');
  });
});

Problematic flow: immediate redirect to Oauth server

describe('My Scenarios', () => {
  beforeEach(() => {
    cy.visit('/goto-oauth');

    cy.origin('http://fake-oauth-server:5001', () => {
      cy.contains('Please log in');
      cy.get('input#username').type('user1');
      cy.get('input#password').type('password1');
      cy.get('a#submit-password').click()
    });
  });

  it.only('test flask', () => {
    cy.visit('/');
    cy.contains('MainApp');
  });
});

Fails with:

CypressError: `cy.origin()` requires the first argument to be a different domain than top. You passed `http://fake-oauth-server:5001` to the origin command, while top is at `http://fake-oauth-server:5001`.

Either the intended page was not visited prior to running the cy.origin block or the cy.origin block may not be needed at all.

There is no publicly available page in my app - how can I amend the test to make it work?

Fody
  • 23,754
  • 3
  • 20
  • 37
Lesiak
  • 22,088
  • 2
  • 41
  • 65
  • 2
    I'm guessing visiting the `goto-oauth` is equivalent to visiting the redirect it contains. Therefore, remove `cy.origin('http://fake-oauth-server:5001', () => {` and see if it's actually needed. Or you may also then need to `cy.origin()` the main URL (not ideal). – Blunt Nov 17 '22 at 10:32
  • @Blunt The latter works, with all obstacles coming from using cy.origin: parameter passing, inability to pass helper functions, and inablity to use cy.intercept https://on.cypress.io/github-issue/20720. While your comment addresses my question and I am willing to accept it as an answer, it looks like I am screwed until I find sth better. – Lesiak Nov 17 '22 at 12:56
  • Well, what about starting the `beforeEach()` with a `cy.visit('/')`? – Blunt Nov 17 '22 at 19:38
  • @Blunt In this fake app I have 2 endpoints: 1. / which simulates non-protected one 2. /goto-oauth which simulates protected one (server immediately responds with a redirect if there is no oauth token). In my real app I have only protected ones. – Lesiak Nov 17 '22 at 19:48
  • 1
    I had this same issue. The solution I ended up on is to modify your app to have at least one page which is unauthenticated and doesn't do the redirect. Make Cypress visit that one as first page. This will allow you to use origin the way you want it to. And if deploying such a page into production (it can literally be a completely empty page, or perhaps some 404 placeholder) is an issue you could always either insert it as part of your test scripting, or strip it out in your CI/CD process. Still not ideal, but imho beats the other solutions in simplicity. – Jasper Mar 16 '23 at 13:10

2 Answers2

3

It seems to work if visit the redirect URL inside the cy.origin().

I set the app on http://localhost:6001 and the auth server on http://localhost:6003, using express rather than flask.

Test

describe('My Scenarios', () => {
  beforeEach(() => {
    cy.origin('http://localhost:6003', () => {
      cy.visit('http://localhost:6001/goto-oauth')
      cy.contains('Please log in');
      cy.get('input#username').type('user1');
      cy.get('input#password').type('password1');
      cy.get('a#submit-password').click()
    });
  });

  it('test main app', () => {
    cy.visit('http://localhost:6001')
    cy.contains('MainApp')
  })
})

App

const express = require('express')
function makeApp() {
  const app = express()
  app.get('/', function (req, res) {
    res.send(`
      <html>
      <body>
        <p>Hello, World MainApp!</p>
        <a id="loginButton" href="http://localhost:6003?redirect_uri=http://localhost:6001">
          Login
        </a>
      </body>
      
      </html>
    `)
  })
  app.get('/goto-oauth', function (req, res) {
    res.redirect('http://localhost:6003')
  })

  const port = 6001

  return new Promise((resolve) => {
    const server = app.listen(port, function () {
      const port = server.address().port
      console.log('Example app listening at port %d', port)

      // close the server
      const close = () => {
        return new Promise((resolve) => {
          console.log('closing server')
          server.close(resolve)
        })
      }

      resolve({ server, port, close })
    })
  })
}

module.exports = makeApp

Auth

const express = require('express')
function makeServer() {
  const app = express()
  app.get('/', function (req, res) {
    res.send(`
    <!doctype html>
    <html>
        <body>
            <p>Please log in</p>
            <label>Username: <label><input id="username" />
            <label>Password: <label><input id="password" />
            <a id="submit-password" href="http://localhost:6001">Submit</a>
        </body>
    </html>
    `)
  })

  const port = 6003

  return new Promise((resolve) => {
    const server = app.listen(port, function () {
      const port = server.address().port
      console.log('Example app listening at port %d', port)

      // close the server
      const close = () => {
        return new Promise((resolve) => {
          console.log('closing server')
          server.close(resolve)
        })
      }

      resolve({ server, port, close })
    })
  })
}

module.exports = makeServer
Fody
  • 23,754
  • 3
  • 20
  • 37
  • 1
    accepted as it elegantly solves the problem in the fake app. Sadly, in the real app in stays on the login page (oauth server) - I need to look more closely to understand what is going on and improve my example :( – Lesiak Nov 18 '22 at 08:26
  • @Lesiak Did you solve your problem in the end? – Marcus Apr 03 '23 at 10:53
  • 1
    @Marcus No, it turned out to be a rabbit hole - I ended up using playwright which does not have these problems. – Lesiak Apr 03 '23 at 12:27
0

It's a dirty hack, but it works for me...
Just add an extra visit to a 404 page (or any page that doesn't require logging in, most likely you have one) so that Cypress gets the origin right.

    describe('My Scenarios', () => {
      beforeEach(() => {
        cy.visit('/404', { failOnStatusCode: false });
        cy.visit('/goto-oauth');
    
        cy.origin('http://fake-oauth-server:5001', () => {
          cy.contains('Please log in');
          cy.get('input#username').type('user1');
          cy.get('input#password').type('password1');
          cy.get('a#submit-password').click()
        });
      });
    
      it.only('test flask', () => {
        cy.visit('/');
        cy.contains('MainApp');
      });
    });
greybeard
  • 2,249
  • 8
  • 30
  • 66
  • 2
    There's no rule that says you must perform visit outside the origin command. Just encapsulate all code in origin sandbox, stop using the hack. – Debella May 31 '23 at 23:31
  • @Debella I'm not sure I get your point right... What's the point in incapsulating the visit inside the auth origin? In this case, you gonna have to wrap every test case into the app domain origin, and then, if you use `cy.session`, you gonna get another error :) – Yuriy Marchenko Jun 01 '23 at 08:58