UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

167 lines (153 loc) 6.36 kB
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); } } } }