Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly handle Basic authentication #185

Merged
merged 3 commits into from
Nov 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"inherits": "2.0.3",
"pouchdb-ajax": "6.3.4",
"pouchdb-binary-utils": "6.3.4",
"pouchdb-promise": "6.3.4",
"pouchdb-utils": "6.3.4",
"url-join": "2.0.2",
Expand Down
104 changes: 104 additions & 0 deletions src/admins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { AuthError, getBaseUrl, getBasicAuthHeaders, getConfigUrl, wrapError } from './utils';

import ajaxCore from 'pouchdb-ajax';
import { assign, toPromise } from 'pouchdb-utils';

var getMembership = toPromise(function (opts, callback) {
var db = this;
if (typeof callback === 'undefined') {
callback = opts;
opts = {};
}

var url = getBaseUrl(db) + '/_membership';
var ajaxOpts = assign({
method: 'GET',
url: url,
headers: getBasicAuthHeaders(db),
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
});

var signUpAdmin = toPromise(function (username, password, opts, callback) {
var db = this;
if (typeof callback === 'undefined') {
callback = typeof opts === 'undefined' ? (typeof password === 'undefined' ?
username : password) : opts;
opts = {};
}
if (['http', 'https'].indexOf(db.type()) === -1) {
return callback(new AuthError('This plugin only works for the http/https adapter. ' +
'So you should use new PouchDB("http://mysite.com:5984/mydb") instead.'));
} else if (!username) {
return callback(new AuthError('You must provide a username'));
} else if (!password) {
return callback(new AuthError('You must provide a password'));
}

db.getMembership(opts, function (error, membership) {
var nodeName;
if (error) {
if (error.error !== 'illegal_database_name') {
return callback(error);
} else {
// Some couchdb-1.x-like server
nodeName = undefined;
}
} else {
// Some couchdb-2.x-like server
nodeName = membership.all_nodes[0];
}

var configUrl = getConfigUrl(db, nodeName);
var url = (opts.configUrl || configUrl) + '/admins/' + encodeURIComponent(username);
var ajaxOpts = assign({
method: 'PUT',
url: url,
processData: false,
headers: getBasicAuthHeaders(db),
body: '"' + password + '"',
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
});
});

var deleteAdmin = toPromise(function (username, opts, callback) {
var db = this;
if (typeof callback === 'undefined') {
callback = typeof opts === 'undefined' ? username : opts;
opts = {};
}
if (['http', 'https'].indexOf(db.type()) === -1) {
return callback(new AuthError('This plugin only works for the http/https adapter. ' +
'So you should use new PouchDB("http://mysite.com:5984/mydb") instead.'));
} else if (!username) {
return callback(new AuthError('You must provide a username'));
}

db.getMembership(opts, function (error, membership) {
var nodeName;
if (error) {
if (error.error !== 'illegal_database_name') {
return callback(error);
} else {
// Some couchdb-1.x-like server
nodeName = undefined;
}
} else {
// Some couchdb-2.x-like server
nodeName = membership.all_nodes[0];
}

var configUrl = getConfigUrl(db, nodeName);
var url = (opts.configUrl || configUrl) + '/admins/' + encodeURIComponent(username);
var ajaxOpts = assign({
method: 'DELETE',
url: url,
processData: false,
headers: getBasicAuthHeaders(db),
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
});
});

export { getMembership, deleteAdmin, signUpAdmin };
6 changes: 4 additions & 2 deletions src/authentication.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

import { AuthError, getSessionUrl, wrapError } from './utils';
import { AuthError, getBasicAuthHeaders, getSessionUrl, wrapError } from './utils';

import ajaxCore from 'pouchdb-ajax';
import { assign, toPromise } from 'pouchdb-utils';
Expand All @@ -24,7 +24,7 @@ var logIn = toPromise(function (username, password, opts, callback) {
var ajaxOpts = assign({
method: 'POST',
url: getSessionUrl(db),
headers: {'Content-Type': 'application/json'},
headers: assign({'Content-Type': 'application/json'}, getBasicAuthHeaders(db)),
body: {name: username, password: password},
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
Expand All @@ -39,6 +39,7 @@ var logOut = toPromise(function (opts, callback) {
var ajaxOpts = assign({
method: 'DELETE',
url: getSessionUrl(db),
headers: getBasicAuthHeaders(db),
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
});
Expand All @@ -54,6 +55,7 @@ var getSession = toPromise(function (opts, callback) {
var ajaxOpts = assign({
method: 'GET',
url: url,
headers: getBasicAuthHeaders(db),
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
});
Expand Down
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

import { deleteAdmin, getMembership, signUpAdmin } from "./admins";
import { getSession, logIn, logOut } from "./authentication";
import {
changePassword,
Expand All @@ -19,6 +20,10 @@ plugin.logout = logOut;
plugin.logOut = logOut;
plugin.getSession = getSession;

plugin.getMembership = getMembership;
plugin.signUpAdmin = signUpAdmin;
plugin.deleteAdmin = deleteAdmin;

plugin.getUsersDatabaseUrl = getUsersDatabaseUrl;
plugin.signup = signUp;
plugin.signUp = signUp;
Expand Down
7 changes: 6 additions & 1 deletion src/users.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

import { AuthError, getUsersUrl, wrapError } from './utils';
import { AuthError, getBasicAuthHeaders, getUsersUrl, wrapError } from './utils';

import Promise from 'pouchdb-promise';
import ajaxCore from 'pouchdb-ajax';
Expand Down Expand Up @@ -43,6 +43,7 @@ function updateUser(db, user, opts, callback) {
method: 'PUT',
url: url,
body: user,
headers: getBasicAuthHeaders(db),
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
}
Expand Down Expand Up @@ -89,6 +90,7 @@ var getUser = toPromise(function (username, opts, callback) {
var ajaxOpts = assign({
method: 'GET',
url: url + '/' + encodeURIComponent('org.couchdb.user:' + username),
headers: getBasicAuthHeaders(db),
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
});
Expand Down Expand Up @@ -137,6 +139,7 @@ var deleteUser = toPromise(function (username, opts, callback) {
var ajaxOpts = assign({
method: 'DELETE',
url: url,
headers: getBasicAuthHeaders(db),
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
});
Expand Down Expand Up @@ -169,6 +172,7 @@ var changePassword = toPromise(function (username, password, opts, callback) {
var ajaxOpts = assign({
method: 'PUT',
url: url,
headers: getBasicAuthHeaders(db),
body: user,
}, opts.ajax || {});
ajaxCore(ajaxOpts, wrapError(callback));
Expand All @@ -193,6 +197,7 @@ var changeUsername = toPromise(function (oldUsername, newUsername, opts, callbac
var updateOpts = assign({
method: 'PUT',
url: url,
headers: getBasicAuthHeaders(db),
body: user,
}, opts.ajax);
return ajax(updateOpts);
Expand Down
26 changes: 25 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import urlJoin from 'url-join';
import urlParse from 'url-parse';
import inherits from 'inherits';
import { btoa } from 'pouchdb-binary-utils';

function getBaseUrl(db) {
if (typeof db.getUrl === 'function') { // pouchdb pre-6.0.0
Expand All @@ -14,6 +15,10 @@ function getBaseUrl(db) {
}
}

function getConfigUrl(db, nodeName) {
return urlJoin(getBaseUrl(db), (nodeName ? '/_node/' + nodeName : '') + '/_config');
}

function getUsersUrl(db) {
return urlJoin(getBaseUrl(db), '/_users');
}
Expand All @@ -22,6 +27,17 @@ function getSessionUrl(db) {
return urlJoin(getBaseUrl(db), '/_session');
}

function getBasicAuthHeaders(db) {
var url = urlParse(db.name);
if (!url.auth) {
return {};
}

var str = url.username + ':' + url.password;
var token = btoa(unescape(encodeURIComponent(str)));
return {Authorization: 'Basic ' + token};
}

function wrapError(callback) {
// provide more helpful error message
return function (err, res) {
Expand All @@ -47,4 +63,12 @@ function AuthError(message) {

inherits(AuthError, Error);

export { AuthError, getSessionUrl, getUsersUrl, wrapError };
export {
AuthError,
getBaseUrl,
getBasicAuthHeaders,
getConfigUrl,
getSessionUrl,
getUsersUrl,
wrapError,
};
73 changes: 73 additions & 0 deletions test/test.admins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

var PouchDB = require('pouchdb-memory');
var Authentication = require('../lib');
PouchDB.plugin(Authentication);

var utils = require('./test-utils');
var chai = require('chai');
var should = chai.should();

describe('admins', function () {

var dbHost = 'http://localhost:5984';
var dbName = dbHost + '/testdb';

var db;

beforeEach(function () {
db = new PouchDB(dbName);
return utils.ensureUsersDatabaseExists(db);
});

afterEach(function () {
return db.logOut().then(function () {
return db.destroy();
});
});

it('Create and delete admin', function () {
return testCreateDeleteAdmin({});
});

it('Create and delete admin with config url', function () {
return db.getMembership().then(function (membership) {
// Some couchdb-2.x-like server
return membership.all_nodes[0];
}).catch(function () {
// Some couchdb-1.x-like server
return undefined;
}).then(function (nodeName) {
var opts = {
configUrl: dbHost + (nodeName ? '/_node/' + nodeName : '') + '/_config',
};

return testCreateDeleteAdmin(opts);
});
});

function testCreateDeleteAdmin(opts) {
return db.signUpAdmin('anna', 'secret', opts).then(function (res) {
should.exist(res);

return db.logIn('anna', 'secret').then(function (res) {
res.ok.should.equal(true);

return db.deleteAdmin('anna', opts).then(function (res) {
should.exist(res);

return db.logOut().then(function () {

return db.logIn('anna', 'secret').then(function () {
should.fail('shouldn\'t have worked');
}, function (err) {
should.exist(err);
err.error.should.equal('unauthorized');
err.reason.should.equal('Name or password is incorrect.');
});
});
});
});
});
}
});
Loading