Skip to content

Commit

Permalink
Added the 'accesstoken get' command solving #1072
Browse files Browse the repository at this point in the history
  • Loading branch information
waldekmastykarz committed Sep 4, 2019
1 parent 062093d commit 005dcc7
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 2 deletions.
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
`-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',
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`
};

0 comments on commit 005dcc7

Please sign in to comment.