Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 50 additions & 17 deletions lib/requester.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ function getAuthenticatorKerberos(client) {
function getAccessToken(operation){
const postData = `${encodeURI('grant_type')}=${encodeURI('apikey')}&${encodeURI('key')}=${encodeURI(operation.client.connectionParams.apiKey)}`;
const path = operation.client.connectionParams.accessTokenDuration?'/token?duration='+encodeURIComponent(operation.client.connectionParams.accessTokenDuration):'/token';
function handleTokenError(error, contextMessage) {
if (!operation.lockAccessToken) {
return;
}

operation.lockAccessToken = false;

const baseMessage = (contextMessage == null) ? 'Failed to obtain access token' : contextMessage;
const errorMessage = (error && error.message) ? `${baseMessage}: ${error.message}` : baseMessage;
operation.errorListener(new Error(errorMessage));
}

const options = {
hostname: operation.client.connectionParams.host,
port: 443,
Expand All @@ -58,26 +70,51 @@ function getAccessToken(operation){
};

const req = https.request(options, (res) => {
let responseBody = '';

res.on('data', (d) => {
responseBody += d.toString();
});

res.on('end', () => {
if(res.statusCode === 400){
throw new Error(d.toString());
handleTokenError(null, 'Token endpoint returned 400: ' + responseBody);
return;
Comment thread
rjdew-progress marked this conversation as resolved.
}
const responseValue = JSON.parse(d.toString());
operation.accessToken = responseValue.access_token;
operation.expiration = new Date(responseValue['.expires']);
if(operation.lockAccessToken){

try {
const responseValue = JSON.parse(responseBody);
if (!responseValue.access_token || !responseValue['.expires']) {
throw new Error('missing required token fields');

@RitaChen609 RitaChen609 Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new Error is used here. Should it be replaced with handleTokenError and return earlier?

}

const expiration = new Date(responseValue['.expires']);
if (Number.isNaN(expiration.getTime())) {
throw new Error('invalid .expires value');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new Error is used here. Should it be replaced with handleTokenError?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is intentional to be picked up by req.on('error', (e) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the throw new Error would be caught by catch(error) block (line 100) later. It then will call handleTokenError. So I think using throw new Error is not a bug, but replacing with handleTokenError and return earlier would be clearer and more explicit.

}

operation.accessToken = responseValue.access_token;
operation.expiration = expiration;
operation.lockAccessToken = false;
} catch (error) {
handleTokenError(error, 'Invalid token endpoint response');
return;
}

authenticatedRequest(operation);
});

res.on('error', (e) => {
operation.accessToken = new Error(e);
throw new Error(operation.accessToken);
handleTokenError(e, 'Failed to obtain access token');
});
});

req.write(postData);
req.end();
req.on('error', (e) => {
handleTokenError(e, 'Failed to obtain access token');
});

req.write(postData);
req.end();
}

function startRequest(operation) {
Expand Down Expand Up @@ -296,14 +333,10 @@ function authenticatedRequest(operation) {
break;
case 'CLOUD':
operation.logger.debug('cloud authentication');
if(operation.accessToken instanceof Error){
throw new Error(operation.accessToken);
} else {
request.setHeader(
'authorization',
'bearer ' +operation.accessToken
);
}
request.setHeader(
'authorization',
'bearer ' + operation.accessToken
);
break;
case 'OAUTH':
request.setHeader(
Expand Down
87 changes: 87 additions & 0 deletions test-basic/cloud_authentication-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let assert = require('assert');
const testconfig = require("../etc/test-config");
const mlutil = require("../lib/mlutil");
const expect = require('chai').expect;
const EventEmitter = require('events');

describe('cloud-authentication tests', function() {
it('should throw error without apiKey.', function(done){
Expand Down Expand Up @@ -99,4 +100,90 @@ describe('cloud-authentication tests', function() {
'Error message should mention accessTokenDuration validation');
}
});

it('should route token request network errors to operation.errorListener', function(done) {
this.timeout(500);
const requester = require('../lib/requester');
const https = require('https');
const originalRequest = https.request;

const operation = {
client: {
connectionParams: {
host: 'example.marklogic.cloud',
apiKey: 'test-key'
}
},
lockAccessToken: true,
errorListener: (error) => {
https.request = originalRequest;
try {
assert(error instanceof Error);
assert(error.message.includes('Failed to obtain access token'));
assert(error.message.includes('ECONNRESET'));
assert.strictEqual(operation.lockAccessToken, false);
done();
} catch (assertionError) {
done(assertionError);
}
}
};

https.request = () => {
const req = new EventEmitter();
req.write = () => {};
req.end = () => {
process.nextTick(() => req.emit('error', new Error('ECONNRESET')));
};
return req;
};

requester.getAccessToken(operation);
});

it('should route token endpoint 400 responses to operation.errorListener', function(done) {
this.timeout(500);
const requester = require('../lib/requester');
const https = require('https');
const originalRequest = https.request;

const operation = {
client: {
connectionParams: {
host: 'example.marklogic.cloud',
apiKey: 'test-key'
}
},
lockAccessToken: true,
errorListener: (error) => {
https.request = originalRequest;
try {
assert(error instanceof Error);
assert(error.message.includes('Token endpoint returned 400'));
assert(error.message.includes('invalid key payload'));
done();
} catch (assertionError) {
done(assertionError);
}
}
};

https.request = (options, callback) => {
const req = new EventEmitter();
req.write = () => {};
req.end = () => {
process.nextTick(() => {
const res = new EventEmitter();
res.statusCode = 400;
callback(res);
res.emit('data', Buffer.from('invalid key '));
res.emit('data', Buffer.from('payload'));
res.emit('end');
});
};
return req;
};

requester.getAccessToken(operation);
});
});
Loading