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

feat: emit an event when new tokens are obtained #341

Merged
merged 5 commits into from
Apr 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,24 @@ function getAuthenticatedClient() {
main();
```

##### IMPORTANT NOTE
`refresh_token` is only returned on the first authorization.
More details [here](https://github.com/google/google-api-nodejs-client/issues/750#issuecomment-304521450)
##### Handling token events
This library will automatically obtain an `access_token`, and automatically refresh the `access_token` if a `refresh_token` is present. The `refresh_token` is only returned on the [first authorization]((https://github.com/google/google-api-nodejs-client/issues/750#issuecomment-304521450), so if you want to make sure you store it safely. An easy way to make sure you always store the most recent tokens is to use the `tokens` event:

```js
const client = await auth.getClient();

client.on('tokens', (tokens) => {
if (tokens.refresh_token) {
// store the refresh_token in my database!
console.log(tokens.refresh_token);
}
console.log(tokens.access_token);
});

const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`;
const res = await client.request({ url });
// The `tokens` event would now be raised if this was the first request
```

##### Retrieve access token
With the code returned, you can ask for an access token as shown below:
Expand Down
55 changes: 35 additions & 20 deletions examples/refreshAccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,37 @@ const keys = require('./oauth2.keys.json');
*/
async function main() {
try {
const oAuth2Client = await getAuthenticatedClient();
// If you're going to save the refresh_token, make sure
// to put it somewhere safe!
console.log(`Refresh Token: ${oAuth2Client.credentials.refresh_token}`);
console.log(`Expiration: ${oAuth2Client.credentials.expiry_date}`);
// create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file,
// which should be downloaded from the Google Developers Console.
const client = new OAuth2Client(
keys.web.client_id,
keys.web.client_secret,
keys.web.redirect_uris[0]
);

client.on('tokens', tokens => {
// You'll get a refresh token the first time the user authorizes this app.
// You can always ask for another access_token using this long lived token.
// Make sure to store it somewhere safe!
if (tokens.refresh_token) {
// store me somewhere safe!
console.log(`Refresh Token: ${tokens.refresh_token}`);
}
// You'll also get new access tokens here! These tokens expire frequently,
// but as long as client.credentials.refresh_token is set, we can ask for
// another one.
if (tokens.access_token) {
console.log(`Access Token: ${tokens.access_token}`);
console.log(`Expiration: ${tokens.expiry_date}`);
}
});

// Prompt the user for consent and obtain an access token.
await authorizeClient(client);

// Now lets go ahead and aske for another access token, why not.
console.log('Refreshing access token ...');
const res = await oAuth2Client.refreshAccessToken();
console.log(`New expiration: ${oAuth2Client.credentials.expiry_date}`);
const res = await client.refreshAccessToken();
} catch (e) {
console.error(e);
}
Expand All @@ -45,18 +68,10 @@ async function main() {
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the full client to the callback.
*/
function getAuthenticatedClient() {
function authorizeClient(client) {
return new Promise((resolve, reject) => {
// create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file,
// which should be downloaded from the Google Developers Console.
const oAuth2Client = new OAuth2Client(
keys.web.client_id,
keys.web.client_secret,
keys.web.redirect_uris[0]
);

// Generate the url that will be used for the consent dialog.
const authorizeUrl = oAuth2Client.generateAuthUrl({
const authorizeUrl = client.generateAuthUrl({
// To get a refresh token, you MUST set access_type to `offline`.
access_type: 'offline',
// set the appropriate scopes
Expand All @@ -80,11 +95,11 @@ function getAuthenticatedClient() {
server.close();

// Now that we have the code, use that to acquire tokens.
const r = await oAuth2Client.getToken(qs.code);
const r = await client.getToken(qs.code);
// Make sure to set the credentials on the OAuth2 client.
oAuth2Client.setCredentials(r.tokens);
client.setCredentials(r.tokens);
console.info('Tokens acquired.');
resolve(oAuth2Client);
resolve(client);
}
})
.listen(3000, () => {
Expand Down
9 changes: 8 additions & 1 deletion src/auth/authclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@
*/

import {AxiosPromise, AxiosRequestConfig} from 'axios';
import {EventEmitter} from 'events';

import {DefaultTransporter} from '../transporters';

import {Credentials} from './credentials';

export abstract class AuthClient {
export declare interface AuthClient {
on(event: 'tokens', listener: (tokens: Credentials) => void): this;
}

export abstract class AuthClient extends EventEmitter {
transporter = new DefaultTransporter();
credentials: Credentials = {};

Expand Down
1 change: 1 addition & 0 deletions src/auth/computeclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class Compute extends OAuth2Client {
((new Date()).getTime() + (res.data.expires_in * 1000));
delete (tokens as CredentialRequest).expires_in;
}
this.emit('tokens', tokens);
return {tokens, res};
}

Expand Down
1 change: 1 addition & 0 deletions src/auth/jwtclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export class JWT extends OAuth2Client {
// tslint:disable-next-line no-any
id_token: (gtoken.rawToken! as any).id_token
};
this.emit('tokens', tokens);
return {res: null, tokens};
}

Expand Down
2 changes: 2 additions & 0 deletions src/auth/oauth2client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ export class OAuth2Client extends AuthClient {
((new Date()).getTime() + (res.data.expires_in * 1000));
delete (tokens as CredentialRequest).expires_in;
}
this.emit('tokens', tokens);
return {tokens, res};
}

Expand Down Expand Up @@ -551,6 +552,7 @@ export class OAuth2Client extends AuthClient {
((new Date()).getTime() + (res.data.expires_in * 1000));
delete (tokens as CredentialRequest).expires_in;
}
this.emit('tokens', tokens);
return {tokens, res};
}

Expand Down
13 changes: 13 additions & 0 deletions test/test.compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ it('should refresh if access token has expired', async () => {
scopes.forEach(s => s.done());
});

it('should emit an event for a new access token', async () => {
const scopes = [mockToken(), mockExample()];
let raisedEvent = false;
compute.on('tokens', tokens => {
assert.equal(tokens.access_token, 'abc123');
raisedEvent = true;
});
await compute.request({url});
assert.equal(compute.credentials.access_token, 'abc123');
scopes.forEach(s => s.done());
assert(raisedEvent);
});

it('should refresh if access token will expired soon and time to refresh before expiration is set',
async () => {
const scopes = [mockToken(), mockExample()];
Expand Down
18 changes: 18 additions & 0 deletions test/test.jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,22 @@ it('can get obtain new access token when scopes are set', (done) => {
});
});

it('should emit an event for tokens', (done) => {
const accessToken = 'initial-access-token';
const scope = createGTokenMock({access_token: accessToken});
const jwt = new JWT({
email: 'foo@serviceaccount.com',
keyFile: PEM_PATH,
scopes: ['http://bar', 'http://foo'],
subject: 'bar@subjectaccount.com'
});
jwt.on('tokens', tokens => {
assert.equal(tokens.access_token, accessToken);
scope.done();
done();
}).getAccessToken();
});

it('can obtain new access token when scopes are set', (done) => {
const jwt = new JWT({
email: 'foo@serviceaccount.com',
Expand All @@ -158,6 +174,8 @@ it('can obtain new access token when scopes are set', (done) => {
});
});



it('gets a jwt header access token', (done) => {
const keys = keypair(1024 /* bitsize of private key */);
const email = 'foo@serviceaccount.com';
Expand Down
15 changes: 13 additions & 2 deletions test/test.oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,10 +736,21 @@ function mockExample() {

it('should refresh token if missing access token', (done) => {
const scopes = mockExample();
client.credentials = {refresh_token: 'refresh-token-placeholder'};
const accessToken = 'abc123';
let raisedEvent = false;
const refreshToken = 'refresh-token-placeholder';
client.credentials = {refresh_token: refreshToken};

// ensure the tokens event is raised
client.on('tokens', tokens => {
assert.equal(tokens.access_token, accessToken);
raisedEvent = true;
});

client.request({url: 'http://example.com'}, err => {
scopes.forEach(s => s.done());
assert.equal('abc123', client.credentials.access_token);
assert(raisedEvent);
assert.equal(accessToken, client.credentials.access_token);
done();
});
});
Expand Down