-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: modular server: implements OAuth
- Loading branch information
1 parent
e7def12
commit fbe6c00
Showing
2 changed files
with
188 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* eslint-env mocha, chai, node */ | ||
|
||
const Armadietto = require('../../lib/armadietto'); | ||
const { shouldImplementOAuth } = require('../oauth.spec'); | ||
|
||
const store = { | ||
authorize (clientId, username, permissions) { | ||
return 'a_token'; | ||
}, | ||
authenticate (params) { | ||
} | ||
}; | ||
|
||
describe('OAuth (monolithic)', function () { | ||
before(function () { | ||
this.store = store; | ||
this.app = new Armadietto({ | ||
bare: true, | ||
store, | ||
http: { }, | ||
logging: { stdout: [], log_dir: './test-log', log_files: ['debug'] } | ||
}); | ||
}); | ||
|
||
shouldImplementOAuth(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* eslint-env mocha, chai, node */ | ||
/* eslint-disable no-unused-expressions */ | ||
const chai = require('chai'); | ||
const expect = chai.expect; | ||
const chaiHttp = require('chai-http'); | ||
chai.use(chaiHttp); | ||
const spies = require('chai-spies'); | ||
chai.use(spies); | ||
|
||
const sandbox = chai.spy.sandbox(); | ||
|
||
async function post (app, url, params) { | ||
return chai.request(app).post(url).type('form').send(params).redirects(0); | ||
} | ||
|
||
exports.shouldImplementOAuth = function () { | ||
describe('with invalid client input', function () { | ||
beforeEach(function () { | ||
this.auth_params = { | ||
username: 'zebcoe', | ||
password: 'locog', | ||
client_id: 'the_client_id', | ||
redirect_uri: 'http://example.com/cb', | ||
response_type: 'token', | ||
scope: 'the_scope' | ||
// state: 'the_state' | ||
}; | ||
|
||
sandbox.on(this.store, ['authorize', 'authenticate']); | ||
}); | ||
|
||
afterEach(function () { | ||
sandbox.restore(); | ||
}); | ||
|
||
it('returns an error if redirect_uri is missing', async function () { | ||
delete this.auth_params.redirect_uri; | ||
const res = await chai.request(this.app).get('/oauth/me').query(this.auth_params); | ||
expect(res).to.have.status(400); | ||
expect(res.text).to.have.been.equal('error=invalid_request&error_description=Required%20parameter%20%22redirect_uri%22%20is%20missing'); | ||
}); | ||
|
||
it('returns an error if client_id is missing', async function () { | ||
delete this.auth_params.client_id; | ||
const res = await chai.request(this.app).get('/oauth/me').query(this.auth_params); | ||
expect(res).to.redirectTo('http://example.com/cb#error=invalid_request&error_description=Required%20parameter%20%22client_id%22%20is%20missing'); | ||
}); | ||
|
||
it('returns an error if response_type is missing', async function () { | ||
delete this.auth_params.response_type; | ||
const res = await chai.request(this.app).get('/oauth/me').query(this.auth_params); | ||
expect(res).to.redirectTo('http://example.com/cb#error=invalid_request&error_description=Required%20parameter%20%22response_type%22%20is%20missing'); | ||
}); | ||
|
||
it('returns an error if response_type is not recognized', async function () { | ||
this.auth_params.response_type = 'wrong'; | ||
const res = await chai.request(this.app).get('/oauth/me').query(this.auth_params); | ||
expect(res).to.redirectTo('http://example.com/cb#error=unsupported_response_type&error_description=Response%20type%20%22wrong%22%20is%20not%20supported'); | ||
}); | ||
|
||
it('returns an error if scope is missing', async function () { | ||
delete this.auth_params.scope; | ||
const res = await chai.request(this.app).get('/oauth/me').query(this.auth_params); | ||
expect(res).to.redirectTo('http://example.com/cb#error=invalid_scope&error_description=Parameter%20%22scope%22%20is%20invalid'); | ||
}); | ||
|
||
it('returns an error if username is missing', async function () { | ||
delete this.auth_params.username; | ||
const res = await post(this.app, '/oauth', this.auth_params); | ||
expect(res).to.have.status(400); | ||
}); | ||
}); | ||
|
||
describe('with valid login credentials', async function () { | ||
beforeEach(function () { | ||
this.auth_params = { | ||
username: 'zebcoe', | ||
password: 'locog', | ||
client_id: 'the_client_id', | ||
redirect_uri: 'http://example.com/cb', | ||
response_type: 'token', | ||
scope: 'the_scope', | ||
state: 'the_state' | ||
}; | ||
|
||
sandbox.on(this.store, ['authorize', 'authenticate']); | ||
}); | ||
|
||
afterEach(function () { | ||
sandbox.restore(); | ||
}); | ||
|
||
describe('without explicit read/write permissions', async function () { | ||
it('authorizes the client to read and write', async function () { | ||
await post(this.app, '/oauth', this.auth_params); | ||
expect(this.store.authorize).to.be.called.with('the_client_id', 'zebcoe', { the_scope: ['r', 'w'] }); | ||
}); | ||
}); | ||
|
||
describe('with explicit read permission', async function () { | ||
it('authorizes the client to read', async function () { | ||
this.auth_params.scope = 'the_scope:r'; | ||
await post(this.app, '/oauth', this.auth_params); | ||
expect(this.store.authorize).to.be.called.with('the_client_id', 'zebcoe', { the_scope: ['r'] }); | ||
}); | ||
}); | ||
|
||
describe('with explicit read/write permission', async function () { | ||
it('authorizes the client to read and write', async function () { | ||
this.auth_params.scope = 'the_scope:rw'; | ||
await post(this.app, '/oauth', this.auth_params); | ||
expect(this.store.authorize).to.be.called.with('the_client_id', 'zebcoe', { the_scope: ['r', 'w'] }); | ||
}); | ||
}); | ||
|
||
it('redirects with an access token', async function () { | ||
const res = await post(this.app, '/oauth', this.auth_params); | ||
expect(res).to.redirectTo('http://example.com/cb#access_token=a_token&token_type=bearer&state=the_state'); | ||
}); | ||
}); | ||
|
||
describe('with invalid login credentials', async function () { | ||
beforeEach(function () { | ||
this.auth_params = { | ||
username: 'zebcoe', | ||
password: 'locog', | ||
client_id: 'the_client_id', | ||
redirect_uri: 'http://example.com/cb', | ||
response_type: 'token', | ||
scope: 'the_scope', | ||
state: 'the_state' | ||
}; | ||
|
||
sandbox.on(this.store, ['authorize', 'authenticate']); | ||
}); | ||
|
||
afterEach(function () { | ||
sandbox.restore(); | ||
}); | ||
|
||
it('does not authorize the client', async function () { | ||
this.store.authenticate = (params) => { | ||
throw new Error(); | ||
}; | ||
await post(this.app, '/oauth', this.auth_params); | ||
expect(this.store.authorize).to.be.called.exactly(0); | ||
}); | ||
|
||
it('returns a 401 response with the login form', async function () { | ||
this.store.authenticate = (params) => { | ||
throw new Error(); | ||
}; | ||
const res = await post(this.app, '/oauth', this.auth_params); | ||
expect(res).to.have.status(401); | ||
expect(res).to.have.header('Content-Type', 'text/html; charset=utf8'); | ||
expect(res).to.have.header('Content-Security-Policy', /sandbox.*default-src 'self'/); | ||
expect(res).to.have.header('Referrer-Policy', 'no-referrer'); | ||
expect(res).to.have.header('X-Content-Type-Options', 'nosniff'); | ||
expect(res.text).to.contain('application <em>the_client_id</em> hosted'); | ||
}); | ||
}); | ||
}; |