Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 6b60c5f

Browse files
authored
Merge pull request #166 from glynnbird/cookies
New cookieauth plugin re issue #120
2 parents bbb1676 + 6e550bb commit 6b60c5f

File tree

4 files changed

+365
-43
lines changed

4 files changed

+365
-43
lines changed

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,17 @@ Cloudant({hostname:"companycloudant.local", username:"somebody", password:"someb
209209

210210
### Request Plugins
211211

212-
This library can be used with one of three `request` plugins:
212+
This library can be used with one of these `request` plugins:
213213

214214
1. `default` - the default [request](https://www.npmjs.com/package/request) library plugin. This uses Node.js's callbacks to communicate Cloudant's replies
215215
back to your app and can be used to stream data using the Node.js [Stream API](https://nodejs.org/api/stream.html).
216216
2. `promises` - if you'd prefer to write code in the Promises style then the "promises" plugin turns each request into a Promise. This plugin cannot be used
217217
stream data because instead of returning the HTTP request, we are simply returning a Promise instead.
218218
3. `retry` - on occasion, Cloudant's multi-tenant offerring may reply with an HTTP 429 response because you've exceed the number of API requests in a given amount of time.
219219
The "retry" plugin will automatically retry your request with exponential back-off. The 'retry' plugin can be used to stream data.
220-
4. custom plugin - you may also supply your own function which will be called to make API calls.
220+
4. `cookieauth` - this plugin will automatically swap your Cloudant credentials for a cookie transparently for you. It will handle the authentication for you
221+
and ensure that the cookie is refreshed. The 'cookieauth' plugin can be used to stream data.
222+
5. custom plugin - you may also supply your own function which will be called to make API calls.
221223

222224
#### The 'promises' Plugins
223225

@@ -257,6 +259,23 @@ var cloudant = Cloudant({url: myurl, plugin:'retry', retryAttempts:5, retryTimeo
257259
var mydb = cloudant.db.use('mydb');
258260
```
259261

262+
#### The 'cookieauth' plugin
263+
264+
When initialising the Cloudant library, you can opt to use the 'retry' plugin:
265+
266+
```js
267+
var cloudant = Cloudant({url: myurl, plugin:'cookieauth'});
268+
var mydb = cloudant.db.use('mydb');
269+
mydb.get('mydoc', function(err, data) {
270+
271+
});
272+
```
273+
274+
The above code will transparently call `POST /_session` to exchange your credentials for a cookie and then call `GET /mydoc` to fetch the document.
275+
276+
Subsequent calls to the same `cloudant` instance will simply use cookie authentication from that point. The library will automatically ensure that the cookie remains
277+
up-to-date by calling Cloudant on an hourly basis to refresh the cookie.
278+
260279
#### Custom plugin
261280

262281
When initialising the Cloudant library, you can supply your own plugin function:

cloudant.js

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = Cloudant;
1717
var Nano = require('cloudant-nano');
1818
var debug = require('debug')('cloudant');
1919
var nanodebug = require('debug')('nano');
20+
var async = require('async');
2021

2122

2223
// function from the old Cloudant library to
@@ -242,49 +243,38 @@ function Cloudant(options, callback) {
242243

243244
function ping(login, callback) {
244245
var nano = this;
245-
246-
if (!callback) {
247-
callback = login;
248-
login = null;
249-
}
250-
251-
// Only call back once.
252-
var inner_callback = callback;
253-
callback = function(er, result, cookie) {
254-
inner_callback(er, result, cookie);
255-
inner_callback = function() {};
256-
};
257-
258246
var cookie = null;
259-
var done = {welcome:false, session:false, auth:true};
260-
nano.session( function(er, body) { returned('session', er, body); });
261-
nano.relax({db:""}, function(er, body) { returned('welcome', er, body); });
262-
263-
// If credentials are supplied, authenticate to get a cookie.
264-
if (login && login.username && login.password) {
265-
done.auth = false;
266-
nano.auth(login.username, login.password, function(er, body, headers) {
267-
returned('auth', er, body, headers);
268-
});
269-
}
270-
271-
function returned(type, er, body, headers) {
272-
if (er)
273-
return callback(er);
274247

275-
debug('Pong/%s %j', type, body);
276-
done[type] = body;
277-
278-
if (type == 'auth') {
279-
if (headers['set-cookie'] && headers['set-cookie'][0]) {
280-
cookie = headers['set-cookie'][0].replace(/;.*$/, '');
248+
async.series([
249+
function(done) {
250+
if (login && login.username && login.password) {
251+
done.auth = false;
252+
nano.auth(login.username, login.password, function(e, b, h) {
253+
cookie = (h && h['set-cookie']) || null;
254+
if (cookie) {
255+
cookie = cookie[0];
256+
}
257+
done(null, b);
258+
});
259+
} else {
260+
done(null, null);
281261
}
262+
},
263+
function(done) {
264+
nano.session(function(e, b, h) {
265+
done(null, b);
266+
});
267+
},
268+
function(done) {
269+
nano.relax({db:''}, function(e, b, h) {
270+
done(null, b);
271+
})
282272
}
283-
284-
if (done.welcome && done.session && done.auth) {
285-
// Return the CouchDB "Welcome" body but with the userCtx added in.
286-
done.welcome.userCtx = done.session.userCtx;
287-
callback(null, done.welcome, cookie);
288-
}
289-
}
273+
], function(err, data) {
274+
var body = (data && data[2]) || {};
275+
body.userCtx = (data && data[1] && data[1].userCtx) || {};
276+
callback(null, body, cookie);
277+
});
290278
}
279+
280+

plugins/cookieauth.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* Copyright (c) 2015 IBM Cloudant, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11+
* either express or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
// this the the 'cookieauth' request handler.
16+
// Instead of passing the authentication credentials using HTTP Basic Auth with every request
17+
// we exchange the credentials for a cookie which we remember and pass back with each
18+
// subsequent request.
19+
var async = require('async');
20+
var debug = require('debug')('cloudant');
21+
var stream = require('stream');
22+
var u = require('url');
23+
var nullcallback = function() {};
24+
25+
module.exports = function(options) {
26+
var requestDefaults = options.requestDefaults || {};
27+
var request = require('request').defaults(requestDefaults);
28+
var jar = request.jar();
29+
var cookieRefresh = null;
30+
31+
// make a request using cookie authentication
32+
// 1) if we have a cookie or have no credentials, just try the request
33+
// 2) otherwise, get session cookie
34+
// 3) then try the request
35+
var cookieRequest = function (req, callback) {
36+
37+
// deal with absence of callback
38+
if (typeof callback !== 'function') {
39+
callback = nullcallback;
40+
}
41+
42+
// parse the url to extract credentials and calculate
43+
// stuburl - the cloudant url without credentials or auth
44+
// auth - whether there are credentials or not
45+
// credentials - object containing username & password
46+
var url = req.uri || req.url;
47+
var parsed = u.parse(url);
48+
var auth = parsed.auth;
49+
var credentials = null;
50+
delete parsed.auth;
51+
delete parsed.href;
52+
url = u.format(parsed);
53+
if (auth) {
54+
var bits = auth.split(':');
55+
credentials = {
56+
username: bits[0],
57+
password: bits[1]
58+
};
59+
}
60+
req.url = url;
61+
delete req.uri;
62+
delete parsed.path;
63+
delete parsed.pathname;
64+
var stuburl = u.format(parsed).replace(/\/$/,'');
65+
66+
// to maintain streaming compatiblity, always return a PassThrough stream
67+
var s = new stream.PassThrough();
68+
69+
// run these three things in series
70+
async.series([
71+
72+
// call the request being asked for
73+
function(done) {
74+
75+
// do we have cookie for this domain name?
76+
var cookies = jar.getCookies(stuburl);
77+
var statusCode = 500;
78+
79+
// if we have a cookie for this domain, then we can try the required API call straight away
80+
if (!auth || cookies.length > 0) {
81+
debug('we have cookies (or no credentials) so attempting API call straight away');
82+
req.jar = jar;
83+
request(req, function(e, h, b) {
84+
// if we have no credentials or we suceeded
85+
if (!auth || (statusCode >= 200 && statusCode < 400)) {
86+
// returning an err of true stops the async sequence
87+
// we're good because we didn't get a 4** or 5**
88+
done(true, [e,h,b]);
89+
} else {
90+
91+
// continue with the async chain
92+
done(null, [e,h,b]);
93+
}
94+
}).on('response', function(r) {
95+
statusCode = r && r.statusCode || 500;
96+
}).on('data', function(chunk) {
97+
// only write to the output stream on success
98+
if (statusCode < 400) {
99+
s.write(chunk);
100+
}
101+
});
102+
103+
} else {
104+
debug('we have no cookies - need to authenticate first');
105+
// we have no cookies so we need to authenticate first
106+
// i.e. do nothing here
107+
done(null, null);
108+
}
109+
110+
},
111+
112+
// call POST /_session to get a cookie
113+
function(done) {
114+
debug('need to authenticate - calling POST /_session');
115+
var r = {
116+
url: stuburl + '/_session',
117+
method: 'post',
118+
form: {
119+
name: credentials.username,
120+
password: credentials.password
121+
},
122+
jar: jar
123+
};
124+
request(r, function(e, h, b) {
125+
var statusCode = h && h.statusCode || 500;
126+
// if we sucessfully authenticate
127+
if (statusCode >= 200 && statusCode < 400) {
128+
// continue to the next stage of the async chain
129+
debug('authentication successful');
130+
131+
// if we don't already have a timer set to refresh the cookie every hour,
132+
// set one up
133+
if (!cookieRefresh) {
134+
debug('setting up recurring cookie refresh request');
135+
cookieRefresh = setInterval(function() {
136+
debug('refreshing cookie');
137+
request({method: 'get', url: stuburl + '/_session', jar: jar});
138+
}, 1000 * 60 * 60);
139+
// prevent setInterval from requiring the event loop to be active
140+
cookieRefresh.unref();
141+
}
142+
143+
done(null, [e,h,b]);
144+
} else {
145+
// failed to authenticate - no point proceeding any further
146+
debug('authentication failed');
147+
done(true, [e,h,b]);
148+
}
149+
});
150+
},
151+
152+
// call the request being asked for with cookie authentication
153+
function(done) {
154+
debug('attempting API call with cookie');
155+
var statusCode = 500;
156+
req.jar = jar;
157+
request(req, function(e, h, b) {
158+
done(null, [e,h,b]);
159+
}).on('response', function(r) {
160+
statusCode = r && r.statusCode || 500;
161+
}).on('data', function(chunk) {
162+
if (statusCode < 400) {
163+
s.write(chunk);
164+
}
165+
});
166+
}
167+
], function(err, data) {
168+
// callback with the last call we made
169+
if (data && data.length > 0) {
170+
var reply = data[data.length - 1];
171+
// error, headers, body
172+
callback(reply[0], reply[1], reply[2]);
173+
} else {
174+
callback(err, { statusCode: 500 }, null);
175+
}
176+
});
177+
178+
// return the pass-through stream
179+
return s;
180+
};
181+
182+
183+
return cookieRequest;
184+
};
185+

0 commit comments

Comments
 (0)