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

Commit d10e983

Browse files
authored
Merge pull request #392 from cloudant/385-iam-error-on-auth-fail
Error when user cannot be authenticated using IAM.
2 parents f8e60ff + e0a4d3f commit d10e983

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)