Skip to content

Commit

Permalink
feat: Add support for --reauthorize flag in login command (#457)
Browse files Browse the repository at this point in the history
  • Loading branch information
arjankowski committed Feb 16, 2023
1 parent f89d9ef commit f653a0d
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 46 deletions.
29 changes: 29 additions & 0 deletions docs/authentication.md
Expand Up @@ -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
--------------------
Expand Down Expand Up @@ -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 <ENVIRONMENT_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.
27 changes: 24 additions & 3 deletions src/box-command.js
Expand Up @@ -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 */
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 <ENVIRONMENT_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
*
Expand All @@ -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
Expand Down
115 changes: 72 additions & 43 deletions src/commands/login.js
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand All @@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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;

0 comments on commit f653a0d

Please sign in to comment.