From a6c1e04c72013a670dc836fa7956a844cb93bca2 Mon Sep 17 00:00:00 2001 From: Mark McCahill Date: Wed, 28 Aug 2013 17:46:02 -0400 Subject: [PATCH] allow extra parameters on OAuth2 authorize URL + examples go OAuth2 for Google Calendar to illustrate using extra parameters Google seems to like having a scope parameter specified in the Oauth2 authorize URL, so I added an extra parameters option to the description. There is also a working example of using OAuth2 against one of the Google Calendar APIs Signed-off-by: Mark McCahill --- README.md | 102 +++++++++++++++++++++++++++++++++++++ app.js | 62 ++++++++++++++++++---- public/data/apiconfig.json | 22 ++++++++ public/data/google.json | 57 +++++++++++++++++++++ 4 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 public/data/google.json diff --git a/README.md b/README.md index 1081b748..77d9f94e 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,108 @@ For the Rdio API test, beta access to their new API is necessary. The site for the beta API is: "http://www.rdio.com/developers/" +--- + + +### Example #5 - Google API config that uses Authorization Code OAuth 2 + +```js +"google": { + "name": "Google Calendar API (OAuth 2.0 Auth Code)", + "protocol": "https", + "baseURL": "www.googleapis.com", + "publicPath": "/calendar/v3", + "privatePath": "/calendar/v3", + "headers" : {}, + "booleanTrueVal": "true", + "booleanFalseVal": "false", + "auth": "oauth2", + "oauth2": { + "type": "authorization-code", + "baseSite": "https://accounts.google.com/", + "authorizeURL": "o/oauth2/auth", + "accessTokenURL": "o/oauth2/token", + "customHeaders": {}, + "tokenName": "access_token", + "extraParameters": {"scope": "https://www.googleapis.com/auth/calendar.readonly", "response_type": "code" } + }, + "keyParam": "" +}, +``` + +Line: + +1. Handle of the API. It is used to pull up the client + interface in the URL: + + Ex: http://localhost:3000/google + +2. "name" key value is a string that holds the name + of the API that is used in the Jade template output. + +3. "protocol" key value contains either *http* or *https*, + but you're welcome to try other protocols. + +4. "baseURL" key value is the base URL that accepts + the API calls (should not include protocol) + +5. "publicPath" key value is the path prefix prepended + to all method URIs for non-protected method resources. + This value often includes the version in RESTful APIs. + + Ex: "/v1", "/1", etc. + +6. "privatePath" key value is the path prefix prepended + to all method URIs for OAuth2 protected method resources. + This value is most often the version in RESTful APIs. + + Ex: "/v1", "/1", etc. + +8. The value to be sent for boolean "True" + +9. The value to be sent for boolean "False" + +10. "auth" key value is set to "oauth2" when OAuth2 is the + authentication mechanism. Field is required. + +11. "oauth" key value is a JSON object that contains the + OAuth implementation details for this API. Field is + required when "auth" value is "oauth". + +12. "type" key value is the OAuth 2 authorization flow + used for this API. Valid values are "authorization-code", + "client_credentials", and "implicit", named for each grant + found here: "http://tools.ietf.org/html/rfc6749". + +13. "baseSite" key value is the base website URL used in + the OAuth 2 dance. It is required. + +14. "authorizeURL" key value is the url string used to + retrieve the authorization token in the + "authorization-code" OAuth 2 flow. This is not necessary + in any other OAuth 2 flow. + +15. "accessTokenURL" key value is the url string used to + retrieve the access (Bearer) token in any OAuth 2 flow. + This is required in all OAuth 2 flows. + +17. "tokenName" key value if the API does not use "access_token" + as the default token name when making calls with the + access token in the url query parameters. Not required if + "access_token" is used. + +18. "extraParameters" lists any parameters that must be included + in the Authorization URL. For instance, Google needs to see + a "scope" parameter in the authorization URL. + Only required if this is the case. + +19. Closing curly bracket for "oauth2" JSON object. + +20. "keyParam" key value is blank when OAuth 2 is the authentication + method. + +21. Closing curly bracket for main object. + API-LEVEL CONFIG DETAILS ======================== diff --git a/app.js b/app.js index 914a1902..823a01a7 100755 --- a/app.js +++ b/app.js @@ -132,7 +132,9 @@ app.configure('production', function() { // Middleware // function oauth(req, res, next) { - console.log('OAuth process started'); + if (config.debug) { + console.log('OAuth process started'); + }; var apiName = req.body.apiName, apiConfig = apisConfig[apiName]; @@ -201,7 +203,9 @@ function oauth(req, res, next) { } function oauth2(req, res, next){ - console.log('OAuth2 process started'); + if (config.debug) { + console.log('OAuth2 process started'); + }; var apiName = req.body.apiName, apiConfig = apisConfig[apiName], urlp = url.parse(req.originalUrl, true); @@ -216,7 +220,8 @@ function oauth2(req, res, next){ apiSecret, apiConfig.oauth2.baseSite, apiConfig.oauth2.authorizeURL, - apiConfig.oauth2.accessTokenURL); + apiConfig.oauth2.accessTokenURL, + apiConfig.oauth2.customHeaders ); if (apiConfig.oauth2.tokenName) { oa.setAccessTokenName(apiConfig.oauth2.tokenName); @@ -231,8 +236,15 @@ function oauth2(req, res, next){ }; if (apiConfig.oauth2.type == 'authorization-code') { - var redirectUrl = oa.getAuthorizeUrl({redirect_uri : callbackURL, response_type : 'code'}); + var oauth2Params = {redirect_uri : callbackURL, response_type : 'code'}; + for( var itemKey in apiConfig.oauth2.extraParameters ) { + oauth2Params[itemKey]= apiConfig.oauth2.extraParameters[itemKey]; + }; + var redirectUrl = oa.getAuthorizeUrl( oauth2Params ); + if (config.debug) { + console.log('OAuth redirectUrl: ' + redirectUrl); + }; db.set(key + ':apiKey', apiKey, redis.print); db.set(key + ':apiSecret', apiSecret, redis.print); db.set(key + ':baseURL', req.headers.referer, redis.print); @@ -264,7 +276,7 @@ function oauth2(req, res, next){ var accessURL = apiConfig.oauth2.baseSite + apiConfig.oauth2.accessTokenURL; var basic_cred = apiKey + ':' + apiSecret; var encoded_basic = new Buffer(basic_cred).toString('base64') - + http_method = (apiConfig.oauth2.authorizationHeader == 'Y') ? "POST" : "GET"; header = (apiConfig.oauth2.authorizationHeader == 'Y') ? {'Authorization' : 'Basic ' + encoded_basic} : ''; fillerpost = query.stringify({grant_type : "client_credentials", client_id : apiKey, client_secret : apiSecret}); @@ -317,7 +329,9 @@ function oauth2(req, res, next){ function oauth2Success(req, res, next) { - console.log('oauth2Success started'); + if (config.debug) { + console.log('oauth2Success started'); + }; var apiKey, apiSecret, apiName = req.params.api, @@ -371,8 +385,15 @@ function oauth2Success(req, res, next) { }; if (apiConfig.oauth2.type == 'authorization-code') { + if (config.debug) { + console.log('inside authorization-code'); + console.log('redirect_uri : '+ callbackURL); + console.log('client_id : '+ apiKey); + console.log('client_secret : ' + apiSecret); + }; + oa.getOAuthAccessToken(req.query.code, - {grant_type : "authorization_code", redirect_uri : baseURL, client_id : apiKey, client_secret : apiSecret}, + {grant_type : "authorization_code", redirect_uri : callbackURL, client_id : apiKey, client_secret : apiSecret}, function(error, oauth2access_token, oauth2refresh_token, results){ if (error) { res.send("Error getting OAuth access token : " + util.inspect(error) + "["+oauth2access_token+"]"+ "["+oauth2refresh_token+"]", 500); @@ -548,13 +569,22 @@ function processRequest(req, res, next) { path: apiConfig.publicPath + methodURL// + ((paramString.length > 0) ? '?' + paramString : "") }; + if (config.debug) { + console.log('just built privateReqURL'); + console.log('apiConfig.baseURL: ' + apiConfig.baseURL); + console.log('apiConfig.privatePath: ' + apiConfig.privatePath); + console.log('methodURL: ' + methodURL); + console.log('paramString: ' + paramString); + }; + if (['POST','DELETE','PUT'].indexOf(httpMethod) !== -1) { var requestBody = query.stringify(params); } if (apiConfig.oauth) { - console.log('Using OAuth'); - + if (config.debug) { + console.log('Using OAuth'); + }; // Three legged OAuth if (apiConfig.oauth.type == 'three-legged' && (reqQuery.oauth == 'authrequired' || (req.session[apiName] && req.session[apiName].authed))) { if (config.debug) { @@ -687,8 +717,9 @@ function processRequest(req, res, next) { unsecuredCall(); } } else if (apiConfig.oauth2) { - console.log('Using OAuth2'); - + if (config.debug) { + console.log('Using OAuth2'); + }; if (implicitAccessToken) { db.mset([key + ':access_token', implicitAccessToken ], function(err, results2) { @@ -736,6 +767,15 @@ function processRequest(req, res, next) { var headers = {Authorization : "Bearer " + access_token}; } + if (config.debug) { + console.log('now calling oa._request with'); + console.log('Access token: ' + access_token); + console.log('httpMethod: ' + httpMethod); + console.log('privateReqURL: ' + privateReqURL); + console.log('headers: ' + headers); + console.log('requestBody: ' + requestBody ); + }; + oa._request(httpMethod, privateReqURL, headers, requestBody, access_token, function (error, data, response) { req.call = privateReqURL; diff --git a/public/data/apiconfig.json b/public/data/apiconfig.json index 16cbffba..a3354698 100644 --- a/public/data/apiconfig.json +++ b/public/data/apiconfig.json @@ -1,4 +1,26 @@ { + "google": { + "name": "Google Calendar API (OAuth 2.0 Auth Code)", + "protocol": "https", + "baseURL": "www.googleapis.com", + "publicPath": "/calendar/v3", + "privatePath": "/calendar/v3", + "headers" : {}, + "booleanTrueVal": "true", + "booleanFalseVal": "false", + "auth": "oauth2", + "oauth2": { + "type": "authorization-code", + "baseSite": "https://accounts.google.com/", + "authorizeURL": "o/oauth2/auth", + "accessTokenURL": "o/oauth2/token", + "customHeaders": {}, + "tokenName": "access_token", + "extraParameters": {"scope": "https://www.googleapis.com/auth/calendar.readonly", "response_type": "code" } + }, + "keyParam": "" + }, + "klout": { "name": "Klout v2 API", "protocol": "http", diff --git a/public/data/google.json b/public/data/google.json new file mode 100644 index 00000000..34f03a3a --- /dev/null +++ b/public/data/google.json @@ -0,0 +1,57 @@ +{ + "endpoints":[ + { + "name":"Google Calendar", + "methods":[ + { + "MethodName":"CalendarList: list", + "Synopsis":"Returns entries on the user's calendar list", + "HTTPMethod":"GET", + "URI":"/users/me/calendarList", + "RequiresOAuth":"Y", + "parameters":[ + { + "Name":"maxResults", + "Required":"N", + "Default":"", + "Type":"int", + "Description":"the maximum number of activity items to be returned" + }, + { + "Name":"minAccessRole", + "Required":"N", + "Default":"", + "Type":"enumerated", + "EnumeratedList": [ + "freeBusyReader", + "owner", + "reader", + "writer" + ], + "EnumeratedDescription": { + "freeBusyReader": "The user can read free/busy information.", + "owner": "The user can read and modify events and access control lists", + "reader": "The user can read events that are not private", + "writer": "The user can read and modify events" + }, + "Description":"The minimum access role for the user in the returned entires. Optional. The default is no restriction." + }, + { + "Name":"pageToken", + "Required":"N", + "Default":"", + "Type":"string", + "Description":"Token specifying which result page to return" + }, + { + "Name":"showHidden", + "Required":"N", + "Type":"boolean", + "Description":"Whether to show hidden entries. default's to False" + } + ] + } + ] + } + ] +} \ No newline at end of file