@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
167 lines (153 loc) • 6.36 kB
JavaScript
const axios = require('axios');
const cds = require('../cds');
const { SettingsManager } = require('./settings_manager');
const { ParamCollection } = require('./params');
const { getMessage } = require('./util/logging');
require('../util/axios').pruneErrors();
const DEBUG = cds.debug('cli');
const REASONS = { invalid_scope: /\binvalid_scope\b/ };
function assign(params, authData) {
if (authData?.access_token) {
params.set('token', authData.access_token);
const refresh = authData.refresh_token && params.get('saveRefreshToken');
if (refresh) {
params.set('refreshToken', authData.refresh_token);
} else {
params.delete('refreshToken');
}
let message = 'Retrieved access token';
if (authData.expires_in) {
params.set('tokenExpirationDate', Date.now() + authData.expires_in * 1000);
if (params.get('saveData')) {
message += ` (${refresh ? 'refreshed' : 'expiring'} after ${(new Date(params.get('tokenExpirationDate'))).toLocaleString()})`;
}
} else {
params.delete('tokenExpirationDate');
if (params.get('saveData') && refresh) {
message += ' (will be refreshed after expiry)';
}
}
console.log(message + '.');
}
if (authData?.passcode_url) {
params.set('passcodeUrl', authData.passcode_url);
}
}
function reqParams(method, params) {
const url = params.get('tokenUrl');
const d = {};
const ignored = {};
if (params.has('subdomain')) {
d.subdomain = params.get('subdomain');
}
if (params.has('refreshToken')) {
d.refresh_token = params.get('refreshToken');
const param = ['passcode', 'clientid'].find(param => params.has(param));
if (param) {
ignored[param] = 'refresh token is present';
}
} else if (params.has('passcode')) {
d.passcode = params.get('passcode');
} else if (params.has('clientsecret')) {
d.clientid = params.get('clientid');
d.clientsecret = params.get('clientsecret');
} else if (params.has('key')) {
d.clientid = params.get('clientid');
d.key = params.get('key');
}
const data = new URLSearchParams(d).toString();
return method === 'post'
? { url, data, ignored }
: { url: `${url}?${data}`, data: undefined, ignored };
}
async function fetchToken(params) {
if (params.has('token') || !(params.has('refreshToken') || params.has('passcode') || params.has('clientid') || !params.has('passcodeUrl'))) {
return;
}
let response, error;
const method = 'post';
do {
const { url, data, ignored } = reqParams(method, params);
if (Object.keys(ignored).length) {
console.log(`Ignoring parameters for fetching token: ${Object.entries(ignored).map(([param, reason]) => `${param} (${reason})`).join(', ')}.`);
}
DEBUG?.(`Getting authentication token from ${method.toUpperCase()} ${params.obfuscateQueryParams(url)}`);
error = undefined;
try {
response = await axios[method](url, data);
break;
} catch (e) {
error = e;
DEBUG?.(`HTTP status ${e.status}`);
if (params.has('refreshToken')) {
DEBUG?.('Discarding invalid refresh token');
params.delete('refreshToken');
} else {
break;
}
}
} while (!response);
const data = response?.data ?? error?.auth;
assign(params, data);
if (error) {
let message;
if (REASONS.invalid_scope.test(error.message)) {
message = 'token has invalid scope. Ensure your user has the required roles';
} else if (error.status === 404) {
message = 'token endpoint not found. Ensure that MTX is running with extensibility enabled';
} else if (error.message.includes('<html')) { // should already be caught when fetching passcode URL
message = 'HTML response received. Ensure the route to MTX is configured correctly in App Router';
} else if (error.status === 401) {
message = `invalid credentials`;
if (params.has('passcode')) {
message += `. Ensure the passcode is correct`;
if (params.has('passcodeUrl')) {
message += `. Passcode URL: ${params.get('passcodeUrl')}`;
}
} else if (params.has('clientid')) {
message += `. Ensure the client credentials are correct`;
}
} else {
message = 'error on token request';
}
throw new Error(message + '.', { cause: error });
}
}
module.exports = class AuthManager {
static async login(paramValues) {
SettingsManager.init();
const params = new ParamCollection(paramValues);
await SettingsManager.loadAndMergeSettings(params);
if (params.has('username')) {
params.set('reqAuth', { auth: { username: params.get('username'), password: params.get('password') } });
} else if (!params.get('skipToken')) {
try {
await fetchToken(params);
} catch (error) {
if (params.get('saveData')) {
await SettingsManager.saveSettings(params);
}
throw getMessage('Failed to login', { error });
}
params.set('reqAuth', { headers: { Authorization: 'Bearer ' + params.get('token') } });
}
if (params.get('saveData')) {
await SettingsManager.saveSettings(params); // saves token conditionally
}
return params;
}
static async logout(paramValues) {
SettingsManager.init();
const params = new ParamCollection(paramValues);
await SettingsManager.setKeytar(params, true);
if (params.get('clearInvalid')) {
await SettingsManager.deleteInvalidSettings();
await SettingsManager.deleteInvalidTokens();
} else {
await SettingsManager.deleteToken(params);
if (params.get('deleteSettings')) {
await SettingsManager.deleteSettingsWithoutToken(params);
}
}
}
}