UNPKG

@perfood/couch-auth

Version:

Easy and secure authentication for CouchDB/Cloudant. Based on SuperLogin, updated and rewritten in Typescript.

277 lines (276 loc) 10.7 kB
'use strict'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DBAuth = void 0; const seed_1 = __importDefault(require("../design/seed")); const util_1 = require("../util"); const couchdb_1 = require("./couchdb"); class DBAuth { constructor(config, userDB, couchServer, couchAuthDB) { this.config = config; this.userDB = userDB; this.couchServer = couchServer; this.adapter = new couchdb_1.CouchAdapter(couchAuthDB, this.couchServer, this.config); } storeKey(username, user_uid, key, password, expires, roles, provider) { return this.adapter.storeKey(username, user_uid, key, password, expires, roles, provider); } /** * Step 1) During deauthorization: Removes the keys of format * org.couchdb.user:TOKEN from the `_users` - database, if they are present. * If this step fails, the user hasn't been deauthorized! */ removeKeys(keys) { return this.adapter.removeKeys(keys); } retrieveKey(key) { return this.adapter.retrieveKey(key); } extendKey(key, newExpiration) { return this.adapter.extendKey(key, newExpiration); } /** generates a random token and password */ getApiKey() { let token = (0, util_1.URLSafeUUID)(); // Make sure our token doesn't start with illegal characters while (token[0] === '_' || token[0] === '-') { token = (0, util_1.URLSafeUUID)(); } return { key: token, password: (0, util_1.URLSafeUUID)() }; } /** * Removes the affected from the `_users` db and from the `_security` of the * user's personal DBs, returning the modified `sl-users` doc * * - 'all' -> logs out all sessions * - 'other' -> logout all sessions except for 'currentSession' * - 'expired' -> only logs out expired sessions */ async logoutUserSessions(userDoc, op, currentSession) { let sessionsToEnd; if (op === 'expired') { sessionsToEnd = (0, util_1.getExpiredSessions)(userDoc, Date.now()); } else { sessionsToEnd = (0, util_1.getSessions)(userDoc); if (op === 'other' && currentSession) { sessionsToEnd = sessionsToEnd.filter(s => s !== currentSession); } } if (sessionsToEnd.length) { // 1.) Remove the keys from our couchDB auth database. Must happen first. await this.removeKeys(sessionsToEnd); // 2.) Deauthorize keys from each personal database await this.deauthorizeUser(userDoc, sessionsToEnd); sessionsToEnd.forEach(session => { delete userDoc.session[session]; }); if (Object.keys(userDoc.session).length === 0) { delete userDoc.session; } userDoc.inactiveSessions = [ ...(userDoc.inactiveSessions ?? []), ...sessionsToEnd ]; } return userDoc; } async authorizeKeys(db, keys) { return this.adapter.authorizeKeys(db, keys); } /** removes the keys from the security doc of the db */ deauthorizeKeys(db, keys) { return this.adapter.deauthorizeKeys(db, keys); } authorizeUserSessions(personalDBs, sessionKeys) { const promises = []; Object.keys(personalDBs).forEach(personalDB => { const db = this.couchServer.use(personalDB); promises.push(this.authorizeKeys(db, (0, util_1.toArray)(sessionKeys))); }); return Promise.all(promises); } async addUserDB(userDoc, dbName, designDocs, type, adminRoles, memberRoles, partitioned) { const promises = []; adminRoles = adminRoles || []; memberRoles = memberRoles || []; partitioned = partitioned || false; // Create and the database and seed it if a designDoc is specified const prefix = this.config.userDBs.privatePrefix ? this.config.userDBs.privatePrefix + '_' : ''; // new in 2.0: use uuid instead of username const finalDBName = type === 'shared' ? dbName : prefix + dbName + '$' + userDoc._id; await this.createDB(finalDBName, partitioned); const newDB = this.couchServer.db.use(finalDBName); await this.adapter.initSecurity(newDB, adminRoles, memberRoles); // Seed the design docs if (designDocs && designDocs instanceof Array) { designDocs.forEach(ddName => { const dDoc = this.getDesignDoc(ddName); if (dDoc) { promises.push((0, seed_1.default)(newDB, dDoc)); } else { console.warn('Failed to locate design doc: ' + ddName); } }); } // Authorize the user's existing DB keys to access the new database const keysToAuthorize = []; if (userDoc.session) { for (const key in userDoc.session) { if (userDoc.session.hasOwnProperty(key) && userDoc.session[key].expires > Date.now()) { keysToAuthorize.push(key); } } } if (keysToAuthorize.length > 0) { promises.push(this.authorizeKeys(newDB, keysToAuthorize)); } await Promise.all(promises); return finalDBName; } /** * Checks from the superlogin-userDB which keys are expired and removes them * from: * 1. the CouchDB authentication-DB (`_users`) * 2. the security-doc of the user's personal DB * 3. the user's doc in the superlogin-DB * * @returns an array of removed keys * @throws This method can fail due to Connection/ CouchDB-Problems. */ async removeExpiredKeys() { const alreadyProcessedUsers = new Set(); let revokedSessions = []; // query a list of expired keys by user const results = await this.userDB.view('auth', 'expiredKeys', { endkey: Date.now(), include_docs: true }); // clean up expired session for each user in the results for (const row of results.rows) { const val = row.value; const userId = val.user; if (alreadyProcessedUsers.has(userId)) { continue; } const sessionsBefore = Object.keys(row.doc.session ?? {}); const userDoc = await this.logoutUserSessions(row.doc, 'expired'); await this.userDB.insert(userDoc); revokedSessions = revokedSessions.concat(sessionsBefore.filter(s => !userDoc.session || !userDoc.session[s])); alreadyProcessedUsers.add(userId); } return revokedSessions; } /** deauthenticates the keys from the user's personal DB */ deauthorizeUser(userDoc, keys) { const promises = []; // If keys is not specified we will deauthorize all of the users sessions if (!keys) { keys = (0, util_1.getSessions)(userDoc); } keys = (0, util_1.toArray)(keys); if (userDoc.personalDBs && typeof userDoc.personalDBs === 'object') { Object.keys(userDoc.personalDBs).forEach(personalDB => { const db = this.couchServer.use(personalDB); promises.push(this.deauthorizeKeys(db, keys)); }); return Promise.all(promises); } else { return Promise.resolve(false); } } getDesignDoc(docName) { if (!docName) { return null; } let designDoc; let designDocDir = this.config.userDBs.designDocDir; if (!designDocDir) { designDocDir = __dirname; } try { designDoc = require(designDocDir + '/' + docName); } catch (err) { console.warn('Design doc: ' + designDocDir + '/' + docName + ' not found.'); designDoc = null; } return designDoc; } getDBConfig(dbName, type) { const dbConfig = { name: dbName }; dbConfig.adminRoles = this.config.userDBs?.defaultSecurityRoles?.admins || []; dbConfig.memberRoles = this.config.userDBs?.defaultSecurityRoles?.members || []; const dbConfigRef = this.config.userDBs?.model?.[dbName]; if (dbConfigRef) { dbConfig.designDocs = dbConfigRef.designDocs || []; dbConfig.type = type || dbConfigRef.type || 'private'; dbConfig.partitioned = dbConfigRef.partitioned || false; const dbAdminRoles = dbConfigRef.adminRoles; const dbMemberRoles = dbConfigRef.memberRoles; if (dbAdminRoles && dbAdminRoles instanceof Array) { dbAdminRoles.forEach(role => { if (role && dbConfig.adminRoles.indexOf(role) === -1) { dbConfig.adminRoles.push(role); } }); } if (dbMemberRoles && dbMemberRoles instanceof Array) { dbMemberRoles.forEach(role => { if (role && dbConfig.memberRoles.indexOf(role) === -1) { dbConfig.memberRoles.push(role); } }); } } else if (this.config.userDBs.model?._default) { // Only add the default design doc to a private database if (!type || type === 'private') { dbConfig.designDocs = this.config.userDBs.model._default.designDocs || []; } else { dbConfig.designDocs = []; } dbConfig.partitioned = this.config.userDBs.model._default.partitioned || false; dbConfig.type = type || 'private'; } else { dbConfig.partitioned = false; dbConfig.type = type || 'private'; } return dbConfig; } async createDB(dbName, partitioned) { partitioned = partitioned || false; try { await this.couchServer.db.create(dbName, { partitioned: partitioned }); } catch (err) { if (err.statusCode === 412) { return false; // already exists } throw err; } return true; } removeDB(dbName) { return this.couchServer.db.destroy(dbName); } } exports.DBAuth = DBAuth;