Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
extrabacon committed Apr 22, 2013
0 parents commit b579fea
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 0 deletions.
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
samples/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2013, Nicolas Mercier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
150 changes: 150 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# google-oauth-jwt

Google API OAuth2 authentication for Server to Server applications with Node.js. Requires a Service account from the
Google API console.

This library generates JWT (JSON Web Token) tokens to establish identity without an end-user being involved.
The tokens must be signed with a key that you need to generate from the API console. The tokens are generated from
the specifications found at
[https://developers.google.com/accounts/docs/OAuth2ServiceAccount](https://developers.google.com/accounts/docs/OAuth2ServiceAccount).

It also integrates with [https://github.com/mikeal/request](request) to seamlessly query Google REST APIs.

## Documentation

### Installation
```bash
npm install google-oauth-jwt
```

### Generating a key to sign the tokens

1. From the Google API Console, create a Service account
For more help:
[https://developers.google.com/console/help/#service_accounts](https://developers.google.com/console/help/#service_accounts)

2. Download the generated P12 key. IMPORTANT: keep a copy of the key, Google keeps only the public key.

3. Convert the key to PEM, so we can use it from the Node crypto module.
To do this, run the following in Terminal:
```bash
openssl pkcs12 -in downloaded-key-file.p12 -out your-key-file.pem -nodes
```
The password for the key is "notasecret", as mentioned when you downloaded the key.

### Granting access to resources that can be requested through an API

In order to query resources using the APIs, access must be granted to the Service Account. Each Google application that
has security settings must be configured individually. Access is granted using the email address of the service account.

For example, in order to list files in Google Drive, folders and files must be shared with the Service Account, by using
its email address.

Same goes for Google Calendar, Google Contacts, etc.

### Querying a RESTful Google API

In this example, we will use a modified instance of [https://github.com/mikeal/request](request) to query the
Google Drive API. This modified instance allows to automatically request and cache the token.

Note that the request options object includes a "jwt" setting to specify how to request the token. The token will
automatically be generated and inserted in the querystring for this API call. The token will also be cached and
reused for subsequent calls using the same service account and scopes.

```javascript
var request = require('google-oauth-jwt').requestWithJWT();

request({
url: 'https://www.googleapis.com/drive/v2/files',
jwt: {
// use the email address of the service account, as indicated in the API console
email: 'my-service-account@developer.gserviceaccount.com',
// use the PEM file we generated from the downloaded key
keyFile: 'my-service-account-key.pem',
// specify the scopes you which to access
scopes: ['https://www.googleapis.com/auth/drive.readonly']
}
}, function (err, res, body) {

console.log(JSON.parse(body));

});
```

### Requesting the token manually

If you wish to simply request the token for use with a Google API, use the 'authenticate' method.

```javascript
var googleAuth = require('google-oauth-jwt');

googleAuth.authenticate({
// use the email address of the service account, as indicated in the API console
email: 'my-service-account@developer.gserviceaccount.com',
// use the PEM file we generated from the downloaded key
keyFile: 'my-service-account-key.pem',
// specify the scopes you which to access
scopes: ['https://www.googleapis.com/auth/drive.readonly']
}, function (err, token) {

console.log(token);

});
```

### Specifying options

The following options can be specified in order to generate the JWT:

```javascript
var options = {
// the email address of the service account (required)
email: 'my-service-account@developer.gserviceaccount.com',
// an array of scopes uris to request access to (required)
scopes: [...],
// the cryptographic key as a string, can be the contents of the PEM file
key: 'KEY_CONTENTS',
// the path to the PEM file to use for the cryptographic key (ignored is 'key' is defined)
keyFile: 'KEY_CONTENTS',
// the duration of the token in milliseconds - default is 1 hour (60 * 60 * 1000), maximum allowed by Google is 1 hour
expiration: 3600000,
// if access is being granted on behalf of someone else, specifies who is impersonating the service account
delegationEmail: 'email_address'
};
```

More information:
[https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset](https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset)

## Compatibility

+ Tested with Node 0.8
+ Tested on Mac OS X 10.8

## Dependencies

+ request

## License

The MIT License (MIT)

Copyright (c) 2013, Nicolas Mercier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
var auth = require('./lib/auth'),
request = require('./lib/request-jwt');

exports.authenticate = auth.authenticate;
exports.encodeJWT = auth.encodeJWT;
exports.requestWithJWT = request.requestWithJWT;
exports.resetCache = request.resetCache;
91 changes: 91 additions & 0 deletions lib/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
var fs = require('fs'),
crypto = require('crypto'),
request = require('request');

/**
* Requests a token by submitting a signed JWT.
* @param options The JWT generation options.
* @param callback
*/
exports.authenticate = function (options, callback) {

callback = callback || function () { };

exports.encodeJWT(options, function (err, jwt) {

if (err) return callback(err);

return request.post('https://accounts.google.com/o/oauth2/token', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
form: {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt
}
}, function (err, res, body) {

if (err) return callback(err);

if (res.statusCode == 200) {
try {
return callback(undefined, JSON.parse(body).access_token);
} catch (parseErr) {
return callback(parseErr);
}
}

return callback(new Error("Unable to request an access token (HTTP " + res.statusCode + ') : ' + body));
});
});

};

/**
* Encodes a JWT using the supplied options.
* @param options The options to use to generate the JWT.
* @param callback
*/
exports.encodeJWT = function (options, callback) {

var iat = Math.floor(new Date().getTime() / 1000),
exp = iat + Math.floor((options.expiration || 60 * 60 * 1000) / 1000),
claims = {
iss: options.email,
scope: options.scopes.join(' '),
aud: 'https://accounts.google.com/o/oauth2/token',
exp: exp,
iat: iat
};

if (options.delegationEmail) {
claims.prn = options.delegationEmail;
}

var JWT_header = new Buffer(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString('base64'),
JWT_claimset = new Buffer(JSON.stringify(claims)).toString('base64'),
unsignedJWT = [JWT_header, JWT_claimset].join('.');

obtainKey(function (err, key) {

if (err) return callback(err);

var JWT_signature = crypto.createSign('RSA-SHA256').update(unsignedJWT).sign(key, 'base64'),
signedJWT = [unsignedJWT, JWT_signature].join('.');

return callback(undefined, signedJWT);

});

function obtainKey(callback) {

if (options.key) {
return callback(undefined, options.key);
} else if (options.keyFile) {
return fs.readFile(options.keyFile, callback);
}

return callback(new Error('Key is not specified. Use options.key or options.keyFile to specify a key.'));
}

};
81 changes: 81 additions & 0 deletions lib/request-jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
var auth = require('./auth'),
tokenCache = {},
cacheInvalidations = [];

/**
* Returns a JWT-enabled request module.
* @param request The request instance to modify to enable JWT.
* @returns {Function} The JWT-enabled request module.
*/
exports.requestWithJWT = function (request) {

if (!request) {
// use the request module from our dependency
request = require('request');
}

return function (uri, options, callback) {

if (typeof uri === 'undefined') throw new Error('undefined is not a valid uri or options object.');
if ((typeof options === 'function') && !callback) callback = options;
if (options && typeof options === 'object') {
options.uri = uri;
} else if (typeof uri === 'string') {
options = {uri: uri};
} else {
options = uri;
}
if (callback) options.callback = callback;

// look for a request with JWT requirements
if (options.jwt) {
return getToken(options.jwt, function (err, token) {
// TODO: for now the token is only passed using the query string
// insert the token in the query string
options.qs = options.qs || {};
options.qs.access_token = token;
request(uri, options, callback);
});
} else {
return request(uri, options, callback);
}

};

function getToken(options, callback) {

var key = options.email + ':' + options.scopes.join('|');

if (tokenCache[key]) {
// token is already available, return it now
callback(null, tokenCache[key]);
} else {
// token must be retrieved
auth.authenticate(options, function (err, token) {

if (err) return callback(err);

// store the token for reuse
tokenCache[key] = token;

// setup token expiration
cacheInvalidations.push(setTimeout(function () {
delete tokenCache[key];
}, (options.expiration || 60 * 60 * 1000)));

return callback(null, token);
});
}
}
};

/**
* Resets the token cache, clearing previously requested tokens.
*/
exports.resetCache = function () {
cacheInvalidations.forEach(function (timerId) {
clearTimeout(timerId);
});
tokenCache = {};
};

37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "google-oauth-jwt",
"version": "0.0.1",
"author": {
"name": "Nicolas Mercier",
"email": "nicolas@extrabacon.net"
},
"description": "Implementation of Google OAuth 2.0 for server-to-server interactions, allowing secure use of Google APIs without the end-user being involved.",
"keywords": [
"google",
"api",
"oauth",
"oauth 2.0",
"oauth2",
"service account",
"jwt",
"token",
"server to server"
],
"dependencies": {
"request": "*"
},
"repository": {
"type": "git",
"url": "http://github.com/extrabacon/google-oauth-jwt"
},
"homepage": "http://github.com/extrabacon/google-oauth-jwt",
"bugs": "http://github.com/extrabacon/google-oauth-jwt/issues",
"licenses": [
{
"type": "MIT"
}
],
"engines": {
"node": ">=0.8"
}
}

0 comments on commit b579fea

Please sign in to comment.