Search code examples
testingsails.jsmocha.jscsrfsupertest

How can I test the Sails.js v1.0 login controller with CSRF security enabled (using mocha, supertest)?


I have an almost-new Sails.js 1.0.2 app and I'm able to log in with both a browser and with Postman. However, I can't seem to make the same process work in my test runner.

The test below should result in a successful login, where a cookie is returned with a new session ID. If I change the security configuration to disable CSRF protection, it runs perfectly. But with security enabled, the request is forbidden (403). The only substantial difference between what I'm sending in Postman seems to be that mocha runs the app on a different port (Postman sends to localhost:1337, express' res variable says PUT /api/v1/entrance/login HTTP/1.1 Host: 127.0.0.1:56002

Anyone see something I'm missing?

Here's the test file:

/**
 * /test/integration/controllers/entrance/login.test.js
 */

'use strict';

const supertest = require('supertest');  // also tried supertest-session

describe('Entrance controllers', () => {

  describe('/api/v1/entrance/login', () => {

    before(() => {
      return supertest(sails.hooks.http.app)
      .get('/login')
      .then(res => {
        const reTokenCapture = /_csrf:\s*unescape\('([^']+)'\)/;
        const found = reTokenCapture.exec(res.text);
        this._csrf = sails.config.security.csrf ? found[1] : '';
        this.url = '/api/v1/entrance/login';
      });
    });

    it('should return a session cookie in response headers', () => {
      return supertest(sails.hooks.http.app)
      .put(this.url)
      .set('x-csrf-token', this._csrf)
      .send({
        emailAddress: '[email protected]',
        password: 'abc123',
        // _csrf: this._csrf,  // I tried this too; no luck
      })
      .expect(200)  // if sails.config.security.csrf is enabled, status is 403
      .then(res => {
        // console.log('res:', res);  // this shows the correct header
        res.headers['set-cookie'].should.be.an('array');
        const hasSid = res.headers['set-cookie'].map(cookie => {
          const reSid = /^sails\.sid=[^;]+;\sPath=\/;(?:\sExpires=[^;]+GMT;)?\sHttpOnly$/;
          return reSid.test(cookie);
        });
        hasSid.should.include.members([true]);
      });
    });

  });

});

I'm running node v8.11.3, sails v1.0.2, mocha v5.2.0, supertest v3.1.0, chai v4.1.2

FYI, here's the request made by Postman that worked fine (CSRF token was copied by hand by a previous Postman request to GET /login):

PUT /api/v1/entrance/login HTTP/1.1
Host: localhost:1337
x-csrf-token: mjWXQTa2-RFEHu78Tr-JGJwhWeryKGRJI4S8
Cache-Control: no-cache
Postman-Token: e3d920fe-6178-4642-80e4-8005b477fd98

{"emailAddress": "[email protected]", "password":"abc123"}

Solution

  • Got it! I thought I was meant to get the session ID from the set-cookie header after logging in. Instead, I'm meant to capture both the CSRF token and session ID while still logged out, then submit email & password, then use the token and ID in subsequent requests. I missed this detail in Postman because I failed to notice the cookie that was persisting between requests.

    Here's the fixed test file (now works with CSRF protections enabled):

    /**
     * /test/integration/controllers/entrance/login.test.js
     */
    
    'use strict';
    
    const supertest = require('supertest');  // also tried
    
    describe('Entrance controllers', () => {
    
      describe('/api/v1/entrance/login', () => {
    
        before(() => {
          this._url = '/api/v1/entrance/login';
    
          return supertest(sails.hooks.http.app).get('/login')
          .then(getRes => {
            const reTokenCapture = /_csrf:\s*unescape\('([^']+)'\)/;
            const foundToken = reTokenCapture.exec(getRes.text);
            this._csrf = sails.config.security.csrf ? foundToken[1] : '';
            this._cookie = getRes.headers['set-cookie'].join('; ');
          });
    
        });
    
        it('should accept the session ID & CSRF token procured by GET /login', () => {
          return supertest(sails.hooks.http.app)
          .put(this._url)
          .set('Cookie', this._cookie)
          .set('X-CSRF-Token', this._csrf)
          .send({
            emailAddress: '[email protected]',
            password: 'abc123',
          })
          .expect(200);
        });
    
        it('should reject requests without a CSRF token', () => {
          return supertest(sails.hooks.http.app)
          .put(this._url)
          .set('Cookie', this._cookie)
          .expect(403);
        });
    
        it('should reject requests without a session cookie', () => {
          return supertest(sails.hooks.http.app)
          .put(this._url)
          .set('Cookie', '')
          .set('x-csrf-token', this._csrf)
          .expect(403);
        });
    
        it('should reject requests with invalid tokens', () => {
          return supertest(sails.hooks.http.app)
          .put(this._url)
          .set('Cookie', 'sails.sid=foo; Path=/; HttpOnly')
          .set('X-CSRF-Token', 'foo')
          .send({
            emailAddress: '[email protected]',
            password: 'abc123',
          })
          .expect(403);
        });
    
        it('should reject requests with invalid credentionals', () => {
          return supertest(sails.hooks.http.app)
          .put(this._url)
          .set('Cookie', this._cookie)
          .set('X-CSRF-Token', this._csrf)
          .send({
            emailAddress: '[email protected]',
            password: 'password'
          })
          .expect(401);
        });
    
        it('should reject get requests', () => {
          return supertest(sails.hooks.http.app)
          .get(this._url)
          .set('Cookie', this._cookie)
          .set('X-CSRF-Token', this._csrf)
          .send({
            emailAddress: '[email protected]',
            password: 'abc123',
          })
          .expect(404);
        });
    
        it('should reject post requests', () => {
          return supertest(sails.hooks.http.app)
          .post(this._url)
          .set('Cookie', this._cookie)
          .set('X-CSRF-Token', this._csrf)
          .send({
            emailAddress: '[email protected]',
            password: 'abc123',
          })
          .expect(404);
        });
    
      });
    
    });