summaryrefslogtreecommitdiff
path: root/share
diff options
context:
space:
mode:
authorDamien F. Katz <damien@apache.org>2009-08-04 19:50:46 +0000
committerDamien F. Katz <damien@apache.org>2009-08-04 19:50:46 +0000
commit8e2215ee6306b0f4c13553796d401e9f5f93bcb6 (patch)
tree948b9179887e73379bc445b9ad058de3a0bbe870 /share
parentfd72a9bc48ebab76976f538c28459a0e26aa1750 (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.am4
-rw-r--r--share/www/script/couch.js61
-rw-r--r--share/www/script/couch_tests.js4
-rw-r--r--share/www/script/jquery.couch.js35
-rw-r--r--share/www/script/oauth.js511
-rw-r--r--share/www/script/sha1.js202
-rw-r--r--share/www/script/test/changes.js4
-rw-r--r--share/www/script/test/cookie_auth.js162
-rw-r--r--share/www/script/test/oauth.js166
-rw-r--r--share/www/script/test/security_validation.js12
-rw-r--r--share/www/script/test/show_documents.js2
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});