SylowTech/sylow

View on GitHub
tests/auth.test.js

Summary

Maintainability
F
6 days
Test Coverage
import request from 'supertest-as-promised';
import httpStatus from 'http-status';
import cheerio from 'cheerio';
import chai, { expect } from 'chai';

import app from '../index';
import config from '../config/config';
import AccessToken from '../server/models/accessToken.model';
import Entity from '../server/models/entity.model';
import Client from '../server/models/client.model';

chai.config.includeStack = true;

const badUsernameCredentials = {
  username: 'react',
  passwordHash: 'xxxxxx'
};

const badPasswordCredentials = {
  username: 'testuser',
  passwordHash: 'xxxxxx'
};

const notAdminCredentials = {
  username: 'notadmin',
  passwordHash: 'xxxxxx'
};

const testClient = {
  clientId: 'sylowTestClient',
  clientSecret: 'sylowTestSecret',
  clientName: 'Sylow Test Client',
  deviceType: 'other',
  redirectUri: 'http://localhost/cb',
};

const testEntity = {
  entityName: 'testuser@testdomain.xyz',
  passwordHash: '33f1ba50d3acdfe04fadbfcdc50edd84a3af0f9d377872003eaedbb68f8e6d7146e87c35e5f3338341d91b84c1371a6a9db054c4104797e99848f4d2d8a2b91e',
  passwordSalt: '694658b93aa9c2f245cca37da3b4d7cc',
  keypair: {
    public: 'xxxxx'
  },
  authoritative: true,
  admin: true
};

const notAdminEntity = Object.assign(
  {}, testEntity, { entityName: 'notadmin@testdomain.xyz', passwordHash: 'xxxxxx', admin: false }
);

function cleanup() {
  if (config.env !== 'test') {
    return Promise.reject('Not in a test environment');
  }
  return Promise.all([
    AccessToken.remove(),
    Entity.remove(),
    Client.remove()
  ]).then(() =>
    Entity.create([testEntity, notAdminEntity])
      .then(([entity1, entity2]) => {
        testEntity.username = entity1.username;
        notAdminEntity.username = entity2.username;
        const newClient = new Client(testClient);
        return newClient.save();
      })
  );
}

describe('## Authentification', () => {
  const localAgent = request.agent(app);
  const oauthAgent = request.agent(app);
  const implicitAgent = request.agent(app);

  describe('# OAuth2: Authentication Code Flow', () => {
    let testTransactionId;
    let testAccessCode;
    let testAuthToken;

    before('Clean up test data', cleanup);

    it('should fail to find a client with this ID', (done) => {
      oauthAgent.get('/api/auth/authorize')
        .query({
          client_id: 'noNameClient',
          response_type: 'code',
          redirect_uri: 'http://localhost/cb'
        })
        .auth(testEntity.username, testEntity.passwordHash)
        .expect(httpStatus.FORBIDDEN)
        .then((res) => {
          expect(res.body.message).to.equal('Forbidden');
          done();
        })
        .catch(done);
    });

    it('should reject client because of hijacked redirect uri', (done) => {
      oauthAgent.get('/api/auth/authorize')
        .query({
          client_id: 'sylowTestClient',
          response_type: 'code',
          redirect_uri: 'http://testdomain.blah/cb'
        })
        .auth(testEntity.username, testEntity.passwordHash)
        .expect(httpStatus.FORBIDDEN)
        .then((res) => {
          expect(res.body.message).to.equal('Forbidden');
          done();
        })
        .catch(done);
    });

    it('should be able to get authorization page', (done) => {
      oauthAgent.get('/api/auth/authorize')
        .query({
          client_id: 'sylowTestClient',
          response_type: 'code',
          redirect_uri: 'http://localhost/cb'
        })
        .auth(testEntity.username, testEntity.passwordHash)
        .expect(httpStatus.OK)
        .then((res) => {
          const html = cheerio.load(res.text);
          // cookieId = req.headers['set-cookie'][0]
          testTransactionId = html('input[type="hidden"]').val();
          done();
        })
        .catch(done);
    });

    it('should be able to retrieve an authorization code', (done) => {
      oauthAgent.post('/api/auth/decision')
        .auth(testEntity.username, testEntity.passwordHash)
        .type('form')
        .send({
          transaction_id: testTransactionId
        })
        .expect(httpStatus.FOUND)
        .then((res) => {
          testAccessCode = res.text.split('code=')[1];
          done();
        })
        .catch(done);
    });

    it('should fail to exchange an invalid authorization code', (done) => {
      request(app)
        .post('/api/auth/token')
        .auth(testClient.clientId, testClient.clientSecret)
        .send({
          code: 'xxxxxx',
          client_id: testClient.clientId,
          client_secret: testClient.clientSecret,
          grant_type: 'authorization_code',
          redirect_uri: testClient.redirectUri
        })
        .type('form')
        .expect(httpStatus.FORBIDDEN)
        .then((res) => {
          expect(res.body.error_description).to.equal('Invalid authorization code');
          done();
        })
        .catch(done);
    });

    it('should fail to exchange an authorization code with a bad redirect uri', (done) => {
      request(app)
        .post('/api/auth/token')
        .auth(testClient.clientId, testClient.clientSecret)
        .send({
          code: testAccessCode,
          client_id: testClient.clientId,
          client_secret: testClient.clientSecret,
          grant_type: 'authorization_code',
          redirect_uri: 'http://testdomain.blah/cb'
        })
        .type('form')
        .expect(httpStatus.FORBIDDEN)
        .then((res) => {
          expect(res.body.error_description).to.equal('Invalid authorization code');
          done();
        })
        .catch(done);
    });

    it('should fail to exchange an invalid client ID', (done) => {
      request(app)
        .post('/api/auth/token')
        .auth(testClient.clientId, testClient.clientSecret)
        .send({
          code: testAccessCode,
          client_id: 'xxxxxx',
          client_secret: testClient.clientSecret,
          grant_type: 'authorization_code',
          redirect_uri: testClient.redirectUri
        })
        .type('form')
        .expect(httpStatus.UNAUTHORIZED)
        .then(() => done())
        .catch(done);
    });

    it('should be able to use the code to retrieve a token', (done) => {
      request(app)
        .post('/api/auth/token')
        .auth(testClient.clientId, testClient.clientSecret)
        .send({
          code: testAccessCode,
          client_id: testClient.clientId,
          client_secret: testClient.clientSecret,
          grant_type: 'authorization_code',
          redirect_uri: testClient.redirectUri
        })
        .type('form')
        .expect(httpStatus.OK)
        .then((res) => {
          testAuthToken = res.body.access_token;
          done();
        })
        .catch(done);
    });

    it('should be able to use the token to access a protected endpoint', (done) => {
      request(app)
        .get('/api/auth/random-number')
        .set('Authorization', `Bearer ${testAuthToken}`)
        .expect(httpStatus.OK)
        .then((res) => {
          expect(res.body.num).to.be.a('number');
          done();
        })
        .catch(done);
    });

    it('should automatically give an access code after first authorize', (done) => {
      oauthAgent.get('/api/auth/authorize')
        .query({
          client_id: 'sylowTestClient',
          response_type: 'code',
          redirect_uri: 'http://localhost/cb'
        })
        .auth(testEntity.username, testEntity.passwordHash)
        .expect(httpStatus.FOUND)
        .then((res) => {
          testAccessCode = res.headers.location.split('code=')[1];
          done();
        })
        .catch(done);
    });

    it('should fail to get random number because of missing Authorization', (done) => {
      request(app)
        .get('/api/auth/random-number')
        .expect(httpStatus.UNAUTHORIZED)
        .then(() => done())
        .catch(done);
    });

    it('should fail to get random number because of wrong token', (done) => {
      request(app)
        .get('/api/auth/random-number')
        .set('Authorization', 'Bearer inValidToken')
        .expect(httpStatus.UNAUTHORIZED)
        .then(() => done())
        .catch(done);
    });
  });

  describe('# OAuth2: Implicit Flow', () => {
    let testTransactionId;
    let testAuthToken;

    before('Clean up test data', cleanup);

    it('should be able to get authorization page', (done) => {
      implicitAgent.get('/api/auth/authorize')
        .query({
          client_id: 'sylowTestClient',
          response_type: 'token',
          redirect_uri: 'http://localhost/cb'
        })
        .auth(testEntity.username, testEntity.passwordHash)
        .expect(httpStatus.OK)
        .then((res) => {
          const html = cheerio.load(res.text);
          // cookieId = req.headers['set-cookie'][0]
          testTransactionId = html('input[type="hidden"]').val();
          done();
        })
        .catch(done);
    });

    it('should be able to retrieve an authorization code', (done) => {
      implicitAgent.post('/api/auth/decision')
        .auth(testEntity.username, testEntity.passwordHash)
        .type('form')
        .send({
          transaction_id: testTransactionId
        })
        .expect(httpStatus.FOUND)
        .then((res) => {
          testAuthToken = res.text.split('token=')[1].split('&')[0];
          done();
        })
        .catch(done);
    });

    it('should be able to use the token to access a protected endpoint', (done) => {
      request(app)
        .get('/api/auth/random-number')
        .set('Authorization', `Bearer ${testAuthToken}`)
        .expect(httpStatus.OK)
        .then((res) => {
          expect(res.body.num).to.be.a('number');
          done();
        })
        .catch(done);
    });
  });

  describe('# GET /api/auth/salt', () => {
    it('should return a random salt if no user found', (done) => {
      request(app)
        .get('/api/auth/salt')
        .query({ username: 'xxxxxx' })
        .expect(httpStatus.OK)
        .then((res) => {
          expect(res.body.salt).to.be.a('string');
          done();
        })
        .catch(done);
    });

    it('should return correct password salt', (done) => {
      request(app)
        .get('/api/auth/salt')
        .query({ username: testEntity.username })
        .expect(httpStatus.OK)
        .then((res) => {
          expect(res.body.salt).to.equal('694658b93aa9c2f245cca37da3b4d7cc');
          done();
        })
        .catch(done);
    });
  });

  describe('# POST /login', () => {
    it('should fail to login with a bad username', (done) => {
      localAgent.post('/login')
        .redirects(1)
        .send(badUsernameCredentials)
        .type('form')
        .expect(httpStatus.OK)
        .then((res) => {
          const html = cheerio.load(res.text);
          const err = html('.ui.error.message p').html();
          expect(err).to.equal('Invalid username or password.');
          done();
        })
        .catch(done);
    });

    it('should fail to login with bad password', (done) => {
      localAgent.post('/login')
        .redirects(1)
        .send(badPasswordCredentials)
        .type('form')
        .expect(httpStatus.OK)
        .then((res) => {
          const html = cheerio.load(res.text);
          const err = html('.ui.error.message p').html();
          expect(err).to.equal('Invalid username or password.');
          done();
        })
        .catch(done);
    });

    it('should fail to login because of a lack of admin status', (done) => {
      localAgent.post('/login')
        .redirects(1)
        .send(notAdminCredentials)
        .type('form')
        .expect(httpStatus.FORBIDDEN)
        .then((res) => {
          const html = cheerio.load(res.text);
          const err = html('.ui.negative.message p').html();
          expect(err).to.equal('Entity does not have sufficient rights');
          done();
        })
        .catch(done);
    });

    it('should successfully login', (done) => {
      localAgent.post('/login')
        .send({
          username: testEntity.username,
          passwordHash: testEntity.passwordHash
        })
        .type('form')
        .expect(httpStatus.FOUND)
        .then((res) => {
          expect(res.headers.location).to.equal('/');
          done();
        })
        .catch(done);
    });
  });

  describe('# POST /logout', () => {
    it('should successfully logout', (done) => {
      localAgent.get('/logout')
        .redirects(1)
        .expect(httpStatus.FOUND)
        .then((res) => {
          expect(res.headers.location).to.equal('/login');
          done();
        })
        .catch(done);
    });
  });
});