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

Commit ab2e1be

Browse files
committed
Use new IamTokenManager class.
1 parent 78f1a4b commit ab2e1be

File tree

5 files changed

+80
-175
lines changed

5 files changed

+80
-175
lines changed

README.md

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,11 @@ var cloudant = Cloudant({ url: myurl, maxAttempt: 5, plugins: [ { iamauth: { iam
392392
This plugin will automatically exchange your IAM API key for a token. It will
393393
handle the authentication and ensure that the token is refreshed as required.
394394
395+
For example:
396+
```js
397+
var cloudant = Cloudant({ url: 'https://examples.cloudant.com', plugins: { iamauth: { iamApiKey: 'xxxxxxxxxx' } } });
398+
```
399+
395400
The production IAM token service at https://iam.cloud.ibm.com/identity/token is
396401
used by default. You can set `iamTokenUrl` in your plugin configuration to
397402
override this. To authenticate with the IAM token service set `iamClientId`
@@ -403,22 +408,6 @@ var cloudant = Cloudant({ url: myurl, maxAttempt: 5, plugins: [ { iamauth: { iam
403408
client request. It also increases the number of token exchange attempts and
404409
therefore may result in rate limiting by the IAM token service.
405410
406-
The retry behavior can be configured using the following options:
407-
408-
- `retryDelayMultiplier`
409-
410-
The multiplication factor used for increasing the timeout after each
411-
subsequent attempt _(default: 2)_.
412-
413-
- `retryInitialDelayMsecs`
414-
415-
The initial retry delay in milliseconds _(default: 500)_.
416-
417-
For example:
418-
```js
419-
var cloudant = Cloudant({ url: 'https://examples.cloudant.com', plugins: { iamauth: { iamApiKey: 'xxxxxxxxxx', retryDelayMultiplier: 4, retryInitialDelayMsecs: 100 } } });
420-
```
421-
422411
If the IAM token cannot be retrieved after the configured number of retries
423412
(either because the IAM token service is down or the IAM API key is
424413
incorrect) then an error is returned to the client.

lib/tokens/IamTokenManager.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class IAMTokenManager extends TokenManager {
5757
if (body.access_token) {
5858
accessToken = body.access_token;
5959
debug('Retrieved access token from IAM token service.');
60-
callback();
60+
callback(null, response);
6161
} else {
6262
callback(new Error('Invalid response from IAM token service'), response);
6363
}
@@ -87,12 +87,14 @@ class IAMTokenManager extends TokenManager {
8787
}
8888
});
8989
}
90-
], done);
90+
], (error, responses) => {
91+
done(error, responses[responses.length - 1]);
92+
});
9193
}
9294

9395
setIamApiKey(newIamApiKey) {
9496
this._iamApiKey = newIamApiKey;
95-
this.setForceRenew();
97+
this.attemptTokenRenewal = true;
9698
}
9799
}
98100

lib/tokens/TokenManager.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ class TokenManager {
3333
debug('Auto renewing token now...');
3434
this._renew().then((response) => {
3535
let maxAgeSecs = cookie.parse(response.headers['set-cookie'][0])['Max-Age'] || defaultMaxAgeSecs;
36-
let delaySecs = maxAgeSecs / 2;
37-
debug(`Renewing token in ${delaySecs} seconds.`);
38-
setTimeout(this._autoRenew.bind(this), delaySecs * 1000).unref();
36+
let delayMSecs = maxAgeSecs / 2 * 1000;
37+
debug(`Renewing token in ${delayMSecs} milliseconds.`);
38+
setTimeout(this._autoRenew.bind(this), delayMSecs).unref();
3939
}).catch((error) => {
4040
debug(`Failed to auto renew token - ${error}. Retrying in 60 seconds.`);
4141
setTimeout(this._autoRenew.bind(this), 60000).unref();

plugins/iamauth.js

Lines changed: 45 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
// limitations under the License.
1414
'use strict';
1515

16-
const async = require('async');
1716
const debug = require('debug')('cloudant:plugins:iamauth');
1817
const request = require('request');
1918
const u = require('url');
2019

2120
const BasePlugin = require('./base.js');
21+
const IAMTokenManager = require('../lib/tokens/IamTokenManager');
2222

2323
/**
2424
* IAM Authentication plugin.
@@ -29,165 +29,77 @@ class IAMPlugin extends BasePlugin {
2929
throw new Error('Missing IAM API key from configuration');
3030
}
3131

32-
// token service retry configuration
3332
cfg = Object.assign({
34-
retryDelayMultiplier: 2,
35-
retryInitialDelayMsecs: 500
33+
autoRenew: true,
34+
iamTokenUrl: 'https://iam.cloud.ibm.com/identity/token',
35+
retryDelayMsecs: 1000
3636
}, cfg);
3737

3838
super(client, cfg);
3939

40-
this.currentIamApiKey = null;
41-
this.baseUrl = cfg.baseUrl || null;
42-
this.cookieJar = null;
43-
this.tokenUrl = cfg.iamTokenUrl || 'https://iam.cloud.ibm.com/identity/token';
44-
this.refreshRequired = true;
40+
let sessionUrl = new u.URL(cfg.serverUrl);
41+
sessionUrl.pathname = '/_iam_session';
42+
43+
this._jar = request.jar();
44+
45+
this._tokenManager = new IAMTokenManager(
46+
client,
47+
this._jar,
48+
u.format(sessionUrl, {auth: false}),
49+
cfg.iamTokenUrl,
50+
cfg.iamApiKey,
51+
cfg.iamClientId,
52+
cfg.iamClientSecret
53+
);
54+
55+
if (cfg.autoRenew) {
56+
this._tokenManager.startAutoRenew();
57+
}
4558
}
4659

4760
onRequest(state, req, callback) {
4861
var self = this;
4962

50-
if (self._cfg.iamApiKey !== self.currentIamApiKey) {
51-
debug('New IAM API key identified.');
52-
self.currentIamApiKey = self._cfg.iamApiKey;
53-
state.stash.newApiKey = true;
54-
} else if (!self.refreshRequired) {
55-
if (self.baseUrl && self.cookieJar.getCookies(self.baseUrl, {expire: true}).length === 0) {
56-
debug('There are no valid session cookies in the jar. Requesting IAM session refresh...');
57-
} else {
58-
req.jar = self.cookieJar; // add jar
59-
return callback(state);
60-
}
61-
}
62-
63-
req.url = req.uri || req.url;
64-
delete req.uri;
63+
req.jar = self._jar;
6564

66-
if (self.baseUrl === null) {
67-
var parsed = u.parse(req.url);
68-
self.baseUrl = u.format({
69-
protocol: parsed.protocol,
70-
host: parsed.host,
71-
port: parsed.port
72-
});
73-
}
65+
req.uri = req.uri || req.url;
66+
delete req.url;
67+
req.uri = u.format(new u.URL(req.uri), {auth: false});
7468

75-
self.refreshCookie(self._cfg, state, function(error) {
76-
if (error) {
77-
debug(error.message);
78-
if (state.attempt < state.maxAttempt) {
79-
state.retry = true;
80-
if (state.attempt === 1) {
81-
state.retryDelayMsecs = self._cfg.retryInitialDelayMsecs;
82-
} else {
83-
state.retryDelayMsecs *= self._cfg.retryDelayMultiplier;
84-
}
69+
self._tokenManager.renewIfRequired().then(() => {
70+
callback(state);
71+
}).catch((error) => {
72+
debug(error);
73+
if (state.attempt < state.maxAttempt) {
74+
state.retry = true;
75+
let iamResponse = error.response;
76+
let retryAfterSecs;
77+
if (iamResponse && iamResponse.headers) {
78+
retryAfterSecs = iamResponse.headers['Retry-After'];
79+
}
80+
if (retryAfterSecs) {
81+
state.retryDelayMsecs = retryAfterSecs * 1000;
8582
} else {
86-
state.abortWithResponse = [ error ]; // return error to client
83+
state.retryDelayMsecs = self._cfg.retryDelayMsecs;
8784
}
8885
} else {
89-
req.jar = self.cookieJar; // add jar
86+
state.abortWithResponse = [ error ]; // return error to client
9087
}
9188
callback(state);
9289
});
9390
}
9491

9592
onResponse(state, response, callback) {
9693
if (response.statusCode === 401) {
97-
debug('Requesting IAM session refresh for 401 response.');
98-
this.refreshRequired = true;
94+
debug('Received 401 response. Asking for request retry.');
9995
state.retry = true;
96+
this._tokenManager.attemptTokenRenewal = true;
10097
}
10198
callback(state);
10299
}
103100

104-
// Perform IAM session request.
105-
refreshCookie(cfg, state, callback) {
106-
var self = this;
107-
108-
if (self.baseUrl === null) {
109-
return callback(new Error('Unspecified base URL'));
110-
}
111-
112-
self.withLock({
113-
stale: cfg.iamLockStaleMsecs || 2500, // 2.5 secs
114-
wait: cfg.iamLockWaitMsecs || 2000 // 2 secs
115-
}, function(error, done) {
116-
if (state.stash.newApiKey) {
117-
debug('Refreshing session with new IAM API key.');
118-
state.stash.newApiKey = false;
119-
self.cookieJar = request.jar(); // new jar
120-
} else if (!self.refreshRequired) {
121-
debug('Session refresh no longer required.');
122-
return callback();
123-
}
124-
debug('Making IAM session request.');
125-
var accessToken = null;
126-
async.series([
127-
function(callback) {
128-
var accessTokenAuth;
129-
if (typeof cfg.iamClientId !== 'undefined' && typeof cfg.iamClientSecret !== 'undefined') {
130-
accessTokenAuth = { user: cfg.iamClientId, pass: cfg.iamClientSecret };
131-
}
132-
// get access token
133-
self._client({
134-
url: self.tokenUrl,
135-
method: 'POST',
136-
auth: accessTokenAuth,
137-
headers: { 'Accepts': 'application/json' },
138-
form: {
139-
'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
140-
'response_type': 'cloud_iam',
141-
'apikey': cfg.iamApiKey
142-
},
143-
json: true
144-
}, function(error, response, body) {
145-
if (error) {
146-
callback(error);
147-
} else if (response.statusCode === 200) {
148-
if (body.access_token) {
149-
accessToken = body.access_token;
150-
debug('Retrieved access token from IAM token service.');
151-
callback();
152-
} else {
153-
callback(new Error('Invalid response from IAM token service'));
154-
}
155-
} else {
156-
callback(new Error(`Failed to acquire access token. Status code: ${response.statusCode}`));
157-
}
158-
});
159-
},
160-
function(callback) {
161-
// perform IAM cookie based user login
162-
self._client({
163-
url: self.baseUrl + '/_iam_session',
164-
method: 'POST',
165-
form: { 'access_token': accessToken },
166-
jar: self.cookieJar,
167-
json: true
168-
}, function(error, response, body) {
169-
if (error) {
170-
callback(error);
171-
} else if (response.statusCode === 200) {
172-
self.refreshRequired = false;
173-
debug('Successfully renewed IAM session.');
174-
callback();
175-
} else {
176-
callback(new Error(`Failed to exchange IAM token with Cloudant. Status code: ${response.statusCode}`));
177-
}
178-
});
179-
}
180-
],
181-
function(error) {
182-
done(); // release lock
183-
callback(error);
184-
});
185-
});
186-
}
187-
188-
setIamApiKey(iamApiKey) {
189-
debug('Setting new IAM API key.');
190-
this._cfg.iamApiKey = iamApiKey;
101+
setIamApiKey(newIamApiKey) {
102+
this._tokenManager.setIamApiKey(newIamApiKey);
191103
}
192104
}
193105

0 commit comments

Comments
 (0)