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

Commit e0a4d3f

Browse files
committed
Always retry failed requests made by the IAM plugin.
If the IAM token cannot be retrieved after the configured number of retries (either because the IAM token service is down or the IAM API key is incorrect) then an error is returned to the client.
1 parent f8e60ff commit e0a4d3f

File tree

4 files changed

+32
-88
lines changed

4 files changed

+32
-88
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# UNRELEASED
2+
- [FIXED] Stopped disabling the IAM auth plugin after failed IAM
3+
authentications. Subsequent requests will re-request authorization,
4+
potentially failing again if the original authentication failure was not
5+
temporary.
6+
- [FIXED] Ensure IAM API key can be correctly changed.
7+
- [FIXED] Callback with an error when a user cannot be authenticated using IAM.
8+
19
# 4.2.1 (2019-08-29)
210
- [FIXED] Include all built-in plugin modules in webpack bundle.
311

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -414,16 +414,15 @@ var cloudant = new Cloudant({ url: myurl, maxAttempt: 5, plugins: [ { iamauth: {
414414
415415
The initial retry delay in milliseconds _(default: 500)_.
416416
417-
If the IAM token cannot be retrieved (either because the IAM token service is
418-
down or the IAM API key is incorrect) then the request will continue without
419-
IAM authentication allowing the database to return a `401` response to the
420-
caller so that it may be handled appropriately.
421-
422417
For example:
423418
```js
424-
var cloudant = new Cloudant({ url: 'https://examples.cloudant.com', plugins: { iamauth: { iamApiKey: 'xxxxxxxxxx', retryDelayMultiplier: 4 } } });
419+
var cloudant = new Cloudant({ url: 'https://examples.cloudant.com', plugins: { iamauth: { iamApiKey: 'xxxxxxxxxx', retryDelayMultiplier: 4, retryInitialDelayMsecs: 100 } } });
425420
```
426421
422+
If the IAM token cannot be retrieved after the configured number of retries
423+
(either because the IAM token service is down or the IAM API key is
424+
incorrect) then an error is returned to the client.
425+
427426
See [IBM Cloud Identity and Access Management](https://console.bluemix.net/docs/services/Cloudant/guides/iam.html#ibm-cloud-identity-and-access-management) for more information.
428427
429428
3. `retry`

plugins/iamauth.js

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ class IAMPlugin extends BasePlugin {
4141
this.baseUrl = cfg.baseUrl || null;
4242
this.cookieJar = null;
4343
this.tokenUrl = cfg.iamTokenUrl || 'https://iam.cloud.ibm.com/identity/token';
44-
this.shouldApplyIAMAuth = true;
4544
this.refreshRequired = true;
4645
}
4746

@@ -50,16 +49,9 @@ class IAMPlugin extends BasePlugin {
5049

5150
if (self._cfg.iamApiKey !== self.currentIamApiKey) {
5251
debug('New IAM API key identified.');
53-
this.cookieJar = request.jar(); // new jar
5452
self.currentIamApiKey = self._cfg.iamApiKey;
55-
self.shouldApplyIAMAuth = self.refreshRequired = true;
56-
}
57-
58-
if (!self.shouldApplyIAMAuth) {
59-
return callback(state);
60-
}
61-
62-
if (!self.refreshRequired) {
53+
state.stash.newApiKey = true;
54+
} else if (!self.refreshRequired) {
6355
if (self.baseUrl && self.cookieJar.getCookies(self.baseUrl, {expire: true}).length === 0) {
6456
debug('There are no valid session cookies in the jar. Requesting IAM session refresh...');
6557
} else {
@@ -80,18 +72,18 @@ class IAMPlugin extends BasePlugin {
8072
});
8173
}
8274

83-
self.refreshCookie(self._cfg, function(error) {
75+
self.refreshCookie(self._cfg, state, function(error) {
8476
if (error) {
8577
debug(error.message);
86-
if (self.shouldApplyIAMAuth) {
78+
if (state.attempt < state.maxAttempt) {
8779
state.retry = true;
8880
if (state.attempt === 1) {
8981
state.retryDelayMsecs = self._cfg.retryInitialDelayMsecs;
9082
} else {
9183
state.retryDelayMsecs *= self._cfg.retryDelayMultiplier;
9284
}
9385
} else {
94-
console.warn(`Disabling IAM authentication: ${error.message}`);
86+
state.abortWithResponse = [ error ]; // return error to client
9587
}
9688
} else {
9789
req.jar = self.cookieJar; // add jar
@@ -101,7 +93,7 @@ class IAMPlugin extends BasePlugin {
10193
}
10294

10395
onResponse(state, response, callback) {
104-
if (this.shouldApplyIAMAuth && response.statusCode === 401) {
96+
if (response.statusCode === 401) {
10597
debug('Requesting IAM session refresh for 401 response.');
10698
this.refreshRequired = true;
10799
state.retry = true;
@@ -110,7 +102,7 @@ class IAMPlugin extends BasePlugin {
110102
}
111103

112104
// Perform IAM session request.
113-
refreshCookie(cfg, callback) {
105+
refreshCookie(cfg, state, callback) {
114106
var self = this;
115107

116108
if (self.baseUrl === null) {
@@ -121,10 +113,11 @@ class IAMPlugin extends BasePlugin {
121113
stale: cfg.iamLockStaleMsecs || 2500, // 2.5 secs
122114
wait: cfg.iamLockWaitMsecs || 2000 // 2 secs
123115
}, function(error, done) {
124-
if (!self.shouldApplyIAMAuth) {
125-
return callback(new Error('Skipping IAM session authentication'));
126-
}
127-
if (!self.refreshRequired) {
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) {
128121
debug('Session refresh no longer required.');
129122
return callback();
130123
}
@@ -160,9 +153,6 @@ class IAMPlugin extends BasePlugin {
160153
callback(new Error('Invalid response from IAM token service'));
161154
}
162155
} else {
163-
if (response.statusCode < 500 && response.statusCode !== 429) {
164-
self.shouldApplyIAMAuth = false; // no retry
165-
}
166156
callback(new Error(`Failed to acquire access token. Status code: ${response.statusCode}`));
167157
}
168158
});
@@ -183,9 +173,6 @@ class IAMPlugin extends BasePlugin {
183173
debug('Successfully renewed IAM session.');
184174
callback();
185175
} else {
186-
if (response.statusCode < 500) {
187-
self.shouldApplyIAMAuth = false; // no retry
188-
}
189176
callback(new Error(`Failed to exchange IAM token with Cloudant. Status code: ${response.statusCode}`));
190177
}
191178
});

test/plugins/iamauth.js

Lines changed: 7 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ describe('#db IAMAuth Plugin', function() {
342342
});
343343
});
344344

345-
it('skips IAM authentication if access token returns non-200 response', function(done) {
345+
it('returns an error if access token returns non-200 response', function(done) {
346346
if (process.env.NOCK_OFF) {
347347
this.skip();
348348
}
@@ -356,19 +356,11 @@ describe('#db IAMAuth Plugin', function() {
356356
.times(3)
357357
.reply(500, 'Internal Error 500\nThe server encountered an unexpected condition which prevented it from fulfilling the request.');
358358

359-
var cloudantMocks = nock(SERVER)
360-
.get(DBNAME)
361-
.reply(401, {error: 'unauthorized', reason: 'Unauthorized'});
362-
363359
var cloudantClient = new Client({ plugins: { iamauth: { iamApiKey: IAM_API_KEY } } });
364360
var req = { url: SERVER + DBNAME, method: 'GET' };
365361
cloudantClient.request(req, function(err, resp, data) {
366-
assert.equal(err, null);
367-
assert.equal(resp.request.headers.cookie, null);
368-
assert.equal(resp.statusCode, 401);
369-
assert.ok(data.indexOf('"error":"unauthorized"') > -1);
362+
assert.equal(err.message, 'Failed to acquire access token. Status code: 500');
370363
iamMocks.done();
371-
cloudantMocks.done();
372364
done();
373365
});
374366
});
@@ -408,7 +400,7 @@ describe('#db IAMAuth Plugin', function() {
408400
});
409401
});
410402

411-
it('skips IAM authentication if IAM cookie login returns non-200 response', function(done) {
403+
it('returns an error if IAM cookie login returns non-200 response', function(done) {
412404
if (process.env.NOCK_OFF) {
413405
this.skip();
414406
}
@@ -425,17 +417,12 @@ describe('#db IAMAuth Plugin', function() {
425417
var cloudantMocks = nock(SERVER)
426418
.post('/_iam_session', {access_token: MOCK_ACCESS_TOKEN})
427419
.times(3)
428-
.reply(500, {error: 'internal_server_error', reason: 'Internal Server Error'})
429-
.get(DBNAME)
430-
.reply(401, {error: 'unauthorized', reason: 'Unauthorized'});
420+
.reply(500, {error: 'internal_server_error', reason: 'Internal Server Error'});
431421

432422
var cloudantClient = new Client({ plugins: { iamauth: { iamApiKey: IAM_API_KEY } } });
433423
var req = { url: SERVER + DBNAME, method: 'GET' };
434424
cloudantClient.request(req, function(err, resp, data) {
435-
assert.equal(err, null);
436-
assert.equal(resp.request.headers.cookie, null);
437-
assert.equal(resp.statusCode, 401);
438-
assert.ok(data.indexOf('"error":"unauthorized"') > -1);
425+
assert.equal(err.message, 'Failed to exchange IAM token with Cloudant. Status code: 500');
439426
iamMocks.done();
440427
cloudantMocks.done();
441428
done();
@@ -479,39 +466,6 @@ describe('#db IAMAuth Plugin', function() {
479466
});
480467
});
481468

482-
it('skips authentication renewal on 401 response if previous attempts failed', function(done) {
483-
if (process.env.NOCK_OFF) {
484-
this.skip();
485-
}
486-
487-
var iamMocks = nock(TOKEN_SERVER)
488-
.post('/identity/token', {
489-
'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
490-
'response_type': 'cloud_iam',
491-
'apikey': IAM_API_KEY
492-
})
493-
.reply(400, {
494-
errorCode: 'BXNIM0415E',
495-
errorMessage: 'Provided API key could not be found'
496-
});
497-
498-
var cloudantMocks = nock(SERVER)
499-
.get(DBNAME)
500-
.reply(401, {error: 'unauthorized', reason: 'Unauthorized'});
501-
502-
var cloudantClient = new Client({ plugins: { iamauth: { iamApiKey: IAM_API_KEY } } });
503-
var req = { url: SERVER + DBNAME, method: 'GET' };
504-
cloudantClient.request(req, function(err, resp, data) {
505-
assert.equal(err, null);
506-
assert.equal(resp.request.headers.cookie, null);
507-
assert.equal(resp.statusCode, 401);
508-
assert.ok(data.indexOf('"error":"unauthorized"') > -1);
509-
iamMocks.done();
510-
cloudantMocks.done();
511-
done();
512-
});
513-
});
514-
515469
it('throws error for unspecified IAM API key', function() {
516470
assert.throws(
517471
() => {
@@ -616,6 +570,7 @@ describe('#db IAMAuth Plugin', function() {
616570
'response_type': 'cloud_iam',
617571
'apikey': 'bad_key'
618572
})
573+
.times(3)
619574
.reply(400, {
620575
errorCode: 'BXNIM0415E',
621576
errorMessage: 'Provided API key could not be found'
@@ -628,8 +583,6 @@ describe('#db IAMAuth Plugin', function() {
628583
.reply(200, MOCK_IAM_TOKEN_RESPONSE);
629584

630585
var cloudantMocks = nock(SERVER)
631-
.get(DBNAME)
632-
.reply(401, {error: 'unauthorized', reason: 'Unauthorized'})
633586
.post('/_iam_session', {access_token: MOCK_ACCESS_TOKEN})
634587
.reply(200, {ok: true}, MOCK_SET_IAM_SESSION_HEADER)
635588
.get(DBNAME)
@@ -638,10 +591,7 @@ describe('#db IAMAuth Plugin', function() {
638591
var cloudantClient = new Client({ plugins: { iamauth: { iamApiKey: 'bad_key' } } });
639592
var req = { url: SERVER + DBNAME, method: 'GET' };
640593
cloudantClient.request(req, function(err, resp, data) {
641-
assert.equal(err, null);
642-
assert.equal(resp.request.headers.cookie, null);
643-
assert.equal(resp.statusCode, 401);
644-
assert.ok(data.indexOf('"error":"unauthorized"') > -1);
594+
assert.equal(err.message, 'Failed to acquire access token. Status code: 400');
645595

646596
// update IAM API key
647597
cloudantClient.getPlugin('iamauth').setIamApiKey(IAM_API_KEY);

0 commit comments

Comments
 (0)