Skip to content

Commit

Permalink
Merge pull request #45 from adobe/PDCL-4946-consume-better-jwt-errors
Browse files Browse the repository at this point in the history
Pdcl 4946 Consume better JWT token exchange errors
  • Loading branch information
brenthosie authored Apr 23, 2021
2 parents 86a89f4 + 271888a commit 9acfe79
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 2,555 deletions.
113 changes: 86 additions & 27 deletions bin/__tests__/getIntegrationAccessToken.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ const getArguments = o => {
);
};

const mockServerErrorResponse = (message, code) => {
let newError = new Error(message);
newError.code = code;
return newError;
}

const mockUnhandledException = message => {
// this error has no "code" attribute
return new Error(message);
}

describe('getIntegrationAccessToken', () => {
let getIntegrationAccessToken;
let mockInquirer;
Expand Down Expand Up @@ -167,13 +178,12 @@ describe('getIntegrationAccessToken', () => {
});

it('reports error retrieving access token', async () => {
const mockedAuthError = 'some error: Bad things happened.'
const mockedAuthError = mockServerErrorResponse('some error: Bad things happened', 'server_error_code');
mockAuth.and.returnValue(
Promise.reject(new Error(mockedAuthError))
Promise.reject(mockedAuthError)
);

let errorMessage;

let returnedError;
try {
await getIntegrationAccessToken(
{
Expand All @@ -182,28 +192,29 @@ describe('getIntegrationAccessToken', () => {
getArguments()
);
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

// we bailed after the first call because it wasn't a scoping error
expect(mockAuth.calls.count()).toBe(1)

expect(errorMessage).toBe(
`Error retrieving access token. ${mockedAuthError}`
);
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: some error: Bad things happened')).toBeTrue();
expect(returnedError.message.includes('Error Code: server_error_code')).toBeTrue();
expect(returnedError.code).toBe('server_error_code');
});

it('attempts authenticating with each supported metascope', async () => {
const mockedAuthError = 'invalid_scope: Invalid metascope.';
const mockedAuthError = mockServerErrorResponse('invalid_scope: Invalid metascope.', 'invalid_scope');
mockAuth.and.returnValue(
Promise.reject(new Error(mockedAuthError))
Promise.reject(mockedAuthError)
);

let errorMessage;
let returnedError;
try {
await getIntegrationAccessToken(getEnvConfig(), getArguments());
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

expect(mockAuth).toHaveBeenCalledWith(
Expand All @@ -230,37 +241,84 @@ describe('getIntegrationAccessToken', () => {

expect(mockAuth.calls.count()).toBe(METASCOPES.length);
// This tests that if all metascopes fail, the error from the last attempt is ultimately thrown.
expect(errorMessage).toBe(
`Error retrieving access token. ${mockedAuthError}`
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: invalid_scope: Invalid metascope.')).toBeTrue();
expect(returnedError.message.includes('Error Code: invalid_scope')).toBeTrue();
expect(returnedError.code).toBe('invalid_scope');
});

it('throws a stack trace when error.code is missing', async () => {
const mockedAuthError = mockUnhandledException('500 server error');
mockAuth.and.returnValue(
Promise.reject(mockedAuthError)
);

let returnedError;
try {
// should be going through a bunch of scopes
await getIntegrationAccessToken(getEnvConfig(), getArguments());
} catch (error) {
returnedError = error;
}

// however, when we don't see an error.code, we bail and report
expect(mockAuth.calls.count()).toBe(1);
// the message should not have any of our pretty formatting
expect(returnedError.message).toBe('500 server error');
expect(returnedError.code).toBeFalsy();
});

it('throws a stack trace when --verbose and request_failed', async () => {
const mockedAuthError = mockServerErrorResponse('some error', 'request_failed');
mockAuth.and.returnValue(
Promise.reject(mockedAuthError)
);

let returnedError;
try {
// should be going through a bunch of scopes
await getIntegrationAccessToken(getEnvConfig(), getArguments({ verbose: true }));
} catch (error) {
returnedError = error;
}

// however, when we don't see an error.code, we bail and report
expect(mockAuth.calls.count()).toBe(1);
// the message should not have any of our pretty formatting
expect(returnedError.message).toBe('some error');
expect(returnedError.code).toBe('request_failed');
});

it('shows JS error details in case they happen', async () => {
const mockedAuthError = 'some error';
const mockedAuthError = mockServerErrorResponse('some error', 'server_error_code');
mockAuth.and.returnValue(
Promise.reject(new Error(mockedAuthError))
Promise.reject(mockedAuthError)
);

let errorMessage;
let returnedError;
try {
await getIntegrationAccessToken(getEnvConfig(), getArguments());
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

// we bailed after the first call because it wasn't a scoping error
expect(mockAuth.calls.count()).toBe(1)

expect(errorMessage).toBe(
`Error retrieving access token. ${mockedAuthError}`
);
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: some error')).toBeTrue();
expect(returnedError.message.includes('Error Code: server_error_code')).toBeTrue();
expect(returnedError.code).toBe('server_error_code');
});

it('contains a fallback message for authentication errors', async () => {
// don't supply a message during auth failure
mockAuth.and.returnValue(Promise.reject(new Error()));
const mockedAuthError = mockServerErrorResponse(undefined, 'server_error_code');
mockAuth.and.returnValue(
Promise.reject(mockedAuthError)
);

let errorMessage;
let returnedError;
try {
await getIntegrationAccessToken(
{
Expand All @@ -275,12 +333,13 @@ describe('getIntegrationAccessToken', () => {
}
);
} catch (error) {
errorMessage = error.message;
returnedError = error;
}

expect(errorMessage).toBe(
'Error retrieving access token. An unknown authentication error occurred.'
);
expect(returnedError.message.includes('Error retrieving your Access Token:')).toBeTrue();
expect(returnedError.message.includes('Error Message: An unknown authentication error occurred')).toBeTrue();
expect(returnedError.message.includes('Error Code: server_error_code')).toBeTrue();
expect(returnedError.code).toBe('server_error_code');
});
});
});
20 changes: 17 additions & 3 deletions bin/getIntegrationAccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,29 @@ module.exports = async (

return response.access_token;
} catch (e) {
// an unexpected error in jwt-auth
if (!e.code || (verbose && 'request_failed' === e.code)) {
throw e;
}

const errorMessage = e.message || 'An unknown authentication error occurred.';
const isScopeError = errorMessage.toLowerCase().indexOf('invalid_scope') !== -1;
const isScopeError = 'invalid_scope' === e.code;
const hasCheckedFinalScope = i === METASCOPES.length - 1;

// throw immediately if we've encountered any error that isn't a scope error
if (!isScopeError || hasCheckedFinalScope) {
throw new Error(
`Error retrieving access token. ${errorMessage}`
let preAmble = 'Error retrieving your Access Token:';
const message = `Error Message: ${errorMessage}`;
const code = `Error Code: ${e.code}`;
if ('request_failed' === e.code) {
preAmble += ' This is likely an error within jwt-auth or the IMS token exchange service';
}

let preparedError = new Error(
[preAmble, message, code].join('\n')
);
preparedError.code = e.code;
throw preparedError;
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,13 @@ const checkOldProductionEnvironmentVariables = require('./checkOldProductionEnvi
argv
);
} catch (error) {
if (argv.verbose) {
if (argv.verbose || !error.code) {
console.log(chalk.bold.red('--verbose output:'))
throw error;
}

console.log(chalk.bold.red(error.message));
console.log(chalk.bold.red('run in --verbose mode for more info'));
console.log(chalk.bold.red('run in --verbose mode for full stack trace'));
process.exitCode = 1;
}
})();
Loading

0 comments on commit 9acfe79

Please sign in to comment.