Skip to content

Commit

Permalink
#54: Added support for client credentials auth type
Browse files Browse the repository at this point in the history
  • Loading branch information
darnjo committed Oct 28, 2023
1 parent aa44557 commit a5b0c6d
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 15 deletions.
31 changes: 28 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,46 @@ if (require?.main === module) {
.requiredOption('-s, --strategy <string>', 'One of TopAndSkip, TimestampAsc, TimestampDesc, or NextLink')
.option('-u, --serviceRootUri <string>', 'OData service root URI (no resource name or query)')
.option('-b, --bearerToken <string>', 'Bearer token to use for authorization')
.option('-c, --clientId <string>', 'OAuth2 client_id parameter, use this OR bearerToken')
.option('-i, --clientSecret <string>', 'OAuth2 client_secret parameter, use this OR bearerToken')
.option('-k, --tokenUri <string>', 'OAuth2 token_uri parameter, use this OR bearerToken')
.option('-e, --scope <string>', 'Optional OAuth2 scopes for client credentials')
.option('-m, --pathToMetadataReportJson <string>', 'Path to metadata report JSON')
.option('-r, --resourceName <string>', 'Resource name to replicate data from')
.option('-x, --expansions <items>', 'Comma-separated list of items to expand during the query process, e.g. Media,OpenHouse')
.option('-f, --filter <string>', 'OData $filter expression')
.option('-t, --top <number>', 'Optional parameter to use for OData $top')
.option('-s, --maxPageSize <number>', 'Optional parameter for the odata.maxpagesize header')
.option('-p, --maxPageSize <number>', 'Optional parameter for the odata.maxpagesize header')
.option('-o, --outputPath <string>', 'Name of directory for results')
.option('-l, --limit <number>', 'Limit total number of records at client level')
.option('-v, --version <string>', 'Data Dictionary version to use', '2.0')
.option('-j, --jsonSchemaValidation <boolean>', 'Sets whether to use JSON schema validation', false)
.action(options => {

// TODO: if run from the command line, we don't want to generate additional reports
// until we have the ability to understand the type and expansions from the metadata
const { pathToMetadataReportJson } = options;
replicate({ ...options, shouldGenerateReports: !!pathToMetadataReportJson });
const { pathToMetadataReportJson, bearerToken, clientId, clientSecret, tokenUri, scope, ...remainingOptions } = options;

let appOptions = {
...remainingOptions,
pathToMetadataReportJson,
shouldGenerateReports: !!pathToMetadataReportJson
};

if (bearerToken) {
appOptions.bearerToken = bearerToken;
} else if (clientId && clientSecret && tokenUri) {
appOptions.clientCredentials = {
clientId,
clientSecret,
tokenUri,
scope
};
} else {
throw new Error('One of bearerToken or clientId, clientSecret, and tokenUri MUST be specified!');
}

replicate(appOptions);
});

program
Expand Down
2 changes: 2 additions & 0 deletions lib/replication/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ const replicate = async ({
responses: []
};

// TODO - add support for multiple strategies

// Each resource and expansion will have its separate set of requests
for await (const request of requests) {
const { requestUri: initialRequestUri, resourceName } = request;
Expand Down
7 changes: 4 additions & 3 deletions lib/replication/replication-iterator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const queryString = require('node:querystring');
const { processHttpErrorResponse, ERROR_TYPES, calculateJsonSize } = require('./utils');
const { processHttpErrorResponse, ERROR_TYPES, calculateJsonSize, NOT_OK } = require('./utils');
const humanizeDuration = require('humanize-duration');

const authService = require('./services/auth/oauth2');
Expand Down Expand Up @@ -41,7 +41,8 @@ const getOAuth2BearerTokenHeader = async ({ bearerToken = '', clientCredentials
if (token) {
return { Authorization: `Bearer ${token}` };
} else {
throw new Error('Could not get authorization token!');
console.error(`ERROR: Could not get authorization token from '${clientCredentials?.tokenUri}'!`);
process.exit(NOT_OK);
}
};

Expand Down Expand Up @@ -93,7 +94,7 @@ async function* replicationIterator({ initialRequestUri = '', maxErrorCount = 3,

// TODO: handle client credentials auth
const headers = {
...await getOAuth2BearerTokenHeader(authInfo)
...(await getOAuth2BearerTokenHeader(authInfo))
};

if (strategy === REPLICATION_STRATEGIES.NEXT_LINK) {
Expand Down
21 changes: 12 additions & 9 deletions lib/replication/services/auth/oauth2.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const _authInfo = {
};

const isClientCredentialsAuth = () =>
_authInfo?.clientCredentials?.clientId && _authInfo?.clientCredentials?.clientSecret && _authInfo?.clientCredentials?.tokenUri;
!!(_authInfo?.clientCredentials?.clientId && _authInfo?.clientCredentials?.clientSecret && _authInfo?.clientCredentials?.tokenUri);

let isInitialized = false;

Expand Down Expand Up @@ -46,29 +46,31 @@ const init = async ({ bearerToken, clientCredentials = {} }) => {
const _fetchClientCredentialsAccessToken = async ({ clientId, clientSecret, tokenUri, scope, useBasicAuth = true, useBody } = {}) => {
try {
let headers = {},
body = {};
body;

if (useBasicAuth) {
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
'User-Agent': USER_AGENT_HEADER
};

body = {
grant_type: 'client_credentials',
scope
};
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('scope', scope);

body = params;

} else if (useBody) {
//TODO: use body
throw new Error('Unsupported auth option!');
} else {
throw new Error('Unsupported auth type!');
}

const response = await fetch(tokenUri, {
method: 'POST',
headers,
body: JSON.stringify(body)
body
});

const { access_token, expires_in, token_type, scope: responseScope } = await response.json();
Expand All @@ -78,6 +80,7 @@ const _fetchClientCredentialsAccessToken = async ({ clientId, clientSecret, toke
_authInfo.tokenType = token_type;
_authInfo.tokenExpiresTimestamp = expires_in ? new Date() + expires_in : null;
_authInfo.scope = responseScope;

} catch (err) {
console.log(err);
process.exit(NOT_OK);
Expand Down

0 comments on commit a5b0c6d

Please sign in to comment.