Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

google-oauth-jwt

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

google-oauth-jwt - npm Package Compare versions

Comparing version 0.0.7 to 0.1.0

test/jwt-settings.json

1

index.js

@@ -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",

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc