From f653a0d526c7194f0a5e80dc837f0f16a9d4f27b Mon Sep 17 00:00:00 2001 From: Artur Jankowski Date: Thu, 16 Feb 2023 14:27:53 +0100 Subject: [PATCH] feat: Add support for `--reauthorize` flag in login command (#457) --- docs/authentication.md | 29 +++++++++++ src/box-command.js | 27 ++++++++-- src/commands/login.js | 115 ++++++++++++++++++++++++++--------------- 3 files changed, 125 insertions(+), 46 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index 3769723b..5e7b10af 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -9,6 +9,8 @@ overview of how the Box API handles authentication. - [Server Auth with JWT](#server-auth-with-jwt) - [Server Auth with CCG](#server-auth-with-ccg) - [Traditional 3-Legged OAuth2](#traditional-3-legged-oauth2) + - [Reauthorize OAuth2](#reauthorize-oauth2) + Ways to Authenticate -------------------- @@ -103,3 +105,30 @@ box configure:environments:add /path/to/file/config.json --ccg-auth --ccg-user " ### Traditional 3-Legged OAuth2 Refer to the [OAuth Guide](https://developer.box.com/guides/cli/quick-start) if you want to use OAuth2. + +#### Reauthorize OAuth2 + +After each successful OAuth2 authorization, a pair of tokens is generated, the Access Token and Refresh Token. + +The first one, the [Access Token](https://developer.box.com/guides/authentication/tokens/access-tokens/), is used to represent the authenticated user to the Box servers and is valid for 60 minutes. + +The second one, the [Refresh Token](https://developer.box.com/guides/authentication/tokens/refresh/), is used to refresh the Access Token when it has expired or is close to expiring. A Refresh Token is valid for 1 use within 60 days. + +However, it may happen that both mentioned tokens, `Access Token` and `Refresh Token`, have expired. You may then see following error: + +```bash +Your refresh token has expired. +Please run this command "box login --name --reauthorize" to reauthorize selected environment and then run your command again. +``` + +In this case, you need to log in again to obtain required tokens by using the following command: + +```bash +box login --name "ENVIRONMENT_NAME" --reauthorize +``` + +where `ENVIRONMENT_NAME` is the name of the environment to be reauthorized. + +Thanks to the `--reauthorize` flag, the `clientID` and `clientSecret` parameters will be retrieved from the existing environment instead of asking the user for them. + +After a successful login, the `ENVIRONMENT_NAME` environment will be updated and set as the default. diff --git a/src/box-command.js b/src/box-command.js index 6e73c871..92e70d73 100644 --- a/src/box-command.js +++ b/src/box-command.js @@ -324,7 +324,7 @@ class BoxCommand extends Command { this.bulkErrors.push({ index: bulkEntryIndex, data: bulkData, - error: err, + error: this.wrapError(err), }); } /* eslint-enable no-await-in-loop */ @@ -736,7 +736,7 @@ class BoxCommand extends Command { client = sdk.getPersistentClient(tokenInfo, tokenCache); } catch (err) { throw new BoxCLIError( - `Can't load the default OAuth environment "${environmentsObj.default}". Please login again or provide a token.` + `Can't load the default OAuth environment "${environmentsObj.default}". Please reauthorize selected environment, login again or provide a token.` ); } } else if (environmentsObj.default) { @@ -1186,6 +1186,27 @@ class BoxCommand extends Command { } } + /** + * Wraps filtered error in an error with a user-friendly description + * + * @param {Error} err The thrown error + * @returns {Error} Error wrapped in an error with user friendly description + */ + wrapError(err) { + let messageMap = { + 'invalid_grant - Refresh token has expired': + 'Your refresh token has expired. \nPlease run this command "box login --name --reauthorize" to reauthorize selected environment and then run your command again.' + }; + + for (const key in messageMap) { + if (err.message.includes(key)) { + return new BoxCLIError(messageMap[key], err); + } + } + + return err; + } + /** * Handles an error thrown within a command * @@ -1197,7 +1218,7 @@ class BoxCommand extends Command { // Let the oclif default handler run first, since it handles the help and version flags there /* eslint-disable promise/no-promise-in-callback */ DEBUG.execute('Running framework error handler'); - await super.catch(err); + await super.catch(this.wrapError(err)); /* eslint-disable no-shadow,no-catch-shadow */ } catch (err) { // The oclif default catch handler rethrows most errors; handle those here diff --git a/src/commands/login.js b/src/commands/login.js index 9ae2893a..580ea765 100644 --- a/src/commands/login.js +++ b/src/commands/login.js @@ -24,48 +24,62 @@ class OAuthLoginCommand extends BoxCommand { const environmentsObj = await this.getEnvironments(); const port = flags.port; const redirectUri = `http://localhost:${port}/callback`; + let environment; - this.info( - chalk`{cyan If you are not using the quickstart guide to set up ({underline https://developer.box.com/guides/tooling/cli/quick-start/}) then go to the Box Developer console ({underline https://cloud.app.box.com/developers/console}) and:}` - ); - this.info( - chalk`{cyan 1. Select an application with OAuth user authentication method. Create a new Custom App if needed.}` - ); - this.info( - chalk`{cyan 2. Click on the Configuration tab and set the Redirect URI to: {italic ${redirectUri}}. Click outside the input field.}` - ); - this.info(chalk`{cyan 3. Click on {bold Save Changes}.}`); - - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'clientID', - message: 'What is the OAuth Client ID of your application?', - }, - { - type: 'input', - name: 'clientSecret', - message: 'What is the OAuth Client Secret of your application?', - }, - ]); + if (this.flags.reauthorize) { + if ( + !environmentsObj.environments.hasOwnProperty(this.flags.name) + ) { + this.error(`The ${this.flags.name} environment does not exist`); + } - const environmentName = flags.name; - const newEnvironment = { - clientId: answers.clientID, - clientSecret: answers.clientSecret, - name: environmentName, - cacheTokens: true, - authMethod: 'oauth20', - }; + environment = environmentsObj.environments[this.flags.name]; + if (environment.authMethod !== 'oauth20') { + this.error('The selected environment is not of type oauth20'); + } + } else { + this.info( + chalk`{cyan If you are not using the quickstart guide to set up ({underline https://developer.box.com/guides/tooling/cli/quick-start/}) then go to the Box Developer console ({underline https://cloud.app.box.com/developers/console}) and:}` + ); + this.info( + chalk`{cyan 1. Select an application with OAuth user authentication method. Create a new Custom App if needed.}` + ); + this.info( + chalk`{cyan 2. Click on the Configuration tab and set the Redirect URI to: {italic ${redirectUri}}. Click outside the input field.}` + ); + this.info(chalk`{cyan 3. Click on {bold Save Changes}.}`); + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'clientID', + message: 'What is the OAuth Client ID of your application?', + }, + { + type: 'input', + name: 'clientSecret', + message: 'What is the OAuth Client Secret of your application?', + }, + ]); + + environment = { + clientId: answers.clientID, + clientSecret: answers.clientSecret, + name: this.flags.name, + cacheTokens: true, + authMethod: 'oauth20', + }; + } + + const environmentName = environment.name; const sdkConfig = Object.freeze({ analyticsClient: { version: pkg.version, - } + }, }); const sdk = new BoxSDK({ - clientID: answers.clientID, - clientSecret: answers.clientSecret, + clientID: environment.clientId, + clientSecret: environment.clientSecret, }); this._configureSdk(sdk, sdkConfig); @@ -101,7 +115,7 @@ class OAuthLoginCommand extends BoxCommand { const user = await client.users.get('me'); - environmentsObj.environments[environmentName] = newEnvironment; + environmentsObj.environments[environmentName] = environment; environmentsObj.default = environmentName; await this.updateEnvironments(environmentsObj); @@ -112,12 +126,18 @@ class OAuthLoginCommand extends BoxCommand { res.send(html); this.info(chalk`{green Successfully logged in as ${user.login}!}`); - this.info( - chalk`{green New environment "${environmentName}" has been created and selected.}` - ); - this.info( - chalk`{green You are set up to make your first API call. Refer to the CLI commands library (https://github.com/box/boxcli#command-topics) for examples.}` - ); + if (this.flags.reauthorize) { + this.info( + chalk`{green Environment "${environmentName}" has been updated, selected and it's ready to use.}` + ); + } else { + this.info( + chalk`{green New environment "${environmentName}" has been created and selected.}` + ); + this.info( + chalk`{green You are set up to make your first API call. Refer to the CLI commands library (https://github.com/box/boxcli#command-topics) for examples.}` + ); + } } catch (err) { throw new BoxCLIError(err); } finally { @@ -159,7 +179,9 @@ class OAuthLoginCommand extends BoxCommand { message: 'What is your state code? (state=)', }, ]); - http.get(`http://localhost:${port}/callback?state=${authInfo.state}&code=${authInfo.code}`); + http.get( + `http://localhost:${port}/callback?state=${authInfo.state}&code=${authInfo.code}` + ); } else { open(authorizeUrl); this.info( @@ -173,7 +195,8 @@ class OAuthLoginCommand extends BoxCommand { // @NOTE: This command MUST skip client setup, since it is used to add the first environment OAuthLoginCommand.noClient = true; -OAuthLoginCommand.description = 'Sign in with OAuth and set a new environment'; +OAuthLoginCommand.description = + 'Sign in with OAuth and set a new environment or update an existing if reauthorize flag is used'; OAuthLoginCommand.flags = { ...BoxCommand.minFlags, @@ -192,6 +215,12 @@ OAuthLoginCommand.flags = { description: 'Set the port number for the local OAuth callback server', default: 3000, }), + reauthorize: flags.boolean({ + char: 'r', + description: 'Reauthorize the existing environment with given `name`', + dependsOn: ['name'], + default: false + }), }; module.exports = OAuthLoginCommand;