diff options
author | Damien F. Katz <damien@apache.org> | 2009-08-04 19:50:46 +0000 |
---|---|---|
committer | Damien F. Katz <damien@apache.org> | 2009-08-04 19:50:46 +0000 |
commit | 8e2215ee6306b0f4c13553796d401e9f5f93bcb6 (patch) | |
tree | 948b9179887e73379bc445b9ad058de3a0bbe870 /share | |
parent | fd72a9bc48ebab76976f538c28459a0e26aa1750 (diff) |
Initial check-in of OAuth and cookie authentication.
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@800938 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'share')
-rw-r--r-- | share/Makefile.am | 4 | ||||
-rw-r--r-- | share/www/script/couch.js | 61 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 4 | ||||
-rw-r--r-- | share/www/script/jquery.couch.js | 35 | ||||
-rw-r--r-- | share/www/script/oauth.js | 511 | ||||
-rw-r--r-- | share/www/script/sha1.js | 202 | ||||
-rw-r--r-- | share/www/script/test/changes.js | 4 | ||||
-rw-r--r-- | share/www/script/test/cookie_auth.js | 162 | ||||
-rw-r--r-- | share/www/script/test/oauth.js | 166 | ||||
-rw-r--r-- | share/www/script/test/security_validation.js | 12 | ||||
-rw-r--r-- | share/www/script/test/show_documents.js | 2 |
11 files changed, 1154 insertions, 9 deletions
diff --git a/share/Makefile.am b/share/Makefile.am index 4e584703..8071525f 100644 --- a/share/Makefile.am +++ b/share/Makefile.am @@ -93,6 +93,8 @@ nobase_dist_localdata_DATA = \ www/script/jquery.resizer.js \ www/script/jquery.suggest.js \ www/script/json2.js \ + www/script/oauth.js \ + www/script/sha1.js \ www/script/test/basics.js \ www/script/test/delayed_commits.js \ www/script/test/all_docs.js \ @@ -138,6 +140,8 @@ nobase_dist_localdata_DATA = \ www/script/test/purge.js \ www/script/test/config.js \ www/script/test/security_validation.js \ + www/script/test/cookie_auth.js \ + www/script/test/oauth.js \ www/script/test/stats.js \ www/script/test/changes.js \ www/script/test/lorem.txt \ diff --git a/share/www/script/couch.js b/share/www/script/couch.js index bd1d49dd..f354f52a 100644 --- a/share/www/script/couch.js +++ b/share/www/script/couch.js @@ -299,6 +299,67 @@ function CouchDB(name, httpHeaders) { // Use this from callers to check HTTP status or header values of requests. CouchDB.last_req = null; +CouchDB.login = function(username, password) { + CouchDB.last_req = CouchDB.request("POST", "/_session", { + headers: {"Content-Type": "application/x-www-form-urlencoded", + "X-CouchDB-WWW-Authenticate": "Cookie"}, + body: "username=" + encodeURIComponent(username) + "&password=" + encodeURIComponent(password) + }); + return JSON.parse(CouchDB.last_req.responseText); +} + +CouchDB.logout = function() { + CouchDB.last_req = CouchDB.request("DELETE", "/_session", { + headers: {"Content-Type": "application/x-www-form-urlencoded", + "X-CouchDB-WWW-Authenticate": "Cookie"} + }); + return JSON.parse(CouchDB.last_req.responseText); +} + +CouchDB.createUser = function(username, password, email, roles, basicAuth) { + var roles_str = "" + if (roles) { + for (var i=0; i< roles.length; i++) { + roles_str += "&roles=" + encodeURIComponent(roles[i]); + } + } + var headers = {"Content-Type": "application/x-www-form-urlencoded"}; + if (!basicAuth) { + headers['X-CouchDB-WWW-Authenticate'] = 'Cookie'; + } + + CouchDB.last_req = CouchDB.request("POST", "/_user/", { + headers: headers, + body: "username=" + encodeURIComponent(username) + "&password=" + encodeURIComponent(password) + + "&email="+ encodeURIComponent(email)+ roles_str + + }); + return JSON.parse(CouchDB.last_req.responseText); +} + +CouchDB.updateUser = function(username, email, roles, password, old_password) { + var roles_str = "" + if (roles) { + for (var i=0; i< roles.length; i++) { + roles_str += "&roles=" + encodeURIComponent(roles[i]); + } + } + + var body = "email="+ encodeURIComponent(email)+ roles_str; + + if (typeof(password) != "undefined" && password) + body += "&password=" + password; + + if (typeof(old_password) != "undefined" && old_password) + body += "&old_password=" + old_password; + + CouchDB.last_req = CouchDB.request("PUT", "/_user/"+encodeURIComponent(username), { + headers: {"Content-Type": "application/x-www-form-urlencoded", + "X-CouchDB-WWW-Authenticate": "Cookie"}, + body: body + }); + return JSON.parse(CouchDB.last_req.responseText); +} CouchDB.allDbs = function() { CouchDB.last_req = CouchDB.request("GET", "/_all_dbs"); diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js index 5ac0f51e..91e95b11 100644 --- a/share/www/script/couch_tests.js +++ b/share/www/script/couch_tests.js @@ -73,6 +73,10 @@ loadTest("purge.js"); loadTest("config.js"); loadTest("form_submit.js"); loadTest("security_validation.js"); +loadTest("cookie_auth.js"); +loadScript("script/sha1.js"); +loadScript("script/oauth.js"); +loadTest("oauth.js"); loadTest("stats.js"); loadTest("rev_stemming.js"); diff --git a/share/www/script/jquery.couch.js b/share/www/script/jquery.couch.js index 73d064c4..54e32617 100644 --- a/share/www/script/jquery.couch.js +++ b/share/www/script/jquery.couch.js @@ -61,6 +61,41 @@ ); }, + // TODO make login/logout and db.login/db.logout DRY + login: function(options) { + options = options || {}; + $.ajax({ + type: "POST", url: "/_login", dataType: "json", + data: {username: options.username, password: options.password}, + complete: function(req) { + var resp = $.httpData(req, "json"); + if (req.status == 200) { + if (options.success) options.success(resp); + } else if (options.error) { + options.error(req.status, resp.error, resp.reason); + } else { + alert("An error occurred logging in: " + resp.reason); + } + } + }); + }, + logout: function(options) { + options = options || {}; + $.ajax({ + type: "POST", url: "/_logout", dataType: "json", + complete: function(req) { + var resp = $.httpData(req, "json"); + if (req.status == 200) { + if (options.success) options.success(resp); + } else if (options.error) { + options.error(req.status, resp.error, resp.reason); + } else { + alert("An error occurred logging out: " + resp.reason); + } + } + }); + }, + db: function(name) { return { name: name, diff --git a/share/www/script/oauth.js b/share/www/script/oauth.js new file mode 100644 index 00000000..aa0019d5 --- /dev/null +++ b/share/www/script/oauth.js @@ -0,0 +1,511 @@ +/* + * Copyright 2008 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Here's some JavaScript software for implementing OAuth. + + This isn't as useful as you might hope. OAuth is based around + allowing tools and websites to talk to each other. However, + JavaScript running in web browsers is hampered by security + restrictions that prevent code running on one website from + accessing data stored or served on another. + + Before you start hacking, make sure you understand the limitations + posed by cross-domain XMLHttpRequest. + + On the bright side, some platforms use JavaScript as their + language, but enable the programmer to access other web sites. + Examples include Google Gadgets, and Microsoft Vista Sidebar. + For those platforms, this library should come in handy. +*/ + +// The HMAC-SHA1 signature method calls b64_hmac_sha1, defined by +// http://pajhome.org.uk/crypt/md5/sha1.js + +/* An OAuth message is represented as an object like this: + {method: "GET", action: "http://server.com/path", parameters: ...} + + The parameters may be either a map {name: value, name2: value2} + or an Array of name-value pairs [[name, value], [name2, value2]]. + The latter representation is more powerful: it supports parameters + in a specific sequence, or several parameters with the same name; + for example [["a", 1], ["b", 2], ["a", 3]]. + + Parameter names and values are NOT percent-encoded in an object. + They must be encoded before transmission and decoded after reception. + For example, this message object: + {method: "GET", action: "http://server/path", parameters: {p: "x y"}} + ... can be transmitted as an HTTP request that begins: + GET /path?p=x%20y HTTP/1.0 + (This isn't a valid OAuth request, since it lacks a signature etc.) + Note that the object "x y" is transmitted as x%20y. To encode + parameters, you can call OAuth.addToURL, OAuth.formEncode or + OAuth.getAuthorization. + + This message object model harmonizes with the browser object model for + input elements of an form, whose value property isn't percent encoded. + The browser encodes each value before transmitting it. For example, + see consumer.setInputs in example/consumer.js. + */ +var OAuth; if (OAuth == null) OAuth = {}; + +OAuth.setProperties = function setProperties(into, from) { + if (into != null && from != null) { + for (var key in from) { + into[key] = from[key]; + } + } + return into; +} + +OAuth.setProperties(OAuth, // utility functions +{ + percentEncode: function percentEncode(s) { + if (s == null) { + return ""; + } + if (s instanceof Array) { + var e = ""; + for (var i = 0; i < s.length; ++s) { + if (e != "") e += '&'; + e += percentEncode(s[i]); + } + return e; + } + s = encodeURIComponent(s); + // Now replace the values which encodeURIComponent doesn't do + // encodeURIComponent ignores: - _ . ! ~ * ' ( ) + // OAuth dictates the only ones you can ignore are: - _ . ~ + // Source: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Functions:encodeURIComponent + s = s.replace(/\!/g, "%21"); + s = s.replace(/\*/g, "%2A"); + s = s.replace(/\'/g, "%27"); + s = s.replace(/\(/g, "%28"); + s = s.replace(/\)/g, "%29"); + return s; + } +, + decodePercent: function decodePercent(s) { + if (s != null) { + // Handle application/x-www-form-urlencoded, which is defined by + // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 + s = s.replace(/\+/g, " "); + } + return decodeURIComponent(s); + } +, + /** Convert the given parameters to an Array of name-value pairs. */ + getParameterList: function getParameterList(parameters) { + if (parameters == null) { + return []; + } + if (typeof parameters != "object") { + return decodeForm(parameters + ""); + } + if (parameters instanceof Array) { + return parameters; + } + var list = []; + for (var p in parameters) { + list.push([p, parameters[p]]); + } + return list; + } +, + /** Convert the given parameters to a map from name to value. */ + getParameterMap: function getParameterMap(parameters) { + if (parameters == null) { + return {}; + } + if (typeof parameters != "object") { + return getParameterMap(decodeForm(parameters + "")); + } + if (parameters instanceof Array) { + var map = {}; + for (var p = 0; p < parameters.length; ++p) { + var key = parameters[p][0]; + if (map[key] === undefined) { // first value wins + map[key] = parameters[p][1]; + } + } + return map; + } + return parameters; + } +, + getParameter: function getParameter(parameters, name) { + if (parameters instanceof Array) { + for (var p = 0; p < parameters.length; ++p) { + if (parameters[p][0] == name) { + return parameters[p][1]; // first value wins + } + } + } else { + return OAuth.getParameterMap(parameters)[name]; + } + return null; + } +, + formEncode: function formEncode(parameters) { + var form = ""; + var list = OAuth.getParameterList(parameters); + for (var p = 0; p < list.length; ++p) { + var value = list[p][1]; + if (value == null) value = ""; + if (form != "") form += '&'; + form += OAuth.percentEncode(list[p][0]) + +'='+ OAuth.percentEncode(value); + } + return form; + } +, + decodeForm: function decodeForm(form) { + var list = []; + var nvps = form.split('&'); + for (var n = 0; n < nvps.length; ++n) { + var nvp = nvps[n]; + if (nvp == "") { + continue; + } + var equals = nvp.indexOf('='); + var name; + var value; + if (equals < 0) { + name = OAuth.decodePercent(nvp); + value = null; + } else { + name = OAuth.decodePercent(nvp.substring(0, equals)); + value = OAuth.decodePercent(nvp.substring(equals + 1)); + } + list.push([name, value]); + } + return list; + } +, + setParameter: function setParameter(message, name, value) { + var parameters = message.parameters; + if (parameters instanceof Array) { + for (var p = 0; p < parameters.length; ++p) { + if (parameters[p][0] == name) { + if (value === undefined) { + parameters.splice(p, 1); + } else { + parameters[p][1] = value; + value = undefined; + } + } + } + if (value !== undefined) { + parameters.push([name, value]); + } + } else { + parameters = OAuth.getParameterMap(parameters); + parameters[name] = value; + message.parameters = parameters; + } + } +, + setParameters: function setParameters(message, parameters) { + var list = OAuth.getParameterList(parameters); + for (var i = 0; i < list.length; ++i) { + OAuth.setParameter(message, list[i][0], list[i][1]); + } + } +, + /** Fill in parameters to help construct a request message. + This function doesn't fill in every parameter. + The accessor object should be like: + {consumerKey:'foo', consumerSecret:'bar', accessorSecret:'nurn', token:'krelm', tokenSecret:'blah'} + The accessorSecret property is optional. + */ + completeRequest: function completeRequest(message, accessor) { + if (message.method == null) { + message.method = "GET"; + } + var map = OAuth.getParameterMap(message.parameters); + if (map.oauth_consumer_key == null) { + OAuth.setParameter(message, "oauth_consumer_key", accessor.consumerKey || ""); + } + if (map.oauth_token == null && accessor.token != null) { + OAuth.setParameter(message, "oauth_token", accessor.token); + } + if (map.oauth_version == null) { + OAuth.setParameter(message, "oauth_version", "1.0"); + } + if (map.oauth_timestamp == null) { + OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp()); + } + if (map.oauth_nonce == null) { + OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6)); + } + OAuth.SignatureMethod.sign(message, accessor); + } +, + setTimestampAndNonce: function setTimestampAndNonce(message) { + OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp()); + OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6)); + } +, + addToURL: function addToURL(url, parameters) { + newURL = url; + if (parameters != null) { + var toAdd = OAuth.formEncode(parameters); + if (toAdd.length > 0) { + var q = url.indexOf('?'); + if (q < 0) newURL += '?'; + else newURL += '&'; + newURL += toAdd; + } + } + return newURL; + } +, + /** Construct the value of the Authorization header for an HTTP request. */ + getAuthorizationHeader: function getAuthorizationHeader(realm, parameters) { + var header = 'OAuth realm="' + OAuth.percentEncode(realm) + '"'; + var list = OAuth.getParameterList(parameters); + for (var p = 0; p < list.length; ++p) { + var parameter = list[p]; + var name = parameter[0]; + if (name.indexOf("oauth_") == 0) { + header += ',' + OAuth.percentEncode(name) + '="' + OAuth.percentEncode(parameter[1]) + '"'; + } + } + return header; + } +, + timestamp: function timestamp() { + var d = new Date(); + return Math.floor(d.getTime()/1000); + } +, + nonce: function nonce(length) { + var chars = OAuth.nonce.CHARS; + var result = ""; + for (var i = 0; i < length; ++i) { + var rnum = Math.floor(Math.random() * chars.length); + result += chars.substring(rnum, rnum+1); + } + return result; + } +}); + +OAuth.nonce.CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; + +/** Define a constructor function, + without causing trouble to anyone who was using it as a namespace. + That is, if parent[name] already existed and had properties, + copy those properties into the new constructor. + */ +OAuth.declareClass = function declareClass(parent, name, newConstructor) { + var previous = parent[name]; + parent[name] = newConstructor; + if (newConstructor != null && previous != null) { + for (var key in previous) { + if (key != "prototype") { + newConstructor[key] = previous[key]; + } + } + } + return newConstructor; +} + +/** An abstract algorithm for signing messages. */ +OAuth.declareClass(OAuth, "SignatureMethod", function OAuthSignatureMethod(){}); + +OAuth.setProperties(OAuth.SignatureMethod.prototype, // instance members +{ + /** Add a signature to the message. */ + sign: function sign(message) { + var baseString = OAuth.SignatureMethod.getBaseString(message); + var signature = this.getSignature(baseString); + OAuth.setParameter(message, "oauth_signature", signature); + return signature; // just in case someone's interested + } +, + /** Set the key string for signing. */ + initialize: function initialize(name, accessor) { + var consumerSecret; + if (accessor.accessorSecret != null + && name.length > 9 + && name.substring(name.length-9) == "-Accessor") + { + consumerSecret = accessor.accessorSecret; + } else { + consumerSecret = accessor.consumerSecret; + } + this.key = OAuth.percentEncode(consumerSecret) + +"&"+ OAuth.percentEncode(accessor.tokenSecret); + } +}); + +/* SignatureMethod expects an accessor object to be like this: + {tokenSecret: "lakjsdflkj...", consumerSecret: "QOUEWRI..", accessorSecret: "xcmvzc..."} + The accessorSecret property is optional. + */ +// Class members: +OAuth.setProperties(OAuth.SignatureMethod, // class members +{ + sign: function sign(message, accessor) { + var name = OAuth.getParameterMap(message.parameters).oauth_signature_method; + if (name == null || name == "") { + name = "HMAC-SHA1"; + OAuth.setParameter(message, "oauth_signature_method", name); + } + OAuth.SignatureMethod.newMethod(name, accessor).sign(message); + } +, + /** Instantiate a SignatureMethod for the given method name. */ + newMethod: function newMethod(name, accessor) { + var impl = OAuth.SignatureMethod.REGISTERED[name]; + if (impl != null) { + var method = new impl(); + method.initialize(name, accessor); + return method; + } + var err = new Error("signature_method_rejected"); + var acceptable = ""; + for (var r in OAuth.SignatureMethod.REGISTERED) { + if (acceptable != "") acceptable += '&'; + acceptable += OAuth.percentEncode(r); + } + err.oauth_acceptable_signature_methods = acceptable; + throw err; + } +, + /** A map from signature method name to constructor. */ + REGISTERED : {} +, + /** Subsequently, the given constructor will be used for the named methods. + The constructor will be called with no parameters. + The resulting object should usually implement getSignature(baseString). + You can easily define such a constructor by calling makeSubclass, below. + */ + registerMethodClass: function registerMethodClass(names, classConstructor) { + for (var n = 0; n < names.length; ++n) { + OAuth.SignatureMethod.REGISTERED[names[n]] = classConstructor; + } + } +, + /** Create a subclass of OAuth.SignatureMethod, with the given getSignature function. */ + makeSubclass: function makeSubclass(getSignatureFunction) { + var superClass = OAuth.SignatureMethod; + var subClass = function() { + superClass.call(this); + }; + subClass.prototype = new superClass(); + // Delete instance variables from prototype: + // delete subclass.prototype... There aren't any. + subClass.prototype.getSignature = getSignatureFunction; + subClass.prototype.constructor = subClass; + return subClass; + } +, + getBaseString: function getBaseString(message) { + var URL = message.action; + var q = URL.indexOf('?'); + var parameters; + if (q < 0) { + parameters = message.parameters; + } else { + // Combine the URL query string with the other parameters: + parameters = OAuth.decodeForm(URL.substring(q + 1)); + var toAdd = OAuth.getParameterList(message.parameters); + for (var a = 0; a < toAdd.length; ++a) { + parameters.push(toAdd[a]); + } + } + return OAuth.percentEncode(message.method.toUpperCase()) + +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeUrl(URL)) + +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeParameters(parameters)); + } +, + normalizeUrl: function normalizeUrl(url) { + var uri = OAuth.SignatureMethod.parseUri(url); + var scheme = uri.protocol.toLowerCase(); + var authority = uri.authority.toLowerCase(); + var dropPort = (scheme == "http" && uri.port == 80) + || (scheme == "https" && uri.port == 443); + if (dropPort) { + // find the last : in the authority + var index = authority.lastIndexOf(":"); + if (index >= 0) { + authority = authority.substring(0, index); + } + } + var path = uri.path; + if (!path) { + path = "/"; // conforms to RFC 2616 section 3.2.2 + } + // we know that there is no query and no fragment here. + return scheme + "://" + authority + path; + } +, + parseUri: function parseUri (str) { + /* This function was adapted from parseUri 1.2.1 + http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js + */ + var o = {key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + parser: {strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ }}; + var m = o.parser.strict.exec(str); + var uri = {}; + var i = 14; + while (i--) uri[o.key[i]] = m[i] || ""; + return uri; + } +, + normalizeParameters: function normalizeParameters(parameters) { + if (parameters == null) { + return ""; + } + var list = OAuth.getParameterList(parameters); + var sortable = []; + for (var p = 0; p < list.length; ++p) { + var nvp = list[p]; + if (nvp[0] != "oauth_signature") { + sortable.push([ OAuth.percentEncode(nvp[0]) + + " " // because it comes before any character that can appear in a percentEncoded string. + + OAuth.percentEncode(nvp[1]) + , nvp]); + } + } + sortable.sort(function(a,b) { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + return 0; + }); + var sorted = []; + for (var s = 0; s < sortable.length; ++s) { + sorted.push(sortable[s][1]); + } + return OAuth.formEncode(sorted); + } +}); + +OAuth.SignatureMethod.registerMethodClass(["PLAINTEXT", "PLAINTEXT-Accessor"], + OAuth.SignatureMethod.makeSubclass( + function getSignature(baseString) { + return this.key; + } + )); + +OAuth.SignatureMethod.registerMethodClass(["HMAC-SHA1", "HMAC-SHA1-Accessor"], + OAuth.SignatureMethod.makeSubclass( + function getSignature(baseString) { + b64pad = '='; + var signature = b64_hmac_sha1(this.key, baseString); + return signature; + } + )); diff --git a/share/www/script/sha1.js b/share/www/script/sha1.js new file mode 100644 index 00000000..1b559823 --- /dev/null +++ b/share/www/script/sha1.js @@ -0,0 +1,202 @@ +/*
+ * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
+ * in FIPS PUB 180-1
+ * Version 2.1a Copyright Paul Johnston 2000 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for details.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
+var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
+var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
+function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
+function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
+function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
+function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
+function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function sha1_vm_test()
+{
+ return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
+}
+
+/*
+ * Calculate the SHA-1 of an array of big-endian words, and a bit length
+ */
+function core_sha1(x, len)
+{
+ /* append padding */
+ x[len >> 5] |= 0x80 << (24 - len % 32);
+ x[((len + 64 >> 9) << 4) + 15] = len;
+
+ var w = Array(80);
+ var a = 1732584193;
+ var b = -271733879;
+ var c = -1732584194;
+ var d = 271733878;
+ var e = -1009589776;
+
+ for(var i = 0; i < x.length; i += 16)
+ {
+ var olda = a;
+ var oldb = b;
+ var oldc = c;
+ var oldd = d;
+ var olde = e;
+
+ for(var j = 0; j < 80; j++)
+ {
+ if(j < 16) w[j] = x[i + j];
+ else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
+ var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
+ safe_add(safe_add(e, w[j]), sha1_kt(j)));
+ e = d;
+ d = c;
+ c = rol(b, 30);
+ b = a;
+ a = t;
+ }
+
+ a = safe_add(a, olda);
+ b = safe_add(b, oldb);
+ c = safe_add(c, oldc);
+ d = safe_add(d, oldd);
+ e = safe_add(e, olde);
+ }
+ return Array(a, b, c, d, e);
+
+}
+
+/*
+ * Perform the appropriate triplet combination function for the current
+ * iteration
+ */
+function sha1_ft(t, b, c, d)
+{
+ if(t < 20) return (b & c) | ((~b) & d);
+ if(t < 40) return b ^ c ^ d;
+ if(t < 60) return (b & c) | (b & d) | (c & d);
+ return b ^ c ^ d;
+}
+
+/*
+ * Determine the appropriate additive constant for the current iteration
+ */
+function sha1_kt(t)
+{
+ return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
+ (t < 60) ? -1894007588 : -899497514;
+}
+
+/*
+ * Calculate the HMAC-SHA1 of a key and some data
+ */
+function core_hmac_sha1(key, data)
+{
+ var bkey = str2binb(key);
+ if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);
+
+ var ipad = Array(16), opad = Array(16);
+ for(var i = 0; i < 16; i++)
+ {
+ ipad[i] = bkey[i] ^ 0x36363636;
+ opad[i] = bkey[i] ^ 0x5C5C5C5C;
+ }
+
+ var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
+ return core_sha1(opad.concat(hash), 512 + 160);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function rol(num, cnt)
+{
+ return (num << cnt) | (num >>> (32 - cnt));
+}
+
+/*
+ * Convert an 8-bit or 16-bit string to an array of big-endian words
+ * In 8-bit function, characters >255 have their hi-byte silently ignored.
+ */
+function str2binb(str)
+{
+ var bin = Array();
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < str.length * chrsz; i += chrsz)
+ bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
+ return bin;
+}
+
+/*
+ * Convert an array of big-endian words to a string
+ */
+function binb2str(bin)
+{
+ var str = "";
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < bin.length * 32; i += chrsz)
+ str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
+ return str;
+}
+
+/*
+ * Convert an array of big-endian words to a hex string.
+ */
+function binb2hex(binarray)
+{
+ var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+ var str = "";
+ for(var i = 0; i < binarray.length * 4; i++)
+ {
+ str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
+ hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
+ }
+ return str;
+}
+
+/*
+ * Convert an array of big-endian words to a base-64 string
+ */
+function binb2b64(binarray)
+{
+ var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ var str = "";
+ for(var i = 0; i < binarray.length * 4; i += 3)
+ {
+ var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16)
+ | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )
+ | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
+ for(var j = 0; j < 4; j++)
+ {
+ if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
+ else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
+ }
+ }
+ return str;
+}
diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index 1800ca82..d8abcc71 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -195,8 +195,8 @@ couchTests.changes = function(debug) { // test for userCtx run_on_modified_server( [{section: "httpd", - key: "authentication_handler", - value: "{couch_httpd, special_test_authentication_handler}"}, + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, {section:"httpd", key: "WWW-Authenticate", value: "X-Couch-Test-Auth"}], diff --git a/share/www/script/test/cookie_auth.js b/share/www/script/test/cookie_auth.js new file mode 100644 index 00000000..63ab21b9 --- /dev/null +++ b/share/www/script/test/cookie_auth.js @@ -0,0 +1,162 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.cookie_auth = function(debug) { + // This tests cookie-based authentication. + + var db = new CouchDB("test_suite_db"); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + // Simple secret key generator + function generateSecret(length) { + var secret = ''; + for (var i=0; i<length; i++) { + secret += String.fromCharCode(Math.floor(Math.random() * 256)); + } + return secret; + } + // this function will be called on the modified server + var testFun = function () { + try { + // try using an invalid cookie + var usersDb = new CouchDB("test_suite_users"); + usersDb.deleteDb(); + usersDb.createDb(); + + var password = "3.141592653589"; + + // Create a user + T(usersDb.save({ + _id: "a1", + salt: "123", + password_sha: "8da1CtkFvb58LWrnup5chgdZVUs=", + username: "Jason Davies", + author: "Jason Davies", + type: "user", + roles: ["_admin"] + }).ok); + + var validationDoc = { + _id : "_design/validate", + validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) { + // docs should have an author field. + if (!newDoc._deleted && !newDoc.author) { + throw {forbidden: + "Documents must have an author field"}; + } + if (oldDoc && oldDoc.author != userCtx.name) { + throw {unauthorized: + "You are not the author of this document. You jerk."+userCtx.name}; + } + }).toString() + ")" + }; + + T(db.save(validationDoc).ok); + + + + T(CouchDB.login('Jason Davies', password).ok); + // update the credentials document + var doc = usersDb.open("a1"); + doc.foo=2; + T(usersDb.save(doc).ok); + + // Save a document that's missing an author field. + try { + // db has a validation function + db.save({foo:1}); + T(false && "Can't get here. Should have thrown an error 2"); + } catch (e) { + T(e.error == "forbidden"); + T(db.last_req.status == 403); + } + + // TODO should login() throw an exception here? + T(!CouchDB.login('Jason Davies', "2.71828").ok); + T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok); + + // test redirect + xhr = CouchDB.request("POST", "/_session?next=/", { + headers: {"Content-Type": "application/x-www-form-urlencoded"}, + body: "username=Jason%20Davies&password="+encodeURIComponent(password) + }); + // should this be a redirect code instead of 200? + T(xhr.status == 200); + + usersDb.deleteDb(); + // test user creation + T(CouchDB.createUser("test", "testpassword", "test@somemail.com", ['read', 'write']).ok); + + // make sure we create a unique user + T(!CouchDB.createUser("test", "testpassword2", "test2@somemail.com", ['read', 'write']).ok); + + // test login + T(CouchDB.login("test", "testpassword").ok); + T(!CouchDB.login('test', "testpassword2").ok); + + // test update user without changing password + T(CouchDB.updateUser("test", "test2@somemail.com").ok); + result = usersDb.view("_auth/users", {key: "test"}); + T(result.rows[0].value['email'] == "test2@somemail.com"); + + + // test changing password + result = usersDb.view("_auth/users", {key: "test"}); + T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "testpassword").ok); + result1 = usersDb.view("_auth/users", {key: "test"}); + T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']); + + + // test changing password with passing old password + T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2").ok); + + // test changing password whith bad old password + T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "badpasswword").ok); + + // Only admins can change roles + T(!CouchDB.updateUser("test", "test2@somemail.com", ['read', 'write']).ok); + + T(CouchDB.logout().ok); + + T(CouchDB.updateUser("test", "test2@somemail.com").ok); + result = usersDb.view("_auth/users", {key: "test"}); + T(result.rows[0].value['email'] == "test2@somemail.com"); + + // test changing password, we don't need to set old password when we are admin + result = usersDb.view("_auth/users", {key: "test"}); + T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword3").ok); + result1 = usersDb.view("_auth/users", {key: "test"}); + T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']); + + // Only admins can change roles + T(CouchDB.updateUser("test", "test2@somemail.com", ['read']).ok); + + } finally { + // Make sure we erase any auth cookies so we don't affect other tests + T(CouchDB.logout().ok); + } + }; + + run_on_modified_server( + [{section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_auth, cookie_authentication_handler}"}, + {section: "couch_httpd_auth", + key: "secret", value: generateSecret(64)}, + {section: "couch_httpd_auth", + key: "authentication_db", value: "test_suite_users"}], + testFun + ); + +}; diff --git a/share/www/script/test/oauth.js b/share/www/script/test/oauth.js new file mode 100644 index 00000000..fb4ab818 --- /dev/null +++ b/share/www/script/test/oauth.js @@ -0,0 +1,166 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +couchTests.oauth = function(debug) { + // This tests OAuth authentication. + + var authorization_url = "/_oauth/authorize"; + + var db = new CouchDB("test_suite_db"); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + var dbA = new CouchDB("test_suite_db_a"); + var dbB = new CouchDB("test_suite_db_b"); + dbA.deleteDb(); + dbA.createDb(); + dbB.deleteDb(); + dbB.createDb(); + + // Simple secret key generator + function generateSecret(length) { + var secret = ''; + for (var i=0; i<length; i++) { + secret += String.fromCharCode(Math.floor(Math.random() * 256)); + } + return secret; + } + + function oauthRequest(path, message, accessor, method) { + message.action = path; + message.method = method || 'GET'; + OAuth.SignatureMethod.sign(message, accessor); + var parameters = message.parameters; + if (method == "POST" || method == "GET") { + if (method == "GET") { + return CouchDB.request("GET", OAuth.addToURL(path, parameters)); + } else { + return CouchDB.request("POST", path, { + headers: {"Content-Type": "application/x-www-form-urlencoded"}, + body: OAuth.formEncode(parameters) + }); + } + } else { + return CouchDB.request("GET", path, { + headers: {Authorization: OAuth.getAuthorizationHeader('', parameters)} + }); + } + } + + var consumerSecret = generateSecret(64); + var tokenSecret = generateSecret(64); + + var host = CouchDB.host; + var dbPair = { + source: { + url: "http://" + host + "/test_suite_db_a", + auth: { + oauth: { + consumer_key: "key", + consumer_secret: consumerSecret, + token_secret: tokenSecret, + token: "foo" + } + } + }, + target: "http://" + host + "/test_suite_db_b" + }; + + // this function will be called on the modified server + var testFun = function () { + try { + var usersDb = new CouchDB("test_suite_users"); + usersDb.deleteDb(); + usersDb.createDb(); + + // Create a user + T(CouchDB.createUser("jason", "testpassword", "test@somemail.com", ['test'], true).ok); + + var accessor = { + consumerSecret: consumerSecret, + tokenSecret: tokenSecret + }; + + var signatureMethods = ["PLAINTEXT", "HMAC-SHA1"]; + var consumerKeys = {key: 200, nonexistent_key: 400}; + + for (var i=0; i<signatureMethods.length; i++) { + for (var consumerKey in consumerKeys) { + var expectedCode = consumerKeys[consumerKey]; + var message = { + parameters: { + oauth_signature_method: signatureMethods[i], + oauth_consumer_key: consumerKey, + oauth_token: "foo", + oauth_token_secret: tokenSecret, + oauth_version: "1.0" + } + }; + + // Get request token via Authorization header + xhr = oauthRequest("http://" + host + "/_oauth/request_token", message, accessor); + T(xhr.status == expectedCode); + + // GET request token via query parameters + xhr = oauthRequest("http://" + host + "/_oauth/request_token", message, accessor, "GET"); + T(xhr.status == expectedCode); + + responseMessage = OAuth.decodeForm(xhr.responseText); + + // Obtaining User Authorization + //Only needed for 3-legged OAuth + //xhr = CouchDB.request("GET", authorization_url + '?oauth_token=' + responseMessage.oauth_token); + //T(xhr.status == expectedCode); + + xhr = oauthRequest("http://" + host + "/_session", message, accessor); + T(xhr.status == expectedCode); + if (xhr.status == expectedCode == 200) { + data = JSON.parse(xhr.responseText); + T(data.name == "jason"); + T(data.roles[0] == "test"); + } + + xhr = oauthRequest("http://" + host + "/_session?foo=bar", message, accessor); + T(xhr.status == expectedCode); + + // Replication + var result = CouchDB.replicate(dbPair.source, dbPair.target); + T(result.ok); + } + } + + } finally { + } + }; + + run_on_modified_server( + [{section: "httpd", + key: "WWW-Authenticate", value: 'Basic realm="administrator",OAuth'}, + {section: "couch_httpd_auth", + key: "secret", value: generateSecret(64)}, + {section: "couch_httpd_auth", + key: "authentication_db", value: "test_suite_users"}, + {section: "oauth_consumer_secrets", + key: "key", value: consumerSecret}, + {section: "oauth_token_users", + key: "foo", value: "jason"}, + {section: "oauth_token_secrets", + key: "foo", value: tokenSecret}, + {section: "couch_httpd_oauth", + key: "authorization_url", value: authorization_url}, + {section: "httpd", + key: "authentication_handlers", + value: "{couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"}], + testFun + ); +}; diff --git a/share/www/script/test/security_validation.js b/share/www/script/test/security_validation.js index 10553e5e..52f895bd 100644 --- a/share/www/script/test/security_validation.js +++ b/share/www/script/test/security_validation.js @@ -39,14 +39,14 @@ couchTests.security_validation = function(debug) { run_on_modified_server( [{section: "httpd", - key: "authentication_handler", - value: "{couch_httpd, special_test_authentication_handler}"}, + key: "authentication_handlers", + value: "{couch_httpd_auth, special_test_authentication_handler}"}, {section:"httpd", key: "WWW-Authenticate", value: "X-Couch-Test-Auth"}], function () { - // try saving document usin the wrong credentials + // try saving document using the wrong credentials var wrongPasswordDb = new CouchDB("test_suite_db", {"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:foo"} ); @@ -59,8 +59,8 @@ couchTests.security_validation = function(debug) { T(wrongPasswordDb.last_req.status == 401); } - // test force_login=true. - var resp = wrongPasswordDb.request("GET", "/_whoami?force_login=true"); + // test force basic login + var resp = wrongPasswordDb.request("GET", "/_session?basic=true"); var err = JSON.parse(resp.responseText); T(err.error == "unauthorized"); T(resp.status == 401); @@ -104,7 +104,7 @@ couchTests.security_validation = function(debug) { T(userDb.save(designDoc).ok); // test the _whoami endpoint - var resp = userDb.request("GET", "/_whoami"); + var resp = userDb.request("GET", "/_session"); var user = JSON.parse(resp.responseText) T(user.name == "Damien Katz"); // test that the roles are listed properly diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index a1138596..c87cfb0b 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -335,7 +335,7 @@ couchTests.show_documents = function(debug) { var doc2 = {_id:"foo", a:2}; db.save(doc1); - //create the conflict with a all_or_nothing bulk docs request + // create the conflict with an all_or_nothing bulk docs request var docs = [doc2]; db.bulkSave(docs, {all_or_nothing:true}); |