Skip to main content

Chatty Couchapp Tutorial

Tutorial on building a secure web application using Apache CouchDB as unique backend

Introduction

Chatty is a Couchapp tutorial providing a flexible and general purpose approach, to develop an instant-messaging Single Page Application, exclusively built with Javascript, HTML and CSS, using Apache CouchDB as web, app and db server, to deliver all presentation, business logic and data layers typical of multi-tier architectures.

Chatty features:

  1. an administration user interface
  2. a user interface for chatting, accessible by regular users only
  3. a secure server side API implementing business rules and logic

Try it first!

You may have a look to the public demo hosted here:

  1. administration User Interface: login with credentials chatty/chatty, then create some regular users and fill in their profile name to grant them access and visibility to chat UI.

  2. chat UI 1, chat UI 2: access with regular user's credentials created at step 1. We provide here two routes to the same frontend application, so you can sign in as different users at same time to test real-time push messaging.

You may also access and edit your own private copy of Chatty, by installing our free CouchDB Hosting, or installing to your own CouchDB installation.

Design-documents basis

Couchapps are web applications stored in one or more CouchDB special documents, called design documents. A typical design-document contains the following fields, files and folders:

  • rewrites.json : this is the base starting point for our API
  • _id : is the design document id
  • _attachments: contains static assets (html, css, javascript files, images, text documents and so on)
  • updates : functions to insert/update/delete documents. Targets of _update.
  • views : queries to be executed on the database. Targets of _view.
  • lists : functions to further filter or transform documents emitted by views, into JSON, html, plain text or whatever you want. Targets of _list.
  • shows : behaves like lists, but works on a single document. Targets of _show.
  • filters : functions to restrict the set of documents returned by _changes or filtered replications.

Design documents may be navigated with the Futon administration interface provided with CouchDB. Small modifications to design documents are possible with Futon as well, but for serious Design documents development, we suggest the DDoc.Lab editor.


Chatty API Design

Our general purpose API needs:

  • a way to authenticate users
  • a way to read: get all and only data belonging to the signed-in user
  • a way to write: insert/update/delete all entities involved

Forwarding rules

Forwarding or rewriting rules are base starting point to design our app. They are contained in the rewrites.json file and are used to:

  • correctly forward incoming requests to designated target functions, responsible for serving content
  • block malicious requests, allowing only valid from patterns
  • block specific requests only, like temporarily blocking all write requests during site maintenance periods
  • perform some kind of path-based access control

Chatty rules

Following rules are defined for Chatty.

  {
    "method":"GET",
    "from" : "/common/*",
    "to" : "common/*"
  },
  {
    "method":"GET",
    "from" : "/fonts/*",
    "to" : "common/fonts/*"
  },
  {
    "method":"GET",
    "from" : "/*",
    "to" : ":site/*"
  },
  {
    "method":"GET",
    "from" : "",
    "to" : ":site/index.html"
  },

Rules above return static assets, HTML, CSS, JS and images stored in the _attachments folder.


  {
    "method": "GET",
    "from" : "/owndocs/:k/0",
    "to": "_list/own_docs_list/own_docs_view",
    "query": {
       "startkey": [
          ":k",
           {
           }
       ],
       "endkey": [
          ":k"
       ],
       "reduce":"false",
       "descending": "true",
       "include_docs": "true"
    }
  },
  {
    "method" : "GET",
    "from" : "/owndocs/:k/:since",
    "to" : "../../_changes",
    "query" : {
        "filter":"app/own_docs_filter",
        "style":"all_docs",
        "include_docs":"true",
        "timeout":"25000",
        "feed":"longpoll",
        "limit":"50"
    }
  },

Rules above return documents belonging to the currently logged in user:

  • When page starts, or user sign-in, the first one is activated
  • next requests activates the second rule, which uses _changes handler with long-polling, to receive real-time updates

As you can see the from attribute within these rules is almost identical: this allows writing a server-side code, which is transparent to the client, thus enabling progressive enhancements. The first rule, as example, wasn't present at all in first Chatty release, but has been introduced only later, to increase performance on initial page loads. This has required modifications on the Server-side only, withouth any change to the Client-side code.

Read more on Read API.


  {
    "method" : "GET",
    "from" : "/allprofiles",
    "to" : "_view/allprofiles",
    "query" : {
        "include_docs":"true",
        "reduce":"false"
    }
  },  

This rule returns a list of all profile entities. Profiles are displayed within the frontend, in a combobox, to initiate conversations. Profiles are public information in Chatty, they do not belong to the currently logged in user, so we can't retrieve them with previous rules. So we created a dedicated view in views/allprofiles.

Read more on Read API.


  {
    "method":"PUT",
    "from" : "/:action/:docid",
    "to" : "_update/put/:docid"
  },
  {
    "method":"PUT",
    "from" : "/:action",
    "to" : "_update/put"
  }

These are used to insert/update/delete any kind of entity.

Read more on Write API.


  {
    "method":"GET",
    "from" : "/_session",
    "to" : "../../../_session"
  },
  {
    "method":"DELETE",
    "from" : "/_session",
    "to" : "../../../_session"
  },

Above rules are used for user authentication. Requests are forwarded to the default authentication handler.

Read more on User Authentication API.

NOTE: To ease reading of this tutorial, we stripped off the prefix /:configsecret/:site/, which is instead present in Chatty rewrites.json file. The prefix is however needed to enable custom authentication and multisite features.


Data modeling

Modeling our data well, is essential to speed up development. We need to understand:

  • which entities are involved
  • how to classify them in a document-oriented database

Chatty only has three entities: user, profile and chat.

Relational DBMS use tables to represent entities and relations, while NoSQL databases make use of the type document attribute to classify different entities.

Shared attributes

Following attributes are common to all entities. They enable common features and behaviours, such as read and write access control or document timestamping.

  • _id: document id
  • _rev: doc revision number, for versioning, internally assigned by CouchDB
  • type: document type or entity name
  • u: user who owns the document or the document is referred to
  • grants: specifies users who have some privileges on this document.
  • ts_create: timestamp the document was created, in ms
  • ts_lastupd: timestamp the document was updated last time, in ms

Entity: User

To attributes defined above, user entity adds those needed for user authentication, inherited from the official CouchDB user document definition:

  • _id: Must be in the form org.couchdb.user:username where username can be any string or email address.
  • name: username of the user. Must match the document id, prefix org.couchdb.user: excluded.
  • roles: an array of strings.
  • password_scheme: hashing algorithm used. Fixed to pbkdf2.
  • iterations: hashing iterations number. Fixed to 10.
  • salt: salt used for hashing. Random string.
  • derived_key: password hash, calculated from entered password, salt and iterations, using pbkdf2 algorithm.

To extend user entity capabilities, grants attribute can include following grant types:

  • creator: enables a user doc, to be removed only from his own creator. This prevent administrators from removing users created by others.

Example org.couchdb.user:chatty: user document with admin UI access

{
   "_id": "org.couchdb.user:chatty",
   "_rev": "1-a731abe1663804acf8f1950d1f52133d",
   "u": "chatty",
   "name": "chatty",
   "roles": [
       "backend",
       "chatty"
   ],
   "grants": {
       "creator": "upgrade"
   },
   "type": "user",
   "ts_create": 1427894490912,
   "ts_lastupd": 1427894490912,
   "derived_key": "8d494aa90f64864a74cee0275dd86e8693017009",
   "iterations": 10,
   "password_scheme": "pbkdf2",
   "salt": "a90bef87acec6899404f797b359989e3"
}

Example org.couchdb.user:mario: user document with frontend access

{
   "_id": "org.couchdb.user:mario",
   "_rev": "1-cd4c6d9cddf06d2ab89d9f81db1edd71",
   "u": "mario",
   "name": "mario",
   "roles": [
       "mario",
       "frontend"
   ],
   "grants": {
       "creator": "chatty"
   },
   "type": "user",
   "ts_create": 1428412272562,
   "ts_lastupd": 1428412272562,
   "derived_key": "5376cb534f8affefed85128fb05280035150bb5b",
   "iterations": 10,
   "password_scheme": "pbkdf2",
   "salt": "3caa80b89644b7edc91e778ccec2640c"
}

Grants vs Roles

Grants attribute:

  • is shared among all entities.
  • is an object, or better a key/value dictionary, where key is the grant type, and value is a user, or array of users, this grant applies
  • when reading: all users included in grants are allowed to read this document. This can easily be modified to match different use cases.
  • when writing: updates/deletes are allowed or rejected, depending on specific grants the current user owns on this doc

Roles attribute:

  • is defined for user entity only
  • is an array of strings: these can be roles, usernames, documents stored as json strings, or anything else useful for access control.
  • among all user attributes, only name and roles are available within userCtx object of authenticated request. They can then be matched against grants, to allow or reject a specific action.

Entity: Profile

Example mario: public profile document

{
   "_id": "mario",
   "_rev": "1-5f52ae053aee863d7f3c8c6d24dfdb66",
   "u": "mario",
   "type": "profile",
   "ts_create": 1428420470733,
   "name": "Mario",
   "url": "",
   "desc": "No description",
   "ts_lastupd": 1428420470733
}

Entity: Chat

Example giorgio|mario: chat/conversation document between Mario and Giorgio

{
   "_id": "giorgio|mario",
   "_rev": "4-0699edbc3b5e3b89f1a28111800e15ab",
   "grants": {
       "giorgio": "giorgio",
       "mario": "mario"
   },
   "history": [
       {
           "msg": "Hi Mario, how are you?",
           "u": "giorgio",
           "ts": 1428420532819
       },
       {
           "msg": "Hi Giorgio, Fine thanks, and you?",
           "u": "mario",
           "ts": 1428420591098
       }
   ],
   "ts_create": 1428420532819,
   "type": "chat"
}

Chat entity defines:

  • _id: in the form a|b, if a and b are the usernames involved in the conversation.
  • grants: contains usernames involved in the conversation.
  • history: contains conversation history. For each message we keep track of:
    • msg: body of the message
    • u: author of the message
    • ts: timestamp, allows sorting

Please note:

  • in _id, usernames must be ordered, to prevent duplicates (different chat documents for same users).
  • history can grow much. To optimize disk space, we limit its size to 10, implementing it as a circual queue: old messages are discarded first.
  • grants contains usually two usernames only, but it can eventually grow if, as example, an administrator joins the conversation.

Read API

Our API should be:

  • general purpose: we want to be able to define new entitites at any time, withouth the need to change our API interface
  • as simple as possible, easy to understand and extend
  • secure, allowing users to access and modify only information they are really granted to

We chose AngularJS as client-side Javascript framework, to automagically reflect database changes from Server to Client User Interface.

Purpose is to fetch on the client, any user document, regardless its entity. We use a plain array as storage to benefit of AngularJS ng-repeat directive and have document changes immediately applied on UI.

User List UI:

<h2>{{(docs | filter:{type:'user'}).length}} users</h2>
<div ng-switch on="(docs | filter:{type:'user'}).length">
    <div ng-switch-when="0"> 
        <p>No visible "user" type documents.</p>
        <button>Create First User</button>
    </div>
    <div ng-switch-default>
        <button>Add User</button>
        <table>
        <tr>
            <th>Delete</th>
            <th>Edit</th>
            <th>Username</th>
            <th>E-mail</th>
            <th>Created</th>
        </tr>
        <tr ng-repeat="doc in docs | filter: {type:'user'}">
            <td><button ng-click="drop(doc._id)">Delete</button></td>
            <td><button user-panel selected-tab="tab-user" selected-username="{{doc.name}}">Edit</button></td>
            <td>{{doc.name}}</td>
            <td>{{doc.email}}</td>
            <td>{{doc.ts_create| date:'MMM d, h:mm a'}}</td>
        </tr>
        </table> 
    </div>
</div>

The above, stored in _attachments/backend/index.html file, shows a list of all users. To show profiles instead of entities, it is sufficient to change the ng-repeat filter and attributes shown.

Read API: Fetching user documents

Following client-side script is responsible for continuously retrieving a portion of the database, strictly related with the current user. These are usually private documents (u field matches with userCtx.key), but may also be documents with public visibility. All documents, regardless of their entity (type field), are retrieved.

// File: _attachments/common/js/ng/f_happy.js

getChanges : function(callingprio) {
    var wait = 5000,prio=callingprio||1;
    if (priosv.prio) {
        sv.prio=prio;
    } else if (sv.fetching) {
        return;
    }
    if (t.userCtx && t.userCtx.key) {
        sv.fetching=true;
        $http({
            method:"GET",
            url: "owndocs/"+t.userCtx.key+"/"+(sv.since||0)
        }).success( function (data) {
            sv.fetching=false;
            if (prio==sv.prio){
                if (t.userCtx && t.userCtx.key) {
                    t.firstLoad =true;
                    if (data && data.last_seq) {
                        sv.since = data.last_seq;
                        var ch = data.results;
                        for (var i in ch) {
                            t.addOrUpdateToDocs(ch[i].doc);
                        }
                        wait = 1000;
                    }
                }
            }
            sv.timeoutChanges = setTimeout(function(){t.getChanges(prio);},wait);
        }).error( function(data) {
            sv.fetching=false;
            sv.timeoutChanges = setTimeout(function(){t.getChanges(prio);},wait);
        });
    } else {
        t.firstLoad = true;
        sv.timeoutChanges = setTimeout(function(){t.getChanges(prio);},wait);
    }
},   

On the server, all and only those documents connected with the user, shall be returned. The _changes handler is then used with the following Javascript filter:

// File: filters/own_docs_filter.js

function(doc, req)
{
  /* if your user indexing key(the one passed within the request) is not intended to be found within the first element of the roles array, you need to modify the following assignment */

  var loggeduser = (req.userCtx 
      && req.userCtx.roles
      && req.userCtx.roles.length>0
      && typeof req.userCtx.roles[0]=="string")?req.userCtx.roles[0]:false;

  // We consider only doc with the "type" attribute set
  if (doc._deleted!==true && typeof doc.type!=="string") return false; 

  /* Here we perform the effective filtering, by matching loggeduser against document grants and owner(doc.u) */
  if (loggeduser=="backend") {
    // if the key "backend" is in roles first position, then it can see everything
    return true
  } else if ((doc.grants?Object.keys(doc.grants).map(function(k){return doc.grants[k];}):[]).concat(doc.u).indexOf(loggeduser)>=0 ) {
    return true;
  }

  return false;
}

Read API: Fetching On-demand

Documents with public visibility may be fetched on-demand too, only when they are really needed. We use CouchDB views and lists handlers in this case. The following client-side function, fetches public user information:

// File: _attachments/common/js/ng/f_happy.js

getAllProfiles:function(){
  var deferred = $q.defer();
  $http({
    method:"GET",
    url: "allprofiles"
  }).success( function (data) {
    if (data && data.rows) {
      for (var i in data.rows) {
        t.addOrUpdateToDocs(data.rows[i].doc);
      }
    }
    deferred.resolve(data);
  }).error(deferred.reject);
  return deferred.promise;
},

On the Server a CouchDB view is used. A list to reduce information is not necessary here, given that the profile entity does not contain sensitive information.

// File: views/allprofiles/map.js

function(doc) {
/* Emits all docs of type profile. This is used to let frontend users, select a public profile as chat target. */
  if (doc.type && doc.type === "profile"){
    emit(doc._id,1);
  }
}

Optimizing initial load of user documents

Fetching user documents using the changes approach works well, but first request. at page load or after sign-in, may be slow especially on large databases.

We can speed it up, using a view and list approach only for first request. A change on the Server-side only is required. We create this view:

// File: views/own_docs_view/map.js

function(doc) {
/*
  Emits data for the first "changes" request requested by the client.
  Emits key value pairs to be passed to the "own_docs_list" list. Within these pairs key is an array and value is 1.

  The key must be an array and its first field must be the indexing key of the requesting user. A user requesting "/owndocs/myuserkey/0" can have access to all documents indexed with "myuserkey" within the first position of the key array.
  Next fields, within the key array, can be used for sorting or extra filtering.
  Why value is always 1? Because for "changes" purposes, using include_docs=true already grants "own_docs_list", to access the entire underlying database document. So we opted to emit a value efficient to store (Probably a boolean value would have been better?!)
*/

// We consider only doc with the "type" attribute set
if (typeof doc.type!=="string") return false; 

var toemit={};
toemit["backend"]=1;
 if ((typeof doc.u)[0]!=="u") {
    toemit[doc.u]=1;
 }
 if ((typeof doc.grants)[0]!=="u"){
    for (var i in (doc.grants||{})){
      if ((typeof doc.grants[i])[0]==="s")
        toemit[doc.grants[i]]=1;
    }
 }
 for (var i in toemit){
   emit([i,doc.ts_create||0],1);
 }
}

this list:

// File: lists/own_docs_list.js

function(head, req) {
  try {
    /* if your user indexing key(the one passed within the request) is not intended to be found within the first element of the roles array, you need to modify the following assignment */
    var loggeduser = (req.userCtx 
      && req.userCtx.roles
      && req.userCtx.roles.length>0
      && typeof req.userCtx.roles[0]=="string")?req.userCtx.roles[0]:false;

    if (loggeduser && loggeduser===(req.query.k||true)) {
      start({
        code : 200
      });
      send('{"results":[');
      var i=0;
      while (row = getRow()) {
        send((i++>0?",":"")+JSON.stringify({doc:row.doc}));
      }
      send('],"last_seq":'+req.info.update_seq+'}');

    } else {
      throw {
        code:401,
        body:JSON.stringify({
          "error":"unauthorized",
          "reason":"You are not authorized to access this db."
        })
      }
    }
  } catch(ex){
    start({
      code : ex.code,
      headers: {"Content-Type" : "text/json"}
    });
    send(ex.body);  
  }   
}

and add a forwarding rule catching only initial requests:

  // File: rewrites.json
  {
    "method": "GET",
    "from" : "/owndocs/:k/0",
    "to": "_list/own_docs_list/own_docs_view",
    "query": {
       "startkey": [
          ":k",
           {
           }
       ],
       "endkey": [
          ":k"
       ],
       "reduce":"false",
       "descending": "true",
       "include_docs": "true"
    }
  },

Finally we add some lines to our changes filter, to block malicious and cpu-intensive requests.

// File: filters/own_docs_filter.js

/* This check helps preventing not authed or malicious users to submit requests which can be cpu-intensive: we throw an error if the "since" value passed within the request is too far from the actual update_sequence. This allows the changes request to never span the entire database, but start checking from since, only on recent updates. If this exception is catched client side, then the client can submit a "/owndocs/myuserkey/0" view-based request, which is instead very efficient. */

if ( !(loggeduser  
    && loggeduser === (req.query.k||true)
    && Math.abs( parseInt(req.query.since) - req.info.update_seq ) < 500) ) {

    throw "Invalid request";
}

Write API

A secure web application always needs some kind of write access control to prevent unauthorized users from modifying our entire database. While these checks are optional client-side, and usually implemented only to increase usability, these are absolutely mandatory on the server. We would like our Write API to:

  • separate presentation from application layer
  • keep business logic on the server only, avoiding redundancies on the client
  • abstract update actions from effective changes on documents

Action based Write API

Given desirable features above, we opted for an action-based approach. Examples of actions are:

  • user edits his profile
  • user sends a message
  • user creates a new user

Action-based approach allows an high level of abstraction: instead of thinking in terms of insert, update or delete operations over documents, we simply start thinking in terms of actions an entity can do. Those who like UML, acronym for Unified Modeling Language, may easily find a mapping between these actions and UML interactions. This similarity results in huge benefits over the whole application development lifecycle.

It turns out that, for each entity defined, we need to identify a set of high level actions, where each one defines:

  • a set of validation rules, to check if user has sufficient privileges to execute that action on that document
  • how the document is modified

Write API: Client-Side

All actions, for all entities, are invoked by the same method:

// File: _attachments/common/js/ng/f_happy.js

action: function(action,params) {
    params = params || {};
    if (typeof t["action"+action]=='function')
        return t["action"+action](params);

    var deferred = $q.defer();
    $http({
        method:     "PUT",
        url:        ""+action+(params && params.doc ? "/" + params.doc : ""),
        data:   params
    }).success(deferred.resolve).error(deferred.reject);
    return deferred.promise;
}, 

Function above is called with parameters appropriate to specific action:

//  File: _attachments/frontend/js/app.js
$scope.chat = function(target){
    var msg = window.prompt("Message to send:");
    if (msg && msg.length>0) {
      happy.action('chat',{
        doc : target,
        msg : msg
      }).then(happy.success,happy.err);
    } 
};

//  File: _attachments/frontend/index.html
<button ng-click="chat(doc._id)">Send message</button>

Write API: Server-Side

On the Server, we create a folder lib and, for each action Y on entity X, we create a file lib/X/Y.js to define its validation rules and behaviours.

Here is, as example, the action to create or edit a profile:

// File lib/profile/profile.js

exports.exec = function(req,params,doc,caller){
  var v = require("lib/utils").utils(req), caller=caller||{}, msg = caller.msg||"Profile updated successfully.", isNew = false;
  // guests are rejected
  v.shouldBeLogged();

  // backend users are allowed to modify every profile
  v.assert(v.isRole('backend') || req.id === req.userCtx.name, "You are not the owner of this profile doc!",401);
  // if present, url must be effectively an url
  v.assert(!params.url || v.isUrl(params.url),'Url not valid',400);

  var isNew = !doc;
  if (isNew) { // insert
      // if not present, we create the profile doc
      var doc = { 
        _id : req.id,
        u : req.id,
        type :'profile',
        ts_create : v.now()
      };
  }  

  // sanitization check: we reject if parameters contain malicous code
  v.sameEscaped(params.name,"Name");
  v.sameEscaped(params.desc,"Description");

  // we update doc fields with parameters received
  doc.name = params.name||doc.name||'Anonymous';
  doc.url = params.url||doc.url||'';
  doc.desc = params.desc||doc.desc||'No description';
  doc.ts_lastupd = v.now();

  // we return the document to create/update and response body for the client
  return [doc,v.response({ok:true, msg:msg}),doc?200:201];
};

Action profile/profile above is responsible for both insert and update operations, thus abstracting client from document existence.

Deletes are performed by the drop action.

// File lib/profile/drop.js
exports.exec = function(req,params,doc,caller){
  var v = require("lib/utils").utils(req), caller=caller||{}, isNew = false, 
  msg = caller.msg||'Doc "'+(doc.name||doc._id)+'" successfully deleted';

  v.shouldBeLogged();
  v.assert(v.isRole('backend') || doc.u === req.userCtx.name, "You cannot delete this doc!",401);

  var old = doc;
  doc = {
      _id:old._id,
      _rev:old._rev,
      u: old.u,
      grants: old.grants,
      _deleted :true
  };

  return [doc,v.response({ok:true, msg:msg}),!isNew?200:201];
};

Above, we sets both u and grants field, to let the change notification works on deleted documents too.

Write API: Final notes

Please note:

  • you can find all actions definitions in the lib folder
  • Actions scripts are invoked from the lib/put.js script, which is in turn invoked from the rewrites.json file handling the request
  • the lib/default folder comes useful on inserts, when the document referred does not exist yet: you can put here functions, to correctly forward incoming request to the appropriate entity folder:

    // File lib/default/profile.js
    exports.exec = function(req,params,doc,caller){
        return require("lib/profile/profile").exec(req,params,doc,{});
    };
    

User Authentication API

To authenticate a user our API needs:

  • a method to know if the user is already signed in
  • a method to sign in the user
  • a method to sign out

CouchDB Default Cookie API

CouchDB provides some options to authenticate a user. Here we will use the cookie authentication scheme:

  • CouchDB generates a token that the client can use for next requests.
  • Tokens are valid until a timeout.
  • When CouchDB sees a valid token in a subsequent request, it will authenticate user by this token without requesting password again

As described in the official guide, cookie authentication API includes following 3 methods:

  • POST /_session:

    • invoked when user asks to sign in
    • receives username and password
    • generates and returns an authentication token/cookie in a response header like:

      Set-Cookie:AuthSession=ALongAndStrangeTokenValue; Version=1; Path=/; HttpOnly
      
    • browser stores the cookie, for use on next requests

  • GET /_session:

    • usually invoked at page start, to check if the user was previously signed-in
    • receives an AuthSession cookie, automatically sent by the browser if available
    • checks cookie validity and returns details about the current user:

      {
          "ok": true,
          "userCtx": {
              "name": "username-currently-signed-in",
              "roles": ["array-of-roles-stored-within-the-user-document"]
          },
          ...
      }
      
  • DELETE /_session:

    • invoked when user asks to sign out
    • returns a response header to clean the AuthSession cookie:

      Set-Cookie:AuthSession=; Version=1; Path=/; HttpOnly
      
    • browser clears the cookie

Default API limitations

Given our use case, CouchDB default methods above, have two limitations.

  1. user documents must be stored within the _users database: this may be not desirable, in case we need views joining user documents too. We can easily workaround this, by storing users in our main chatty database and creating a persistent continous replication, from chatty to _users.

        {
           "_id": "sync-users",
           "source": "chatty",
           "target": "_users",
           "create_target": true,
           "continuous": true,
           "user_ctx": {
               "name": "admin",
               "roles": [
                   "_admin"
               ]
           },
           "owner": "admin"
        }
    

    If you may be wondering, only user entities get replicated: the validate_doc_update function, defined in _users/_design/_auth, rejects all entities different from user.

  2. As long as username and password are correct and the user document exists within _users, the user gets authenticated. This behaviour is not desirable, because it means that any Chatty user, regardless his roles, can authenticate to any section: frontend and admin UI.

    In order to correctly allow/prevent user access to different sections, we need to implement our own custom authentication method.

Custom Cookie Authentication within Couchapp

The great news is that, within Chatty, we created our custom authentication function, within the couchapp directly, withouth the need to create any external, erlang-written, CouchDB plugin. Once more time, we made another step forward to no vendor lock-in.

Following method implements custom authentication. It uses base64 for encoding and CryptoJS libraries for hashing.

// File lib/user/signin.js

exports.exec = function(req,params,doc,caller){

   var v = require("lib/utils").utils(req),
        caller=caller||{}, msg = caller.msg||"User successfully authed.", isNew = !doc, 
        userid=v.getUserid(), pbkdf2 = require("lib/pbkdf2"),
        CryptoJS   = require("lib/hmac-sha1"),base64 = require("lib/base64"),
        hash = "", calc = "", timestamp    = Math.round(v.now()/1000);

    v.assert(doc&&doc.type=="user","Invalid username or password",400);
    v.assert(doc.salt && doc.derived_key && doc.roles,"salt, derived_key or roles not found",400);
    v.assert(req.query && doc.roles.indexOf(req.query.site)>=0,"Not granted on this site",400);

      v.assert(typeof params.password ==="string","Invalid password. Must be at least 6 characters long",400);

    hash = pbkdf2.pbkdf2(params.password, doc.salt, { keySize: 256/32, iterations: doc.iterations }).toString().substring(0,40);
    v.assert(hash===doc.derived_key,"Incorrect password",401);

    var sessdata = doc.name+":"+timestamp.toString(16).toUpperCase();

    v.assert(req.query.configsecret,"\"configsecret\" query parameter is mandatory. You can define it directly within your domain base path, from your smileupps control panel. Your vhost host must point to /"+req.info.db_name+"/_design/app/_rewrite/CONFIGSECRET/SITE/ where CONFIGSECRET is the value in your \"couch_httpd_auth/secret\" CouchDB config parameter and SITE can be one of [frontend,backend]");
    var configsecret = req.query.configsecret;

    var ret = [];
    hash = CryptoJS.HmacSHA1(sessdata,configsecret+doc.salt).words;
    for (var i in hash) {
        var v = hash[i], pos = v>=0, last=ret.length;
        for(v=pos?v:v>>>0; v>0; v=Math.floor(v/256)) {
            ret.splice(last, 0, v%256);
        }
    }

    calc = base64.btoa(sessdata+":"+(String.fromCharCode.apply(String,ret)));
    calc = calc.replace(/\//g,'_').replace(/\+/g,'-');

    return [null,
        {
            "code": 200,
            "headers" : {
                "Content-Type" : "application/json",
                "Set-Cookie" : "AuthSession="+calc+"; Version=1; Path=/; HttpOnly"
            },
            "body" : JSON.stringify({ ok:true, msg:msg})
        },
        200
    ];
};

To authenticate a user, these checks must pass:

  • user document must exists
  • user document must have salt, derived_key and roles attribute
  • roles attribute must contain one of frontend or backend, depending on which website/domain the user is accessing. Please note that actual values for req.query.site, are taken from current domain base path, defined in CouchDB virtual hosts configuration. Forwarding rules defined in rewrites.json, just name this value as site, before forwarding requests to their appropriate target functions.
  • provided password must be at least 6 characters long
  • calculated password hash must match derived_key

If all these checks execute successfully, we calculate the authentication token and return it to the client.