Difference between revisions of "CouchDB Designs for Saving and Retrieving Security Data"

From wiki.gpii
Jump to: navigation, search
m
(No difference)

Revision as of 02:47, 15 September 2015

This document is part of the work of GPII-1274

The security server currently uses an In Memory Data Store to save and retrieve authentication data. To support couchDB, a couchDB data store will be created to replicate the API that has been provided by the in memory data store. This document contains the couchDB design that is needed for the couchDB data store to communicate with the backend couchDB for implementing all API functions.

This document experiments with two set of couchDB designs to observe their pros and cons: Flat Data Structure and Nested Data Structure. The flat data structure uses the concept of relational database design that has all documents containing only one level of data. The nested data structure has multiple level of data to accommodate some one to many relationship. For example, an user document contains an array of all authentication decisions made by this user, and each authentication decision may contain another array of authentication codes assigned to this decision.

Pre-defined items for understanding HTTP requests in designs:

  • The database name is oauth_security;
  • CouchDB views and lists are required to support some HTTP requests. See the last two sections in each design for the source code to create couchDB views and lists;
  • All PUT requests for creating new documents implies one step before that fetches a UUID from couchDB as per what is recommended in CouchDB HTTP Document API.

Flat Data Structure

This design uses the concept of relational database design that has all documents containing only one level of data.

Data Structure

There are 4 type of data to be saved for OAuth2 authentication: users, clients, user authentication decisions and authentication codes.

Type Structure How an example looks like in couchDB
Users
{
    "type": "user", 
    "username": string, 
    "password": string, 
    "gpiiToken": string
}
{
    "_id":"c58ebe4b64a6d55f0e4bac3b2c000ad7",
    "_rev":"1-39a76e3f1b12bdac56364bf55966906c",
    "type":"user",
    "username":"chromehc",
    "password":"chromehc",
    "gpiiToken":"review3_chrome_high_contrast"
}
Clients
{
    "type": "client",
    "name": string,
    "oauth2ClientId": string,
    "oauth2ClientSecret": string,
    "redirectUri": string, // an url
    "allowDirectGpiiTokenAccess": boolean
}
{
    "_id": "c58ebe4b64a6d55f0e4bac3b2c0017c2",
    "_rev": "1-1ff289412b3f7272a0e28aa39d387ca3",
    "type": "client",
    "name": "Easit4all",
    "oauth2ClientId": "com.bdigital.easit4all",
    "oauth2ClientSecret": "client_secret_easit4all",
    "redirectUri": "http://www.easit4all.com/oauth_signin/authorize_callback",
    "allowDirectGpiiTokenAccess": false
}
User authentication decisions
{
    "type": "authDecision",
    // Corresponds to the _id value of the user record
    "userId": string,
    // Corresponds to the _id value of the client record
    "clientId": string,
    "redirectUri": string,
    "accessToken": string,
    // A preference object, such as {"textSize": 2}
    "selectedPreferences": object,
    "revoked": boolean
}
{
    "_id": "c58ebe4b64a6d55f0e4bac3b2c001a8d",
    "_rev": "2-e288dcca47aedf636d88f12a38a211dd",
    "type": "authDecision",
    "userId": "3",
    "clientId": "7",
    "redirectUri": "http://eastit.gpii.com/callback",
    "accessToken": "ma1_access_token",
    "selectedPreferences": { "": true },
    "revoked": false
}
Authentication codes
{
    "type": "authCode",
    // Corresponds to the _id value of the decision record
    "authDecisionId": string,
    "code": string
}
{
    "_id": "c58ebe4b64a6d55f0e4bac3b2c002572",
    "_rev": "1-8590de10487b34230079885556db6df0",
    "type": "authCode",
    "authDecisionId": "11",
    "code": "e288dcca4"
}

HTTP Requests

API Function Method URL Data (only for PUT or POST Requests)
gpii.oauth2.dataStore.findUserById GET /oauth_security/%userId N/A
gpii.oauth2.dataStore.findUserByUsername GET /oauth_security/_design/fetch/_view/find_user_by_username?key="%username"&include_docs=true N/A
gpii.oauth2.dataStore.findClientById GET /oauth_security/%clientId N/A
gpii.oauth2.dataStore.findClientByOauth2ClientId GET /oauth_security/_design/fetch/_view/find_client_by_oauth2ClientId?key=%oauth2ClientId&include_docs=true N/A
gpii.oauth2.dataStore.addAuthDecision PUT /oauth_security/%newDecisionId -d %jsonData See the data structure of user authentication decisions
gpii.oauth2.dataStore.updateAuthDecision PUT oauth_security/%existingDecisionId?rev=%_rev -d %jsonData See the data structure of user authentication decisions
gpii.oauth2.dataStore.revokeAuthDecision GET & PUT Step 1 (GET), to fetch the decision record using the decision ID and user ID: /oauth_security/_design/fetch/_view/find_decision_by_decisonId_and_userId?key=["%authDecisionId", "%userId"]&include_docs=true

Step 2 (PUT), to save the update decision record with revoked value set to true: /oauth_security/%decisionId?rev=%_rev

For PUT request in Step 2, see the data structure of user authentication decisions
gpii.oauth2.dataStore.findAuthDecisionById GET /oauth_security/%decisionId N/A
gpii.oauth2.dataStore.findAuthDecision GET /oauth_security/_design/fetch/_view/find_activeDecision_by_userId_and_clientId_and_redirectUri?key=["userId","clientId","redirectUri"]&include_docs=true N/A
gpii.oauth2.dataStore.saveAuthCode PUT /oauth_security/%newAuthCodeId See the data structure of authentication codes
gpii.oauth2.dataStore.findAuthByCode GET /oauth_security/_design/fetch/_view/find_decision_by_authCode?key="%code"&include_docs=true N/A
gpii.oauth2.dataStore.findAuthorizedClientsByUserId GET /oauth_security/_design/fetch/_view/find_clients_by_userid?key="%userId"&include_docs=true N/A
gpii.oauth2.dataStore.findAuthByAccessToken GET /oauth_security/_design/list/_list/get_auth_content/fetch/find_auth_by_accessToken?startkey=["%accessToken"]&endkey=["%accessToken,{}]&include_docs=true N/A
gpii.oauth2.dataStore.findAccessTokenByOAuth2ClientIdAndGpiiToken GET Step 1, find the user by gpii token:

/oauth_security/_design/fetch/_view/find_user_by_gpiiToken?key="%gpiiToken"&include_docs=true

Step 2, find the client by oauth client ID: /oauth_security/_design/fetch/_view/find_client_by_oauth2ClientId?key="%oauth2ClientId"&include_docs=true

Step 3: find the decision by user ID and client ID from steps above: /oauth_security/_design/fetch/_view/find_activeDecision_by_userId_and_clientId?key=["%userId", "%clientId"]&include_docs=true

N/A

CouchDB Views

The source code for creating couchDB views to support requests described in the HTTP requests section.

{
    "_id" : "_design/fetch",
    "views" : {
        "find_user_by_username" : {
            "map" : "function(doc) {if (doc.type === 'user') {emit(doc.username, null);}}"
        },
        "find_user_by_gpiiToken" : {
            "map" : "function(doc) {if (doc.type === 'user') {emit(doc.gpiiToken, null);}}"
        },
        "find_client_by_oauth2ClientId" : {
            "map" : "function(doc) {if (doc.type === 'client') {emit(doc.oauth2ClientId, null);}}"
        },
        "find_clients_by_userid": {
            "map" : "function(doc) {if (doc.type === 'authDecision') {emit(doc.userId, {'_id': doc.clientId});}}"
        },
        "find_decision_by_decisonId_and_userId" : {
            "map" : "function(doc) {if (doc.type === 'authDecision') {emit([doc._id, doc.userId], null);}}"
        },
        "find_activeDecision_by_userId_and_clientId_and_redirectUri" : {
            "map" : "function(doc) {if (doc.type === 'authDecision' && doc.revoked === false) {emit([doc.userId, doc.clientId, doc.redirectUri], null);}}"
        },
        "find_activeDecision_by_userId_and_clientId" : {
            "map" : "function(doc) {if (doc.type === 'authDecision' && doc.revoked === false) {emit([doc.userId, doc.clientId], null);}}"
        },
        "find_decision_by_authCode" : {
            "map" : "function(doc) {if (doc.type === 'authCode') {emit(doc.code, {'_id': doc.authDecisionId});}}"
        },
        "find_auth_by_accessToken" : {
            "map" : "function(doc) {if (doc.type === 'authDecision') {emit([doc.accessToken, 0], {'_id': doc.userId});emit([doc.accessToken, 1], {'_id': doc.clientId});emit([doc.accessToken, 2], {'_id': doc._id});}}"
        }
    }
}

CouchDB Lists

The source code for creating couchDB lists to support requests described in the HTTP requests section.

{
    "_id":  "_design/list",
    "lists": {
        "get_auth_content": "function(head, req) {
            var headers = {'Content-Type': 'application/json'};
            var result;
            start({'headers': headers});
            result = {'content': {}};
            while(row = getRow()) {
                if (row.doc.type === 'user') {
                    result.content.gpiiToken = row.doc.gpiiToken;
                }
                if (row.doc.type === 'client') {
                    result.content.oauth2ClientId = row.doc.oauth2ClientId;
                }
                if (row.doc.type === 'authDecision') {
                    result.content.selectedPreferences = row.doc.selectedPreferences;
                }
            }
            send(JSON.stringify(result));
        }"
    }
}

Nested Data Structure

This design uses nested data structure.

Data Structure

There are 2 type of data in this design: users, clients.

Type Structure How an example looks like in couchDB
Users
{
   "type": "user",
   "username": string,
   "password": string,
   "gpiiToken": string,
   // An array of objects
   "authDecisions": [
       {
           "decisionId": string,
           // Corresponds to the _id value of the client record
           "clientId": string,
           "redirectUri": string,
           "accessToken": string,
           // A preference object such as {"textSize": 1.2}
           "selectedPreferences": object,
           "revoked": boolean,
           // An array of objects
           "authCodes": [{code: string}, ...]
       },
       ...
   ]
}
{
  "_id": "c58ebe4b64a6d55f0e4bac3b2c007004",
"_rev": "2-23bd8eff1490825dc2a02fc714f177ed",
"type": "user",
"username": "ma1",
"password": "ma1",
"gpiiToken": "review3_ma1",
"authDecisions": [
   {
       "decisionId": "93421e9eb8f8d1d2e1a5e588120013c1",
       "clientId": "c58ebe4b64a6d55f0e4bac3b2c005890",
       "redirectUri": false,
       "accessToken": "ma1_access_token",
       "selectedPreferences": {
           "": true
       },
       "revoked": false,
       "authCodes": [
           {
               "code": "abc"
           },
           {
               "code": "bcd"
           }
       ]
   },
   {
       "decisionId": "93421e9eb8f8d1d2e1a5e58812000ff2",
       "clientId": "c58ebe4b64a6d55f0e4bac3b2c005cc4",
       "redirectUri": "redirectUri",
       "accessToken": "ma2_access_token",
       "selectedPreferences": {
           "textSize": 1.2
       },
       "revoked": false,
       "authCodes": [
           {
               "code": "cde"
           },
           {
               "code": "def"
           }
       ]
   }
]

}

Clients
{
    "type": "client",
    "name": string,
    "oauth2ClientId": string,
    "oauth2ClientSecret": string,
    "redirectUri": string, // an url
    "allowDirectGpiiTokenAccess": boolean
}
{
    "_id": "c58ebe4b64a6d55f0e4bac3b2c0017c2",
    "_rev": "1-1ff289412b3f7272a0e28aa39d387ca3",
    "type": "client",
    "name": "Easit4all",
    "oauth2ClientId": "com.bdigital.easit4all",
    "oauth2ClientSecret": "client_secret_easit4all",
    "redirectUri": "http://www.easit4all.com/oauth_signin/authorize_callback",
    "allowDirectGpiiTokenAccess": false
}

HTTP Requests

API Function Method URL Data (only for PUT or POST Requests)
gpii.oauth2.dataStore.findUserById GET Same as the flat data structure - /oauth_security/%userId N/A
gpii.oauth2.dataStore.findUserByUsername GET Same as the flat data structure - /oauth_security/_design/fetch/_view/find_user_by_username?key="%username"&include_docs=true N/A
gpii.oauth2.dataStore.findClientById GET Same as the flat data structure - /oauth_security/%clientId N/A
gpii.oauth2.dataStore.findClientByOauth2ClientId GET Same as the flat data structure - /oauth_security/_design/fetch/_view/find_client_by_oauth2ClientId?key=%oauth2ClientId&include_docs=true N/A
gpii.oauth2.dataStore.addAuthDecision GET & PUT Step 1 (GET), fetch the user record by the user id: /oauth_security/%userId

Step 2 (PUT), update the user record by adding the new decision into the authDecisions array, then PUT: /oauth_security/%userId -d %jsonData

For PUT request in Step 2, See the data structure of user authentication decisions in the user type
gpii.oauth2.dataStore.updateAuthDecision GET & PUT Step 1 (GET), fetch the user record by the user id: /oauth_security/%userId

Step 2 (PUT), update the decision record by finding the decision with the user id and the decision id, then PUT: oauth_security/%userId?rev=%_rev -d %jsonData

For PUT request in Step 2, See the data structure of user authentication decisions in the user type
gpii.oauth2.dataStore.revokeAuthDecision GET & PUT Step 1 (GET), to fetch the user record using the user ID: /oauth_security/%userId

Step 2 (PUT), update the decision record by finding the decision with the user id and the decision id, then set the revoke to true and PUT: /oauth_security/%userId?rev=%_rev

For PUT request in Step 2, see the data structure of user authentication decisions
gpii.oauth2.dataStore.findAuthDecisionById GET Need a view with its output having keys of the decision id /oauth_security/_design/fetch/find_decision_by_decisionId?key=%decisionId N/A
gpii.oauth2.dataStore.findAuthDecision GET Same as the flat data structure - /oauth_security/_design/fetch/_view/find_activeDecision_by_userId_and_clientId_and_redirectUri?key=["userId","clientId","redirectUri"]&include_docs=true N/A
gpii.oauth2.dataStore.saveAuthCode GET & PUT Step 1 (GET), find the user record using the decision id, (needs a view for it)

Step 2 (PUT), add the new auth code into the user record, then PUT the updated user record: /oauth_security/%userId

See the data structure of authentication codes in the user record
gpii.oauth2.dataStore.findAuthByCode GET Same as the flat data structure with a different view implementation - /oauth_security/_design/fetch/_view/find_decision_by_authCode?key="%code"&include_docs=true N/A
gpii.oauth2.dataStore.findAuthorizedClientsByUserId GET Same as the flat data structure with a different view implementation - /oauth_security/_design/fetch/_view/find_clients_by_userid?key="%userId"&include_docs=true N/A
gpii.oauth2.dataStore.findAuthByAccessToken GET Also need a view and a list but the list function is simpler - /oauth_security/_design/list/_list/get_auth_content/fetch/find_auth_by_accessToken?startkey=["%accessToken"]&endkey=["%accessToken,{}]&include_docs=true N/A
gpii.oauth2.dataStore.findAccessTokenByOAuth2ClientIdAndGpiiToken GET Step 1, find the client by oauth client ID: /oauth_security/_design/fetch/_view/find_client_by_oauth2ClientId?key="%oauth2ClientId"&include_docs=true

Step 2, find the user record by the gpii token and the client id from step 1: /oauth_security/_design/fetch/_view/find_user_by_gpiiToken?key="%gpiiToken"&include_docs=true

N/A

CouchDB Views

The source code for creating couchDB views to support requests described in the HTTP requests section.


CouchDB Lists

The source code for creating couchDB lists to support requests described in the HTTP requests section.


Summary

Flat Data Structure Nested Data Structure
Pros Have separate data types; easy to update each data type; same amount of work to fetch data from user's or client's perspective; Easier to join especially when involving more than 2 data types
Cons Difficult to join more than 2 data types more work to fetch or update nested data such as auth decisions or auth codes; as data is nested in user records, more work to fetch those data from client's perspective