Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the 'accesstoken get' command solving #1072 #1091

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/manual/docs/cmd/accesstoken-get.md
@@ -0,0 +1,38 @@
# accesstoken get

Gets access token for the specified resource

## Usage

```sh
accesstoken get [options]
```

## Options

Option|Description
------|-----------
`--help`|output usage information
`-r, --resource <resource>`|The resource for which to retrieve an access token
`--new`|Retrieve a new access token to ensure that it's valid for as long as possible
waldekmastykarz marked this conversation as resolved.
Show resolved Hide resolved
`-o, --output [output]`|Output type. `json|text`. Default `text`
`--verbose`|Runs command with verbose logging
`--debug`|Runs command with debug logging

## Remarks

The `accesstoken get` command returns an access token for the specified resource. If an access token has been previously retrieved and is still valid, the command will return the cached token. If you want to ensure that the returned access token is valid for as long as possible, you can force the command to retrieve a new access token by using the `--new` option.

## Examples

Get access token for the Microsoft Graph

```sh
accesstoken get --resource https://graph.microsoft.com
```

Get a new access token for SharePoint Online

```sh
accesstoken get --resource https://contoso.sharepoint.com --new
```
1 change: 1 addition & 0 deletions docs/manual/mkdocs.yml
Expand Up @@ -10,6 +10,7 @@ nav:
- login: 'cmd/login.md'
- logout: 'cmd/logout.md'
- status: 'cmd/status.md'
- accesstoken get: 'cmd/accesstoken-get.md'
- Azure Active Directory Graph (aad):
- groupsetting:
- groupsetting add: 'cmd/aad/groupsetting/groupsetting-add.md'
Expand Down
24 changes: 24 additions & 0 deletions src/Auth.spec.ts
Expand Up @@ -200,6 +200,30 @@ describe('Auth', () => {
});
});

it('retrieves new access token using existing refresh token when refresh forced', (done) => {
const now = new Date();
now.setSeconds(now.getSeconds() + 1);
auth.service.accessTokens[resource] = {
expiresOn: now.toISOString(),
value: 'abc'
}
auth.service.refreshToken = refreshToken;
sinon.stub((auth as any).authCtx, 'acquireTokenWithRefreshToken').callsArgWith(3, undefined, { accessToken: 'acc' });
sinon.stub(auth, 'storeConnectionInfo').callsFake(() => Promise.resolve());

auth.ensureAccessToken(resource, stdout, true, true).then((accessToken) => {
try {
assert.equal(accessToken, 'acc');
done();
}
catch (e) {
done(e);
}
}, (err) => {
done(err);
});
});

it('starts device code authentication flow when no refresh token available and no authType specified', (done) => {
const acquireUserCodeStub = sinon.stub((auth as any).authCtx, 'acquireUserCode').callsArgWith(3, undefined, {});
sinon.stub((auth as any).authCtx, 'acquireTokenWithDeviceCode').callsArgWith(3, undefined, {});
Expand Down
4 changes: 2 additions & 2 deletions src/Auth.ts
Expand Up @@ -90,7 +90,7 @@ export class Auth {
});
}

public ensureAccessToken(resource: string, stdout: Logger, debug: boolean = false): Promise<string> {
public ensureAccessToken(resource: string, stdout: Logger, debug: boolean = false, fetchNew: boolean = false): Promise<string> {
/* istanbul ignore next */
Logging.setLoggingOptions({
level: debug ? 3 : 0,
Expand All @@ -104,7 +104,7 @@ export class Auth {
const accessToken: AccessToken | undefined = this.service.accessTokens[resource];
const expiresOn: Date = accessToken ? new Date(accessToken.expiresOn) : new Date(0);

if (accessToken && expiresOn > now) {
if (!fetchNew && accessToken && expiresOn > now) {
if (debug) {
stdout.log(`Existing access token ${accessToken.value} still valid. Returning...`);
}
Expand Down
157 changes: 157 additions & 0 deletions src/o365/commands/accesstoken-get.spec.ts
@@ -0,0 +1,157 @@
import commands from './commands';
import Command, { CommandOption, CommandValidate, CommandError } from '../../Command';
import * as sinon from 'sinon';
const command: Command = require('./accesstoken-get');
import * as assert from 'assert';
import Utils from '../../Utils';
import appInsights from '../../appInsights';
import auth from '../../Auth';

describe(commands.ACCESSTOKEN_GET, () => {
let vorpal: Vorpal;
let log: any[];
let cmdInstanceLogSpy: sinon.SinonSpy;
let cmdInstance: any;

before(() => {
sinon.stub(appInsights, 'trackEvent').callsFake(() => { });
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
auth.service.connected = true;
});

beforeEach(() => {
vorpal = require('../../vorpal-init');
log = [];
cmdInstance = {
commandWrapper: {
command: command.name
},
action: command.action(),
log: (msg: any) => {
log.push(msg);
}
};
cmdInstanceLogSpy = sinon.spy(cmdInstance, 'log');
});

afterEach(() => {
Utils.restore([
vorpal.find,
auth.ensureAccessToken
]);
auth.service.accessTokens = {};
});

after(() => {
Utils.restore([
appInsights.trackEvent,
auth.restoreAuth
]);
});

it('has correct name', () => {
assert.equal(command.name.startsWith(commands.ACCESSTOKEN_GET), true);
});

it('has a description', () => {
assert.notEqual(command.description, null);
});

it('retrieves access token for the specified resource', (done) => {
const d: Date = new Date();
d.setMinutes(d.getMinutes() + 1);
auth.service.accessTokens['https://graph.microsoft.com'] = {
expiresOn: d.toString(),
value: 'ABC'
};

cmdInstance.action({ options: { debug: false, resource: 'https://graph.microsoft.com' } }, () => {
try {
assert(cmdInstanceLogSpy.calledWith('ABC'));
done();
}
catch (e) {
done(e);
}
});
});

it('correctly handles error when retrieving access token', (done) => {
sinon.stub(auth, 'ensureAccessToken').callsFake(() => Promise.reject('An error has occurred'));

cmdInstance.action({ options: { debug: false, resource: 'https://graph.microsoft.com' } }, (err?: any) => {
try {
assert.equal(JSON.stringify(err), JSON.stringify(new CommandError('An error has occurred')));
done();
}
catch (e) {
done(e);
}
});
});

it('fails validation if resource is not passed', () => {
const actual = (command.validate() as CommandValidate)({ options: {} });
assert.notEqual(actual, true);
});

it('fails validation if resource is undefined', () => {
const actual = (command.validate() as CommandValidate)({ options: { resource: undefined } });
assert.notEqual(actual, true);
});

it('fails validation if resource is blank', () => {
const actual = (command.validate() as CommandValidate)({ options: { resource: '' } });
assert.notEqual(actual, true);
});

it('passes validation when resource is specified', () => {
const actual = (command.validate() as CommandValidate)({ options: { resource: 'https://graph.microsoft.com' } });
assert.equal(actual, true);
});

it('supports debug mode', () => {
const options = (command.options() as CommandOption[]);
let containsOption = false;
options.forEach(o => {
if (o.option === '--debug') {
containsOption = true;
}
});
assert(containsOption);
});

it('has help referring to the right command', () => {
const cmd: any = {
log: (msg: string) => { },
prompt: () => { },
helpInformation: () => { }
};
const find = sinon.stub(vorpal, 'find').callsFake(() => cmd);
cmd.help = command.help();
cmd.help({}, () => { });
assert(find.calledWith(commands.ACCESSTOKEN_GET));
});

it('has help with examples', () => {
const _log: string[] = [];
const cmd: any = {
log: (msg: string) => {
_log.push(msg);
},
prompt: () => { },
helpInformation: () => { }
};
sinon.stub(vorpal, 'find').callsFake(() => cmd);
cmd.help = command.help();
cmd.help({}, () => { });
let containsExamples: boolean = false;
_log.forEach(l => {
if (l && l.indexOf('Examples:') > -1) {
containsExamples = true;
}
});
Utils.restore(vorpal.find);
assert(containsExamples);
});
});
88 changes: 88 additions & 0 deletions src/o365/commands/accesstoken-get.ts
@@ -0,0 +1,88 @@
import commands from './commands';
import GlobalOptions from '../../GlobalOptions';
import Command, {
CommandOption,
CommandValidate,
CommandError
} from '../../Command';
import auth from '../../Auth';

const vorpal: Vorpal = require('../../vorpal-init');

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
new?: boolean;
resource: string;
}

class AccessTokenGetCommand extends Command {
public get name(): string {
return `${commands.ACCESSTOKEN_GET}`;
}

public get description(): string {
return 'Gets access token for the specified resource';
}

public commandAction(cmd: CommandInstance, args: CommandArgs, cb: (err?: any) => void): void {
auth
.ensureAccessToken(args.options.resource, cmd, this.debug, args.options.new)
.then((accessToken: string): void => {
cmd.log(accessToken);
cb();
}, (err: any): void => cb(new CommandError(err)));
}

public options(): CommandOption[] {
const options: CommandOption[] = [
{
option: '-r, --resource <resource>',
description: 'The resource for which to retrieve an access token'
},
{
option: '--new',
waldekmastykarz marked this conversation as resolved.
Show resolved Hide resolved
description: 'Retrieve a new access token to ensure that it\'s valid for as long as possible'
}
];

const parentOptions: CommandOption[] = super.options();
return options.concat(parentOptions);
}

public validate(): CommandValidate {
return (args: CommandArgs): boolean | string => {
if (!args.options.resource) {
return 'Required parameter resource missing';
}

return true;
};
}

public commandHelp(args: any, log: (help: string) => void): void {
const chalk = vorpal.chalk;
log(vorpal.find(this.name).helpInformation());
log(
`Remarks:

The ${chalk.blue(this.name)} command returns an access token for the specified
resource. If an access token has been previously retrieved and is still
valid, the command will return the cached token. If you want to ensure that
the returned access token is valid for as long as possible, you can force
the command to retrieve a new access token by using the ${chalk.grey('--new')} option.

Examples:

Get access token for the Microsoft Graph
${this.name} --resource https://graph.microsoft.com

Get a new access token for SharePoint Online
${this.name} --resource https://contoso.sharepoint.com --new
`);
}
}

module.exports = new AccessTokenGetCommand();
1 change: 1 addition & 0 deletions src/o365/commands/commands.ts
Expand Up @@ -2,5 +2,6 @@ export default {
LOGIN: `login`,
LOGOUT: `logout`,
STATUS: `status`,
ACCESSTOKEN_GET: `accesstoken get`,
TENANT_ID_GET: `tenant id get`
};