@perfood/couch-auth
Version:
Easy and secure authentication for CouchDB/Cloudant. Based on SuperLogin, updated and rewritten in Typescript.
210 lines (209 loc) • 6.98 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CouchAdapter = void 0;
const util_1 = require("../util");
const session_hashing_1 = require("../session-hashing");
const userPrefix = 'org.couchdb.user:';
class CouchAdapter {
constructor(couchAuthDB, couch, config, session = new session_hashing_1.SessionHashing(config)) {
this.couchAuthDB = couchAuthDB;
this.couch = couch;
this.config = config;
this.session = session;
this.couchAuthOnCloudant = false;
if (this.config?.dbServer.couchAuthOnCloudant) {
this.couchAuthOnCloudant = true;
}
}
/**
* stores a CouchDbAuthDoc with the passed information. Expects the `username`
* (i.e. `key`) and the UUID.
*/
async storeKey(username, user_uid, key, password, expires, roles, provider) {
if (roles instanceof Array) {
// Clone roles to not overwrite original
roles = roles.slice(0);
}
else {
roles = [];
}
roles.unshift('user:' + username);
const newKey = {
_id: userPrefix + key,
type: 'user',
name: key,
user_uid: user_uid,
user_id: username,
expires: expires,
roles: roles,
provider: provider,
...(await this.session.hashSessionPassword(password))
};
try {
await this.couchAuthDB.insert(newKey);
}
catch (e) {
if (e.statusCode !== 409) {
// not "409 Conflict"
throw e;
}
let doc = await this.couchAuthDB.get(newKey._id);
if (doc.user_uid !== newKey.user_uid) {
throw e;
}
newKey._rev = doc._rev;
await this.couchAuthDB.insert(newKey);
}
newKey._id = key;
return newKey;
}
async extendKey(key, newExpiration) {
const token = await this.retrieveKey(key);
token.expires = newExpiration;
return await this.couchAuthDB.insert(token);
}
/**
* fetches the document from the couchAuthDB, if it's present. Throws an error otherwise.
*/
retrieveKey(key) {
return this.couchAuthDB.get(userPrefix + key);
}
/**
* Removes the keys of format `org.couchdb.user:TOKEN` from the `_users` - database, if they are present.
*/
async removeKeys(keys) {
const keylist = [];
// Transform the list to contain the CouchDB _user ids
(0, util_1.toArray)(keys).forEach(key => {
keylist.push(userPrefix + key);
});
const toDelete = [];
// success: have row.doc, but possibly row.doc = null and row.value.deleted = true
// failure: have row.key and row.error
const keyDocs = await this.couchAuthDB.fetch({ keys: keylist });
keyDocs.rows.forEach(row => {
if (!('doc' in row)) {
console.info('removeKeys() - could not retrieve: ' + row.key);
}
else if (!('deleted' in row.value)) {
const deletion = {
_id: row.doc._id,
_rev: row.doc._rev,
_deleted: true
};
toDelete.push(deletion);
}
});
if (toDelete.length) {
return this.couchAuthDB.bulk({ docs: toDelete });
}
else {
return false;
}
}
/**
* initializes the `_security` doc with the passed roles
* @param {import('nano').DocumentScope} db
* @param {string[]} adminRoles
* @param {string[]} memberRoles
*/
async initSecurity(db, adminRoles, memberRoles) {
let changes = false;
const secDoc = await (0, util_1.getSecurityDoc)(this.couch, db);
if (!secDoc.admins) {
secDoc.admins = { names: [], roles: [] };
}
if (!secDoc.admins.roles) {
secDoc.admins.roles = [];
}
if (!secDoc.members) {
secDoc.members = { names: [], roles: [] };
}
if (!secDoc.members.roles) {
secDoc.admins.roles = [];
}
adminRoles.forEach(function (role) {
if (secDoc.admins.roles.indexOf(role) === -1) {
changes = true;
secDoc.admins.roles.push(role);
}
});
memberRoles.forEach(function (role) {
if (secDoc.members.roles.indexOf(role) === -1) {
changes = true;
secDoc.members.roles.push(role);
}
});
if (this.couchAuthOnCloudant && !secDoc.couchdb_auth_only) {
changes = true;
secDoc.couchdb_auth_only = true;
}
if (changes) {
return (0, util_1.putSecurityDoc)(this.couch, db, secDoc);
}
else {
return false;
}
}
/**
* authorises the passed keys to access the db
*/
async authorizeKeys(db, keys) {
// Check if keys is an object and convert it to an array
if (typeof keys === 'object' && !(keys instanceof Array)) {
const keysArr = [];
Object.keys(keys).forEach(theKey => {
keysArr.push(theKey);
});
keys = keysArr;
}
// Convert keys to an array if it is just a string
keys = (0, util_1.toArray)(keys);
const secDoc = await (0, util_1.getSecurityDoc)(this.couch, db);
if (!secDoc.members) {
secDoc.members = { names: [], roles: [] };
}
if (!secDoc.members.names) {
secDoc.members.names = [];
}
let changes = false;
keys.forEach(key => {
const index = secDoc.members.names.indexOf(key);
if (index === -1) {
secDoc.members.names.push(key);
changes = true;
}
});
if (changes) {
return await (0, util_1.putSecurityDoc)(this.couch, db, secDoc);
}
else {
return false;
}
}
/**
* removes the keys from the security doc of the db
*/
async deauthorizeKeys(db, keys) {
const keysArr = (0, util_1.toArray)(keys);
const secDoc = await (0, util_1.getSecurityDoc)(this.couch, db);
if (!secDoc.members || !secDoc.members.names) {
return false;
}
let changes = false;
keysArr.forEach(key => {
const index = secDoc.members.names.indexOf(key);
if (index > -1) {
secDoc.members.names.splice(index, 1);
changes = true;
}
});
if (changes) {
return await (0, util_1.putSecurityDoc)(this.couch, db, secDoc);
}
else {
return false;
}
}
}
exports.CouchAdapter = CouchAdapter;