google-oauth-jwt
Advanced tools
Comparing version 0.0.7 to 0.1.0
@@ -8,2 +8,1 @@ var auth = require('./lib/auth'), | ||
exports.requestWithJWT = request.requestWithJWT; | ||
exports.resetCache = request.resetCache; |
@@ -5,6 +5,10 @@ var fs = require('fs'), | ||
// constants | ||
var GOOGLE_OAUTH2_URL = 'https://accounts.google.com/o/oauth2/token'; | ||
/** | ||
* Requests a token by submitting a signed JWT to Google. | ||
* @param options The JWT generation options. | ||
* @param callback | ||
* Request an authentication token by submitting a signed JWT to Google OAuth2 service. | ||
* | ||
* @param {Object} options The JWT generation options. | ||
* @param {Function} callback The callback function to invoke with the resulting token. | ||
*/ | ||
@@ -17,10 +21,5 @@ exports.authenticate = function (options, callback) { | ||
if (err) { | ||
if (options.debug) { | ||
console.error('[google-oauth-jwt]: JWT encoding failed >> %s', err); | ||
} | ||
return callback(err); | ||
} | ||
if (err) return callback(err); | ||
return request.post('https://accounts.google.com/o/oauth2/token', { | ||
return request.post(GOOGLE_OAUTH2_URL, { | ||
headers: { | ||
@@ -37,32 +36,45 @@ 'Content-Type': 'application/x-www-form-urlencoded' | ||
if (err) { | ||
if (options.debug) { | ||
console.error('[google-oauth-jwt]: token request failed >> %s', body); | ||
} | ||
return callback(err); | ||
} else if (res.statusCode != 200) { | ||
err = new Error( | ||
'failed to obtain an authentication token, request failed with HTTP code ' + | ||
res.statusCode + ': ' + body.error | ||
); | ||
err.statusCode = res.statusCode; | ||
err.body = body; | ||
return callback(err); | ||
} | ||
if (res.statusCode != 200) { | ||
if (options.debug) { | ||
console.error('[google-oauth-jwt]: token request failed, HTTP %d >> %s', res.statusCode, body); | ||
} | ||
return callback(new Error("access token request failed (HTTP " + res.statusCode + ') : ' + body)); | ||
} | ||
if (options.debug) { | ||
console.log('[google-oauth-jwt]: successfully obtained a valid token'); | ||
} | ||
return callback(null, body.access_token); | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Encodes a JWT using the supplied options. | ||
* @param options The options to use to generate the JWT. | ||
* @param callback | ||
* Encode a JSON Web Token (JWT) using the supplied options. | ||
* | ||
* The token represents an authentication request for a specific user and is signed with a private key to ensure | ||
* authenticity. | ||
* | ||
* Available options are: | ||
* `options.email`: the email address of the service account (required) | ||
* `options.scopes`: an array of scope URIs to demand access for (required) | ||
* `options.key` or options.keyFile: the private key to use to sign the token (required) | ||
* `options.expiration`: the duration of the requested token, in milliseconds (default: 1 hour) | ||
* `options.delegationEmail`: an email address for which access is being granted on behalf of (optional) | ||
* | ||
* @param {Object} options The options to use to generate the JWT | ||
* @param {Function} callback The callback function to invoke with the encoded JSON Web Token (JWT) | ||
*/ | ||
exports.encodeJWT = function (options, callback) { | ||
if (!options) throw new Error('options is required'); | ||
if (!options.email) throw new Error('options.email is required'); | ||
if (!options.scopes) throw new Error('options.scopes is required'); | ||
if (!Array.isArray(options.scopes)) throw new Error('options.scopes must be an array'); | ||
if (options.scopes.length == 0) throw new Error('options.scopes must contain at least one scope'); | ||
if (!options.key && !options.keyFile) throw new Error('options.key or options.keyFile are required'); | ||
callback = callback || function () {}; | ||
var iat = Math.floor(new Date().getTime() / 1000), | ||
@@ -73,3 +85,3 @@ exp = iat + Math.floor((options.expiration || 60 * 60 * 1000) / 1000), | ||
scope: options.scopes.join(' '), | ||
aud: 'https://accounts.google.com/o/oauth2/token', | ||
aud: GOOGLE_OAUTH2_URL, | ||
exp: exp, | ||
@@ -94,10 +106,6 @@ iat: iat | ||
if (JWT_signature == '') { | ||
if (JWT_signature === '') { | ||
return callback(new Error('failed to sign JWT, the key is probably invalid')); | ||
} | ||
if (options.debug) { | ||
console.log('[google-oauth-jwt]: successfully encoded signed JWT'); | ||
} | ||
return callback(null, signedJWT); | ||
@@ -108,12 +116,14 @@ | ||
function obtainKey(callback) { | ||
if (options.key && options.key != '') { | ||
// key is supplied as a string | ||
return callback(null, options.key); | ||
} else if (options.keyFile) { | ||
// read the key from the specified file | ||
return fs.readFile(options.keyFile, callback); | ||
} | ||
return callback(new Error('key is not specified, use options.key or options.keyFile to specify the key to use to sign the JWT')); | ||
return callback(new Error( | ||
'key is not specified, use "options.key" or "options.keyFile" to specify the key to use to sign the JWT' | ||
)); | ||
} | ||
}; |
@@ -1,11 +0,32 @@ | ||
var TokenCache = require('./token-cache'), | ||
tokens = new TokenCache(); | ||
var TokenCache = require('./token-cache'); | ||
/** | ||
* Returns a JWT-enabled request module. | ||
* @param request The request instance to modify to enable JWT. | ||
* @returns {Function} The JWT-enabled request module. | ||
* Returns a Google OAuth2 enabled request module. | ||
* The modified function accepts a "jwt" setting in the options parameter to configure token-based authentication. | ||
* | ||
* When a "jwt" setting is defined, a token will automatically be requested (or reused) and inserted into the | ||
* "authorization" header. | ||
* | ||
* The "jwt" setting accepts the following parameters: | ||
* `email`: the email address of the service account (required) | ||
* `scopes`: an array of scope URIs to demand access for (required) | ||
* `key` or `keyFile`: the private key to use to sign the token (required) | ||
* `expiration`: the duration of the requested token, in milliseconds (default: 1 hour) | ||
* `delegationEmail`: an email address for which access is being granted on behalf of (optional) | ||
* | ||
* @param {Object} tokens The TokenCache instance to use. If not specified, `TokenCache.global` will be used. | ||
* @param {Function} request The request module to modify to enable Google OAuth2 support. If not supplied, the bundled | ||
* version will be used. | ||
* @returns {Function} The modified request module with Google OAuth2 support. | ||
*/ | ||
exports.requestWithJWT = function (request) { | ||
exports.requestWithJWT = function (tokens, request) { | ||
if (typeof tokens === 'function') { | ||
request = tokens; | ||
tokens = null; | ||
} | ||
if (!tokens) { | ||
// use the global token cache | ||
tokens = TokenCache.global; | ||
} | ||
if (!request) { | ||
@@ -17,3 +38,3 @@ // use the request module from our dependency | ||
return function (uri, options, callback) { | ||
if (typeof uri === 'undefined') throw new Error('undefined is not a valid uri or options object.'); | ||
@@ -24,3 +45,3 @@ if ((typeof options === 'function') && !callback) callback = options; | ||
} else if (typeof uri === 'string') { | ||
options = {uri: uri}; | ||
options = { uri: uri }; | ||
} else { | ||
@@ -30,13 +51,10 @@ options = uri; | ||
if (callback) options.callback = callback; | ||
// look for a request with JWT requirement | ||
// look for a request with JWT requirement and perform authentication transparently | ||
if (options.jwt) { | ||
return tokens.get(options.jwt, function (err, token) { | ||
if (err) return callback(err); | ||
options.headers = options.headers || {}; | ||
options.headers.authorization = 'Bearer ' + token; | ||
return request(options, callback); | ||
}); | ||
@@ -46,12 +64,4 @@ } else { | ||
} | ||
}; | ||
}; | ||
/** | ||
* Resets the token cache, clearing previously requested tokens. | ||
*/ | ||
exports.resetCache = function () { | ||
tokens.clear(); | ||
}; | ||
@@ -1,33 +0,65 @@ | ||
var auth = require('./auth'); | ||
/** | ||
* A cache of tokens for reusing previously requested tokens until they expire. | ||
* | ||
* Tokens are requested by calling the `authenticate` method and cached for any combination of `options.email` and | ||
* `options.scopes`. | ||
* | ||
* @constructor TokenCache | ||
*/ | ||
function TokenCache() { | ||
// cache is just a key/value pair | ||
this._cache = {}; | ||
}; | ||
module.exports = function TokenCache() { | ||
/** | ||
* Retrieve an authentication token, or reuse a previously obtained one if it is not expired. | ||
* Only one request will be performed for any combination of `options.email` and `options.scopes`. | ||
* | ||
* @param options The JWT generation options. | ||
* @callback {Function} The callback to invoke with the resulting token. | ||
*/ | ||
TokenCache.prototype.get = function (options, callback) { | ||
var key = options.email + ':' + options.scopes.join(','); | ||
if (!this._cache[key]) { | ||
this._cache[key] = new TokenRequest(this.authenticate, options); | ||
} | ||
this._cache[key].get(callback); | ||
}; | ||
var self = this; | ||
/** | ||
* Clear all tokens previously requested by this instance. | ||
*/ | ||
TokenCache.prototype.clear = function () { | ||
this._cache = {}; | ||
}; | ||
TokenCache.prototype.get = function (options, callback) { | ||
/** | ||
* The method to use to perform authentication and retrieving a token. | ||
* Used for overriding the authentication mechanism. | ||
* | ||
* @param {Object} options The JWT generation options. | ||
* @callback {Function} callback The callback to invoke with the resulting token. | ||
*/ | ||
TokenCache.prototype.authenticate = require('./auth').authenticate; | ||
var key = options.email + ':' + options.scopes.join(','); | ||
/** | ||
* A single cacheable token request with support for concurrency. | ||
* @private | ||
* @constructor | ||
*/ | ||
function TokenRequest(authenticate, options) { | ||
if (!self._cache[key]) { | ||
self._cache[key] = new TokenRequest(options); | ||
} | ||
self._cache[key].get(callback); | ||
}; | ||
TokenCache.prototype.clear = function () { | ||
self._cache = {}; | ||
}; | ||
}; | ||
function TokenRequest(options) { | ||
var self = this; | ||
this.status = 'expired'; | ||
this.pendingCallbacks = []; | ||
// execute accumulated callbacks during the 'pending' state | ||
function fireCallbacks(err, token) { | ||
self.pendingCallbacks.forEach(function (callback) { | ||
callback(err, token); | ||
}); | ||
self.pendingCallbacks = []; | ||
} | ||
this.get = function (callback) { | ||
TokenRequest.prototype.get = function (callback) { | ||
@@ -39,7 +71,5 @@ if (this.status == 'expired') { | ||
auth.authenticate(options, function (err, token) { | ||
authenticate(options, function (err, token) { | ||
if (err) return fireCallbacks(err, null); | ||
self.issued = new Date().getTime(); | ||
self.issued = Date.now(); | ||
self.duration = options.expiration || 60 * 60 * 1000; | ||
@@ -49,10 +79,10 @@ self.token = token; | ||
return fireCallbacks(null, token); | ||
}); | ||
} else if (this.status == 'pending') { | ||
// wait for the pending request instead of issuing a new one | ||
this.pendingCallbacks.push(callback); | ||
} else if (this.status == 'completed') { | ||
if (this.issued + this.duration < new Date().getTime()) { | ||
if (this.issued + this.duration < Date.now()) { | ||
this.status = 'expired'; | ||
@@ -66,9 +96,10 @@ this.get(callback); | ||
}; | ||
} | ||
function fireCallbacks(err, token) { | ||
self.pendingCallbacks.forEach(function (callback) { | ||
callback(err, token); | ||
}); | ||
self.pendingCallbacks = []; | ||
} | ||
} | ||
/** | ||
* The global token cache that can be used as a default instance. | ||
* @type TokenCache | ||
*/ | ||
TokenCache.global = new TokenCache(); | ||
module.exports = TokenCache; |
{ | ||
"name": "google-oauth-jwt", | ||
"version": "0.0.7", | ||
"version": "0.1.0", | ||
"author": { | ||
@@ -22,2 +22,12 @@ "name": "Nicolas Mercier", | ||
}, | ||
"devDependencies": { | ||
"underscore": "*", | ||
"async": "*", | ||
"mocha": "*", | ||
"chai": "*", | ||
"chai-spies": "*" | ||
}, | ||
"scripts": { | ||
"test": "mocha test/*.js -t 5000" | ||
}, | ||
"repository": { | ||
@@ -24,0 +34,0 @@ "type": "git", |
106
README.md
@@ -6,3 +6,3 @@ # google-oauth-jwt | ||
This library generates [JWT](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) tokens to establish | ||
identity to an API, without an end-user being involved. This is the preferred scenario for server-side communications. | ||
identity for an API, without an end-user being involved. This is the preferred scenario for server-side communications. | ||
It can be used to interact with Google APIs requiring access to user data (such as Google Drive, Calendar, etc.) for | ||
@@ -18,3 +18,3 @@ which URL-based callbacks and user authorization prompts are not appropriate. | ||
The package also integrates with [request](https://github.com/mikeal/request) to seamlessly query Google RESTful APIs, | ||
This package also integrates with [request](https://github.com/mikeal/request) to seamlessly query Google RESTful APIs, | ||
which is optional. Integration with [request](https://github.com/mikeal/request) provides automatic requesting of | ||
@@ -30,7 +30,21 @@ tokens, as well as built-in token caching. | ||
### Generating a key to sign the tokens | ||
### How does it work? | ||
1. From the [Google API Console](https://code.google.com/apis/console/), create a | ||
[service account](https://developers.google.com/console/help/#service_accounts). | ||
When using Google APIs from the server (or any non-browser based application), authentication is performed through a | ||
Service Account, which is a special account representing your application. This account has a unique email address that | ||
can be used to grant permissions to. If a user wants to give access to his Google Drive to your application, he must share the files or folders with the Service Account using the supplied email address. | ||
Now that the Service Account has permission to some user resources, the application can query the API with OAuth2. | ||
When using OAuth2, authentication is performed using a token that has been obtained first by submitting a JSON Web | ||
Token (JWT). The JWT identifies the user as well as the scope of the data he wants access to. The JWT is also signed | ||
with a cryptographic key to prevent tampering. Google generates the key and keeps only the public key for validation. | ||
You must keep the private key secure with your application so that you can sign the JWT in order to guarantee its authenticity. | ||
The application requests a token that can be used for authentication in exchange with a valid JWT. The resulting token | ||
can then be used for multiple API calls, until it expires and a new token must be obtained by submitting another JWT. | ||
### Creating a Service Account and generating the encryption key | ||
1. From the [Google API Console](https://code.google.com/apis/console/), create a [service account](https://developers.google.com/console/help/#service_accounts). | ||
2. Download the generated P12 key. | ||
@@ -40,3 +54,3 @@ | ||
3. Convert the key to PEM, so we can use it from the Node crypto module. | ||
3. Convert the key to PEM, so we can use it from the Node [crypto](http://nodejs.org/api/crypto.html) module. | ||
@@ -48,7 +62,7 @@ To do this, run the following in Terminal: | ||
The password for the key is `notasecret`, as mentioned when you downloaded the key. | ||
The password for the key is `notasecret`, as mentioned when you downloaded the key from Google. | ||
### Granting access to resources to be requested through an API | ||
In order to query resources using the API, access must be granted to the service account. Each Google application that | ||
In order to query resources using the API, access must be granted to the Service Account. Each Google application that | ||
has security settings must be configured individually. Access is granted by assigning permissions to the service | ||
@@ -60,7 +74,6 @@ account, using its email address found in the API console. | ||
### Querying a RESTful Google API with "request" | ||
### Querying Google APIs with "request" | ||
In this example, we use a modified instance of [request](https://github.com/mikeal/request) to query the | ||
Google Drive API. The modified request module handles the token automatically using a `jwt` setting passed to | ||
the `request` function. | ||
Google Drive API. `request` is a full-featured HTTP client which will be augmented with Google OAuth2 capabilities by using the `requestWithJWT` method. The modified module will request and cache tokens automatically when supplied with a `jwt` setting in the options. | ||
@@ -87,5 +100,14 @@ ```javascript | ||
Note that the `options` object includes a `jwt` object we use to configure how to encode the JWT. The token will then | ||
automatically be requested and inserted in the query string for this API call. It will also be cached and | ||
automatically be requested and inserted in the authorization header for this API call. It will also be cached and | ||
reused for subsequent calls using the same service account and scopes. | ||
If you want to use a specific version of `request`, simply pass it to the the `requestWithJWT` method as such: | ||
```javascript | ||
// my version of request | ||
var request = require('request'); | ||
// my modified version of request | ||
request = require('google-oauth-jwt').requestWithJWT(request); | ||
``` | ||
### Requesting the token manually | ||
@@ -132,3 +154,3 @@ | ||
### Encoding JWT manually | ||
### Encoding the JWT manually | ||
@@ -152,5 +174,5 @@ It is also possible to encode the JWT manually using the `encodeJWT` method. | ||
### Specifying options | ||
### Specifying JWT generation options | ||
The following options can be specified in order to generate the JWT: | ||
The following options can be specified in order to generate the JWT used for authentication: | ||
@@ -165,5 +187,4 @@ ```javascript | ||
// an array of scopes uris to request access to (required) | ||
// different scopes are available for each application (refer to the app documentation) | ||
// scopes are NOT permission levels, but limitations applied to the API access | ||
// so remember to also grant permissions for the application! | ||
// different scopes are available for each application, refer to the app documentation | ||
// scopes are limitations applied to the API access | ||
scopes: [...], | ||
@@ -177,3 +198,3 @@ | ||
// the key will be used to sign the JWT and validated by Google OAuth | ||
keyFile: 'path_to/key.pem', | ||
keyFile: 'path/to/key.pem', | ||
@@ -185,18 +206,51 @@ // the duration of the requested token in milliseconds (optional) | ||
// if access is being granted on behalf of someone else, specifies who is impersonating the service account | ||
delegationEmail: 'email_address@mycompany.com', | ||
delegationEmail: 'email_address@mycompany.com' | ||
// turns on simple console logging for debugging | ||
debug: false | ||
}; | ||
``` | ||
Options are used to encode the JWT that will be sent to Google OAuth servers in order to issue a token that can then be | ||
used for the APIs. | ||
For more information: | ||
[https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset](https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset) | ||
Options are used to encode the JWT that will be sent to Google OAuth servers in order to issue a token that can then be | ||
used for authentification to Google APIs. The same options are used for `authenticate`, `TokenCache.get` or the `jwt` | ||
setting passed to `request` options. | ||
## Running the tests | ||
Running the unit tests for `google-oauth-jwt` requires a valid Service Account, its encryption key and a URL to test. | ||
To launch the tests, first configure your account in "test/jwt-settings.json" using the sample file. Make sure your | ||
test URL also matches with the requested scopes. The tests do not make any assumption on the results from the API, so | ||
you can use any OAuth2 enabled API. | ||
For example, to run the tests by listing Google Drive files, you can use the following configuration: | ||
```javascript | ||
{ | ||
"email": "my-account@developer.gserviceaccount.com", | ||
"scopes": ["https://www.googleapis.com/auth/drive.readonly"], | ||
"keyFile": "./test/key.pem", | ||
"test_url": "https://www.googleapis.com/drive/v2/files" | ||
} | ||
``` | ||
To run the tests: | ||
```bash | ||
npm test | ||
``` | ||
or | ||
```bash | ||
mocha -t 5000 | ||
``` | ||
The 5 seconds timeout is required since some tests make multiple calls to the API. If you get timeout exceeded errors, | ||
you can bump this value since not all Google APIs may respond with the same timings. | ||
## Changelog | ||
* 0.1.0: improved documentation, introduced unit tests and refactoring aimed at testability | ||
* 0.0.7: fixed token expiration check | ||
@@ -203,0 +257,0 @@ * 0.0.6: fixed request function call when using a URI string without options |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
34526
12
487
284
5
2
1