Skip to content

Commit

Permalink
chore: docs and samples for refresh token, update uris (#215)
Browse files Browse the repository at this point in the history
Note: This is a semver minor change (though it will be included in the 1.0 release). I have no reason to believe it breaks any functionality, but it does (I think) bring us up to date with the most recent documentation:
https://developers.google.com/identity/protocols/OAuth2InstalledApp

It's also suggested in #120 that this is causing problems for users as well.
  • Loading branch information
JustinBeckwith authored Jan 3, 2018
1 parent de8e298 commit 18bfdd9
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 9 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,24 @@ oauth2Client.setCredentials(tokens);
```

##### Manually refreshing access token
If you need to manually refresh the `access_token` associated with your OAuth2 client, make sure you have a `refresh_token` set in your credentials first and then call:
If you need to manually refresh the `access_token` associated with your OAuth2 client, ensure the call to `generateAuthUrl` sets the `access_type` to `offline`. The refresh token will only be returned for the first authorization by the user. To force consent, set the `prompt` property to `consent`:

```js
// Generate the url that will be used for the consent dialog.
const authorizeUrl = oAuth2Client.generateAuthUrl({
// To get a refresh token, you MUST set access_type to `offline`.
access_type: 'offline',
// set the appropriate scopes
scope: 'https://www.googleapis.com/auth/plus.me',
// A refresh token is only returned the first time the user
// consents to providing access. For illustration purposes,
// setting the prompt to 'consent' will force this consent
// every time, forcing a refresh_token to be returned.
prompt: 'consent'
});
```

If a refresh_token is set again on `OAuth2Client.credentials.refresh_token`, you can can `refreshAccessToken()`:

``` js
const tokens = await oauth2Client.refreshAccessToken();
Expand Down
95 changes: 95 additions & 0 deletions examples/refreshAccessToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2017, Google, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

const OAuth2Client = require('../build/src/index').OAuth2Client;
const http = require('http');
const url = require('url');
const querystring = require('querystring');
const opn = require('opn');

// Download your OAuth2 configuration from the Google
const keys = require('./oauth2.keys.json');

/**
* Start by acquiring a pre-authenticated oAuth2 client.
*/
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}`);
console.log('Refreshing access token ...');
const res = await oAuth2Client.refreshAccessToken();
console.log(`New expiration: ${oAuth2Client.credentials.expiry_date}`);
} catch (e) {
console.error(e);
}
process.exit();
}

/**
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the full client to the callback.
*/
function getAuthenticatedClient() {
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({
// To get a refresh token, you MUST set access_type to `offline`.
access_type: 'offline',
// set the appropriate scopes
scope: 'https://www.googleapis.com/auth/plus.me',
// A refresh token is only returned the first time the user
// consents to providing access. For illustration purposes,
// setting the prompt to 'consent' will force this consent
// every time, forcing a refresh_token to be returned.
prompt: 'consent'
});

// Open an http server to accept the oauth callback. In this simple example, the
// only request to our webserver is to /oauth2callback?code=<code>
const server = http.createServer(async (req, res) => {
if (req.url.indexOf('/oauth2callback') > -1) {
// acquire the code from the querystring, and close the web server.
const qs = querystring.parse(url.parse(req.url).query);
console.log(`Code is ${qs.code}`);
res.end('Authentication successful! Please return to the console.');
server.close();

// Now that we have the code, use that to acquire tokens.
const r = await oAuth2Client.getToken(qs.code)
// Make sure to set the credentials on the OAuth2 client.
oAuth2Client.setCredentials(r.tokens);
console.info('Tokens acquired.');
resolve(oAuth2Client);
}
}).listen(3000, () => {
// open the browser to the authorize url to start the workflow
opn(authorizeUrl);
});
});
}

main();
99 changes: 95 additions & 4 deletions src/auth/oauth2client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,104 @@ import {CredentialRequest, Credentials} from './credentials';
import {LoginTicket} from './loginticket';

export interface GenerateAuthUrlOpts {
/**
* Recommended. Indicates whether your application can refresh access tokens
* when the user is not present at the browser. Valid parameter values are
* 'online', which is the default value, and 'offline'. Set the value to
* 'offline' if your application needs to refresh access tokens when the user
* is not present at the browser. This value instructs the Google
* authorization server to return a refresh token and an access token the
* first time that your application exchanges an authorization code for
* tokens.
*/
access_type?: string;

/**
* The 'response_type' will always be set to 'CODE'.
*/
response_type?: string;

/**
* The client ID for your application. The value passed into the constructor
* will be used if not provided. You can find this value in the API Console.
*/
client_id?: string;

/**
* Determines where the API server redirects the user after the user
* completes the authorization flow. The value must exactly match one of the
* 'redirect_uri' values listed for your project in the API Console. Note that
* the http or https scheme, case, and trailing slash ('/') must all match.
* The value passed into the constructor will be used if not provided.
*/
redirect_uri?: string;

/**
* Required. A space-delimited list of scopes that identify the resources that
* your application could access on the user's behalf. These values inform the
* consent screen that Google displays to the user. Scopes enable your
* application to only request access to the resources that it needs while
* also enabling users to control the amount of access that they grant to your
* application. Thus, there is an inverse relationship between the number of
* scopes requested and the likelihood of obtaining user consent. The
* OAuth 2.0 API Scopes document provides a full list of scopes that you might
* use to access Google APIs. We recommend that your application request
* access to authorization scopes in context whenever possible. By requesting
* access to user data in context, via incremental authorization, you help
* users to more easily understand why your application needs the access it is
* requesting.
*/
scope?: string[]|string;

/**
* Recommended. Specifies any string value that your application uses to
* maintain state between your authorization request and the authorization
* server's response. The server returns the exact value that you send as a
* name=value pair in the hash (#) fragment of the 'redirect_uri' after the
* user consents to or denies your application's access request. You can use
* this parameter for several purposes, such as directing the user to the
* correct resource in your application, sending nonces, and mitigating
* cross-site request forgery. Since your redirect_uri can be guessed, using a
* state value can increase your assurance that an incoming connection is the
* result of an authentication request. If you generate a random string or
* encode the hash of a cookie or another value that captures the client's
* state, you can validate the response to additionally ensure that the
* request and response originated in the same browser, providing protection
* against attacks such as cross-site request forgery. See the OpenID Connect
* documentation for an example of how to create and confirm a state token.
*/
state?: string;

/**
* Optional. Enables applications to use incremental authorization to request
* access to additional scopes in context. If you set this parameter's value
* to true and the authorization request is granted, then the new access token
* will also cover any scopes to which the user previously granted the
* application access. See the incremental authorization section for examples.
*/
include_granted_scopes?: boolean;

/**
* Optional. If your application knows which user is trying to authenticate,
* it can use this parameter to provide a hint to the Google Authentication
* Server. The server uses the hint to simplify the login flow either by
* prefilling the email field in the sign-in form or by selecting the
* appropriate multi-login session. Set the parameter value to an email
* address or sub identifier, which is equivalent to the user's Google ID.
*/
login_hint?: string;

/**
* Optional. A space-delimited, case-sensitive list of prompts to present the
* user. If you don't specify this parameter, the user will be prompted only
* the first time your app requests access. Possible values are:
*
* 'none' - Donot display any authentication or consent screens. Must not be
* specified with other values.
* 'consent' - Prompt the user for consent.
* 'select_account' - Prompt the user to select an account.
*/
prompt?: string;
}

export interface AuthClientOpts {
Expand Down Expand Up @@ -135,13 +228,13 @@ export class OAuth2Client extends AuthClient {
* The base URL for auth endpoints.
*/
private static readonly GOOGLE_OAUTH2_AUTH_BASE_URL_ =
'https://accounts.google.com/o/oauth2/auth';
'https://accounts.google.com/o/oauth2/v2/auth';

/**
* The base endpoint for token retrieval.
*/
private static readonly GOOGLE_OAUTH2_TOKEN_URL_ =
'https://accounts.google.com/o/oauth2/token';
'https://www.googleapis.com/oauth2/v4/token';

/**
* The base endpoint to revoke tokens.
Expand Down Expand Up @@ -180,7 +273,6 @@ export class OAuth2Client extends AuthClient {
opts.response_type = opts.response_type || 'code';
opts.client_id = opts.client_id || this._clientId;
opts.redirect_uri = opts.redirect_uri || this.redirectUri;

// Allow scopes to be passed either as array or a string
if (opts.scope instanceof Array) {
opts.scope = opts.scope.join(' ');
Expand Down Expand Up @@ -226,7 +318,6 @@ export class OAuth2Client extends AuthClient {
data: querystring.stringify(values),
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});

const tokens = res.data as Credentials;
if (res.data && res.data.expires_in) {
tokens.expiry_date =
Expand Down
8 changes: 4 additions & 4 deletions test/test.oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,8 +924,8 @@ describe('OAuth2 client', () => {
let scope: nock.Scope;

beforeEach(() => {
scope = nock('https://accounts.google.com')
.post('/o/oauth2/token', undefined, {
scope = nock('https://www.googleapis.com')
.post('/oauth2/v4/token', undefined, {
reqheaders:
{'content-type': 'application/x-www-form-urlencoded'}
})
Expand Down Expand Up @@ -1056,8 +1056,8 @@ describe('OAuth2 client', () => {
it('should return expiry_date', (done) => {
const now = (new Date()).getTime();
const scope =
nock('https://accounts.google.com')
.post('/o/oauth2/token', undefined, {
nock('https://www.googleapis.com')
.post('/oauth2/v4/token', undefined, {
reqheaders:
{'Content-Type': 'application/x-www-form-urlencoded'}
})
Expand Down

0 comments on commit 18bfdd9

Please sign in to comment.