diff --git a/README.md b/README.md index ce8e5406..642bfcdd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ So far, `everyauth` enables you to login via: Google Google Hybrid RocketLabs Development LinkedIn + Windows Live Dropbox Torgeir Tumblr Evernote Danny Amey @@ -1056,6 +1057,62 @@ object whose parameter name keys map to description values: everyauth.linkedin.configurable(); ``` +## Setting up Windows Live OAuth + +```javascript +var everyauth = require('everyauth') + , connect = require('connect'); + +everyauth.windowsLive + .appId('YOUR CLIENT ID HERE') + .appSecret('YOUR CLIENT SECRET HERE') + .findOrCreateUser( function (session, accessToken, accessTokenExtra, liveUserMetadata) { + // find or create user logic goes here + }) + .redirectPath('/'); + +var routes = function (app) { + // Define your routes here +}; + +connect( + connect.bodyParser() + , connect.cookieParser() + , connect.session({secret: 'whodunnit'}) + , everyauth.middleware() + , connect.router(routes); +).listen(3000); +``` + +You can also configure more parameters (most are set to defaults) via +the same chainable API: + +```javascript +everyauth.windowsLive + .entryPath('/auth/windowslive') + .callbackPath('/auth/windowslive/callback'); +``` + +If you want to see what the current value of a +configured parameter is, you can do so via: + +```javascript +everyauth.windowsLive.callbackPath(); // '/auth/windowslive/callback' +everyauth.windowsLive.entryPath(); // '/auth/windowslive' +``` + +To see all parameters that are configurable, the following will return an +object whose parameter name keys map to description values: + +```javascript +everyauth.windowsLive.configurable(); +``` + +To run the Windows Live sample please run access the server through the url local.hosti:3000, +since Windows Live limits the apps by one app per domain and local.host was taken. + +## Setting up Google OAuth2 +======= ### Google OAuth2 ```javascript @@ -2670,6 +2727,8 @@ Thanks to the following contributors for the following modules: - Mail.ru - [Rodolphe Stoclin](https://github.com/srod) - Skyrock +- [Or Kaplan](https://github.com/orkaplan) + - Windows Live - [Danny Amey](https://github.com/dannyamey) - 500px - Evernote diff --git a/example/conf.js b/example/conf.js index 6a28044a..15d68748 100644 --- a/example/conf.js +++ b/example/conf.js @@ -27,6 +27,10 @@ module.exports = { apiKey: 'pv6AWspODUeHIPNZfA531OYcFyB1v23u3y-KIADJdpyw54BXh-ciiQnduWf6FNRH' , apiSecret: 'Pdx7DCoJRdAk0ai3joXsslZvK1DPCQwsLn-T17Opkae22ZYDP5R7gmAoFes9TNHy' } + , windowsLive: { + apiKey: '0000000048088A51' + , apiSecret: 'z5zxAbjIgFMlT4lyeuu5HFJodwku5XGs' + } , google: { clientId: '3335216477.apps.googleusercontent.com' , clientSecret: 'PJMW_uP39nogdu0WpBuqMhtB' diff --git a/example/server.js b/example/server.js index bd6d0870..e8bfa0bd 100644 --- a/example/server.js +++ b/example/server.js @@ -33,6 +33,7 @@ var usersByInstagramId = {}; var usersByFoursquareId = {}; var usersByGowallaId = {}; var usersByLinkedinId = {}; +var usersByWindowsLiveId = {}; var usersByGoogleId = {}; var usersByAngelListId = {}; var usersByYahooId = {}; @@ -208,6 +209,14 @@ everyauth.linkedin return usersByLinkedinId[linkedinUser.id] || (usersByLinkedinId[linkedinUser.id] = addUser('linkedin', linkedinUser)); }) .redirectPath('/'); + + everyauth.windowsLive + .appId(conf.windowsLive.apiKey) + .appSecret(conf.windowsLive.apiSecret) + .findOrCreateUser( function (sess, accessToken, accessSecret, windowsLiveUser) { + return usersByWindowsLiveId[windowsLiveUser.id] || (usersByWindowsLiveId[windowsLiveUser.id] = addUser('windowsLive', windowsLiveUser)); + }) + .redirectPath('/'); everyauth.google .appId(conf.google.clientId) @@ -432,5 +441,6 @@ app.get('/', function (req, res) { app.listen(3000); console.log('Go to http://local.host:3000'); +console.log('For Windows Live Go to http://local.hosti:3000'); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/example/views/home.jade b/example/views/home.jade index 4b39cd68..928c9ca1 100644 --- a/example/views/home.jade +++ b/example/views/home.jade @@ -29,6 +29,9 @@ #linkedin-login a(href='/auth/linkedin', style='border: 0px') img(style='border: 0px', src='http://press.linkedin.com/sites/all/themes/presslinkedin/images/LinkedIn_WebLogo_LowResExample.jpg') + #windows-live-login + a(href='/auth/windowslive', style='border: 0px') + img(style='border: 0px', src='http://upload.wikimedia.org/wikipedia/en/thumb/5/50/Windows_Live_logo.svg/300px-Windows_Live_logo.svg.png') #google-login a(href='/auth/google', style='border: 0px') img(style='border: 0px', src='https://www.google.com/favicon.ico') @@ -117,6 +120,9 @@ - if (everyauth.linkedin) h3 LinkedIn User Data p= JSON.stringify(everyauth.linkedin.user) + - if (everyauth.windowsLive) + h3 Windows Live User Data + p= JSON.stringify(everyauth.windowsLive.user) - if (everyauth.google) h3 Google User Data p= JSON.stringify(everyauth.google.user) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index f313afae..20b0b9b1 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -55,7 +55,7 @@ everyModule.submodule('oauth2') .promises('code') .canBreakTo('authCallbackErrorSteps') .step('getAccessToken') - .accepts('code') + .accepts('req code') .promises('accessToken extra') .step('fetchOAuthUser') .accepts('accessToken') @@ -136,7 +136,11 @@ everyModule.submodule('oauth2') } return parsedUrl.query && parsedUrl.query.code; }) - .getAccessToken( function (code) { + .getAccessToken( function (req, code) { + // Automatic hostname detection + assignment + if (!this._myHostname || this._alwaysDetectHostname) { + this.myHostname(extractHostname(req)); + } var p = this.Promise() , params = { client_id: this._appId diff --git a/lib/modules/windowsLive.js b/lib/modules/windowsLive.js new file mode 100644 index 00000000..5dd468cc --- /dev/null +++ b/lib/modules/windowsLive.js @@ -0,0 +1,77 @@ +var oauthModule = require('./oauth2') + , url = require('url'); + +var fb = module.exports = + oauthModule.submodule('windowsLive') + .configurable({ + scope: 'specify types of access: http://msdn.microsoft.com/en-us/library/hh243646(en-us).aspx', + display: 'The display type to be used for the authorization page. Valid values are "popup", "touch", "page", or "none".', + locale: 'Optional. A market string that determines how the consent UI is localized. If the value of this parameter is missing or is not valid, a market value is determined by using an internal algorithm.' + }) + + .apiHost('https://apis.live.net/v5.0') + .oauthHost('https://oauth.live.com') + + .authPath('https://oauth.live.com/authorize') + + .entryPath('/auth/windowslive') + .accessTokenHttpMethod('get') + .accessTokenPath('/token') + .callbackPath('/auth/windowslive/callback') + + .scope('wl.signin') + .display('page') + + .authQueryParam('scope', function () { + return this._scope && this.scope(); + }) + + .authQueryParam('response_type', function () { + return 'code'; + }) + + .accessTokenParam('grant_type', function () { + return 'authorization_code'; + }) + + .authQueryParam('display', function () { + return this._display && this.display(); + }) + + .authCallbackDidErr( function (req) { + var parsedUrl = url.parse(req.url, true); + return parsedUrl.query && !!parsedUrl.query.error; + }) + + .handleAuthCallbackError( function (req, res) { + var parsedUrl = url.parse(req.url, true) + , errorDesc = parsedUrl.query.error_description; + if (res.render) { + res.render(__dirname + '/../views/auth-fail.jade', { + errorDescription: errorDesc + }); + } else { + // TODO Replace this with a nice fallback + throw new Error("You must configure handleAuthCallbackError if you are not using express"); + } + }) + + .fetchOAuthUser( function (accessToken) { + var p = this.Promise(); + this.oauth.get(this.apiHost() + '/me', accessToken, function (err, data) { + if (err) + return p.fail(err); + var oauthUser = JSON.parse(data); + p.fulfill(oauthUser); + }) + return p; + }) + + .convertErr( function (data) { + if (typeof data == 'string') + return new Error(JSON.parse(data.data).error.message); + if (data) + return new Error(data.error + ' - ' + data.error_description); + else + return new Error('unknown error'); + }); diff --git a/lib/step.js b/lib/step.js index c1503e58..ebd60889 100644 --- a/lib/step.js +++ b/lib/step.js @@ -78,8 +78,9 @@ Step.prototype = { new Error('Step ' + this.name + ' of `' + _module.name + '` is promising: ' + promises.join(', ') + ' ; however, the step returns nothing. ' + - 'Fix the step by returning the expected values OR ' + - 'by returning a Promise that promises said values.') + 'Fix the step by returning the expected values OR ' + + 'by returning a Promise that promises said values.'), + seq.values ); } // Convert return value into a Promise @@ -109,13 +110,13 @@ Step.prototype = { } else if ('string' === typeof err) { err = new Error(err); } - return oldFn.call(this, err); + return oldFn.call(this, err, scope); }; return oldErrback.call(this, fn, scope); }; } - ret.errback(errorCallback); + ret.errback(errorCallback, seq.values); ret.callback( function () { // Store the returned values diff --git a/media/windows_live.ico b/media/windows_live.ico new file mode 100644 index 00000000..2376bb8b Binary files /dev/null and b/media/windows_live.ico differ