UNPKG

@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
'use strict'; 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;