From 34509b09782d65f148fabaf9fa82577707664998 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 4 Dec 2017 15:48:53 -0700 Subject: [PATCH] Release v2.0.0 (#3) Release v2.0.0 * Require Node 4 or greater * Remove legacy oauth 1 strategy and related code * Sign all requests when using a signer * Add support for userName parameter * Low Bandwidth: Passing low_bandwidth param if in options. (#1) * Improve example app * Improve test coverage --- .gitignore | 2 + .npmignore | 1 - .travis.yml | 8 +- CHANGELOG.md | 16 + Makefile | 24 -- README.md | 27 +- examples/legacy-login/app.js | 125 -------- examples/legacy-login/package.json | 10 - examples/legacy-login/views/account.ejs | 2 - examples/legacy-login/views/index.ejs | 9 - examples/legacy-login/views/login.ejs | 1 - examples/login/app.js | 79 ++--- examples/login/package.json | 17 +- examples/login/views/account.ejs | 4 + examples/login/views/index.ejs | 4 + examples/login/views/layout.ejs | 21 -- examples/login/views/layoutBottom.ejs | 2 + .../layout.ejs => login/views/layoutTop.ejs} | 3 - lib/passport-familysearch/index.js | 4 +- lib/passport-familysearch/legacy-strategy.js | 174 ----------- lib/passport-familysearch/strategy.js | 51 +++- package.json | 60 +++- test/fs-extensions-test.js | 68 +++++ test/index-test.js | 26 +- test/legacy-strategy-test.js | 173 ----------- test/strategy-test.js | 276 ++++++++---------- 26 files changed, 377 insertions(+), 810 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 Makefile delete mode 100644 examples/legacy-login/app.js delete mode 100644 examples/legacy-login/package.json delete mode 100644 examples/legacy-login/views/account.ejs delete mode 100644 examples/legacy-login/views/index.ejs delete mode 100644 examples/legacy-login/views/login.ejs delete mode 100644 examples/login/views/layout.ejs create mode 100644 examples/login/views/layoutBottom.ejs rename examples/{legacy-login/views/layout.ejs => login/views/layoutTop.ejs} (91%) delete mode 100644 lib/passport-familysearch/legacy-strategy.js create mode 100644 test/fs-extensions-test.js delete mode 100644 test/legacy-strategy-test.js diff --git a/.gitignore b/.gitignore index d95c77f..48654fa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ # Node.js node_modules npm-debug.log +.nyc_output +reports diff --git a/.npmignore b/.npmignore index 2bdd885..3c05c81 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,4 @@ README.md -Makefile doc/ examples/ test/ diff --git a/.travis.yml b/.travis.yml index a57e4db..e9c7b14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: "node_js" node_js: - - 0.4 - - 0.6 - - 0.8 + - '4' + - '6' + - '8' + - 'node' +dist: trusty diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d6f007b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# 2.0.0 (unreleased) + +## **Breaking Changes** +- Drop support for Node versions before 4.x +- Drop support for OAuth 1(a) Legacy strategy + +## New features: +- Add flag for low bandwidth mobile login +- Add flag to help with post-registration login + +## Fixes: +- Example app updated to current Express and other dependencies + +# 1.0.0 + +First public release \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index cfb2f3b..0000000 --- a/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -SOURCES = lib/**/*.js - -# ============================================================================== -# Node Tests -# ============================================================================== - -VOWS = ./node_modules/.bin/vows -TESTS ?= test/*-test.js - -test: - @NODE_ENV=test NODE_PATH=lib $(VOWS) $(TESTS) - -# ============================================================================== -# Static Analysis -# ============================================================================== - -JSHINT = jshint - -hint: lint -lint: - $(JSHINT) $(SOURCES) - - -.PHONY: test hint lint diff --git a/README.md b/README.md index eceaa87..f7b2b18 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # Passport-FamilySearch [Passport](http://passportjs.org/) strategy for authenticating with [FamilySearch](http://familysearch.org) -using the OAuth 2.0 API. The legacy strategy using the OAuth 1.0a API is -available for use, to support older applications. +using the OAuth 2.0 API. This module lets you authenticate using FamilySearch in your Node.js applications. By plugging into Passport, FamilySearch authentication can be @@ -58,28 +57,6 @@ application: For a complete, working example, refer to the [login example](https://github.com/jaredhanson/passport-familysearch/tree/master/examples/login). -## Legacy Usage - -#### Configure Strategy - -The FamilySearch authentication strategy authenticates users using a -FamilySearch account and OAuth tokens. The strategy requires a `verify` -callback, which accepts these credentials and calls `done` providing a user, as -well as `options` specifying a consumer key, consumer secret, and callback URL. - - var FamilySearchStrategy = require('passport-familysearch').LegacyStrategy; - - passport.use(new FamilySearchStrategy({ - consumerKey: FAMILYSEARCH_DEVELOPER_KEY, - consumerSecret: '', - callbackURL: "http://127.0.0.1:3000/auth/familysearch/callback" - }, - function(token, tokenSecret, profile, done) { - User.findOrCreate({ familysearchId: profile.id }, function (err, user) { - return done(err, user); - }); - } - )); ## Tests @@ -98,4 +75,4 @@ well as `options` specifying a consumer key, consumer secret, and callback URL. [The MIT License](http://opensource.org/licenses/MIT) -Copyright (c) 2012-2013 Jared Hanson <[http://jaredhanson.net/](http://jaredhanson.net/)> +Copyright (c) 2012-2017 Jared Hanson <[http://jaredhanson.net/](http://jaredhanson.net/)> diff --git a/examples/legacy-login/app.js b/examples/legacy-login/app.js deleted file mode 100644 index 00ceb71..0000000 --- a/examples/legacy-login/app.js +++ /dev/null @@ -1,125 +0,0 @@ -var express = require('express') - , passport = require('passport') - , util = require('util') - , FamilySearchStrategy = require('passport-familysearch').LegacyStrategy; - -var FAMILYSEARCH_DEVELOPER_KEY = "insert_familysearch_developer_key_here"; - - -// Passport session setup. -// To support persistent login sessions, Passport needs to be able to -// serialize users into and deserialize users out of the session. Typically, -// this will be as simple as storing the user ID when serializing, and finding -// the user by ID when deserializing. However, since this example does not -// have a database of user records, the complete FamilySearch profile is -// serialized and deserialized. -passport.serializeUser(function(user, done) { - done(null, user); -}); - -passport.deserializeUser(function(obj, done) { - done(null, obj); -}); - - -// Use the FamilySearchStrategy within Passport. -// Strategies in passport require a `verify` function, which accept -// credentials (in this case, a token, tokenSecret, and FamilySearch profile), and -// invoke a callback with a user object. -passport.use(new FamilySearchStrategy({ - requestTokenURL: 'https://sandbox.familysearch.org/identity/v2/request_token', - accessTokenURL: 'https://sandbox.familysearch.org/identity/v2/access_token', - userAuthorizationURL: 'https://sandbox.familysearch.org/identity/v2/authorize', - userProfileURL: 'https://sandbox.familysearch.org/identity/v2/user', - consumerKey: FAMILYSEARCH_DEVELOPER_KEY, - consumerSecret: '', - callbackURL: "http://127.0.0.1:3000/auth/familysearch/callback" - }, - function(token, tokenSecret, profile, done) { - // asynchronous verification, for effect... - process.nextTick(function () { - - // To keep the example simple, the user's FamilySearch profile is returned to - // represent the logged-in user. In a typical application, you would want - // to associate the FamilySearch account with a user record in your database, - // and return that user instead. - return done(null, profile); - }); - } -)); - - - - -var app = express(); - -// configure Express -app.configure(function() { - app.set('views', __dirname + '/views'); - app.set('view engine', 'ejs'); - app.use(express.logger()); - app.use(express.cookieParser()); - app.use(express.bodyParser()); - app.use(express.methodOverride()); - app.use(express.session({ secret: 'keyboard cat' })); - // Initialize Passport! Also use passport.session() middleware, to support - // persistent login sessions (recommended). - app.use(passport.initialize()); - app.use(passport.session()); - app.use(app.router); - app.use(express.static(__dirname + '/public')); -}); - - -app.get('/', function(req, res){ - res.render('index', { user: req.user }); -}); - -app.get('/account', ensureAuthenticated, function(req, res){ - res.render('account', { user: req.user }); -}); - -app.get('/login', function(req, res){ - res.render('login', { user: req.user }); -}); - -// GET /auth/familysearch -// Use passport.authenticate() as route middleware to authenticate the -// request. The first step in FamilySearch authentication will involve redirecting -// the user to familysearch.org. After authorization, FamilySearch will redirect the user -// back to this application at /auth/familysearch/callback -app.get('/auth/familysearch', - passport.authenticate('familysearch'), - function(req, res){ - // The request will be redirected to FamilySearch for authentication, so this - // function will not be called. - }); - -// GET /auth/familysearch/callback -// Use passport.authenticate() as route middleware to authenticate the -// request. If authentication fails, the user will be redirected back to the -// login page. Otherwise, the primary route function function will be called, -// which, in this example, will redirect the user to the home page. -app.get('/auth/familysearch/callback', - passport.authenticate('familysearch', { failureRedirect: '/login' }), - function(req, res) { - res.redirect('/'); - }); - -app.get('/logout', function(req, res){ - req.logout(); - res.redirect('/'); -}); - -app.listen(3000); - - -// Simple route middleware to ensure user is authenticated. -// Use this route middleware on any resource that needs to be protected. If -// the request is authenticated (typically via a persistent login session), -// the request will proceed. Otherwise, the user will be redirected to the -// login page. -function ensureAuthenticated(req, res, next) { - if (req.isAuthenticated()) { return next(); } - res.redirect('/login') -} diff --git a/examples/legacy-login/package.json b/examples/legacy-login/package.json deleted file mode 100644 index 8a20b10..0000000 --- a/examples/legacy-login/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "passport-familysearch-examples-login", - "version": "0.0.0", - "dependencies": { - "express": ">= 0.0.0", - "ejs": ">= 0.0.0", - "passport": ">= 0.0.0", - "passport-familysearch": ">= 0.0.0" - } -} diff --git a/examples/legacy-login/views/account.ejs b/examples/legacy-login/views/account.ejs deleted file mode 100644 index 758ba25..0000000 --- a/examples/legacy-login/views/account.ejs +++ /dev/null @@ -1,2 +0,0 @@ -

ID: <%= user.id %>

-

Name: <%= user.displayName %>

diff --git a/examples/legacy-login/views/index.ejs b/examples/legacy-login/views/index.ejs deleted file mode 100644 index af4316d..0000000 --- a/examples/legacy-login/views/index.ejs +++ /dev/null @@ -1,9 +0,0 @@ -<% if (!user) { %> -

Welcome! Please log in.

-<% } else { %> -

Hello, <%= user.displayName %>.

- Log out -<% } %> -

- View Account -

\ No newline at end of file diff --git a/examples/legacy-login/views/login.ejs b/examples/legacy-login/views/login.ejs deleted file mode 100644 index 1c88774..0000000 --- a/examples/legacy-login/views/login.ejs +++ /dev/null @@ -1 +0,0 @@ -Login with FamilySearch diff --git a/examples/login/app.js b/examples/login/app.js index 44c04bb..d510713 100644 --- a/examples/login/app.js +++ b/examples/login/app.js @@ -1,10 +1,27 @@ -var express = require('express') - , passport = require('passport') - , util = require('util') - , FamilySearchStrategy = require('passport-familysearch').Strategy; - -var FAMILYSEARCH_DEVELOPER_KEY = "insert_familysearch_developer_key_here"; - +const express = require('express'); +const path = require('path'); +const logger = require('morgan'); +const cookieParser = require('cookie-parser'); +const bodyParser = require('body-parser'); +const session = require("express-session"); + +const passport = require('passport'); +const FamilySearchStrategy = require('passport-familysearch').Strategy; + +/* + Provide your FamilySearch developer key here. You can register for one at https://www.familysearch.org/developers/ + */ +const FAMILYSEARCH_DEVELOPER_KEY = 'insert_familysearch_developer_key_here'; + +/* + Uncomment the environment that you want to run against. Your developer key must be registered with this environment. + */ +// const FAMILYSEARCH_IDENT_BASE = 'https://ident.familysearch.org'; +// const FAMILYSEARCH_IDENT_BASE = 'https://identbeta.familysearch.org'; +const FAMILYSEARCH_IDENT_BASE = 'https://integration.familysearch.org'; +// const FAMILYSEARCH_API_BASE = 'https://api.familysearch.org'; +// const FAMILYSEARCH_API_BASE = 'https://apibeta.familysearch.org'; +const FAMILYSEARCH_API_BASE = 'https://api-integ.familysearch.org'; // Passport session setup. // To support persistent login sessions, Passport needs to be able to @@ -27,10 +44,11 @@ passport.deserializeUser(function(obj, done) { // credentials (in this case, a token, tokenSecret, and FamilySearch profile), and // invoke a callback with a user object. passport.use(new FamilySearchStrategy({ - authorizationURL: 'https://sandbox.familysearch.org/cis-web/oauth2/v3/authorization', - tokenURL: 'https://sandbox.familysearch.org/cis-web/oauth2/v3/token', + authorizationURL: `${FAMILYSEARCH_IDENT_BASE}/cis-web/oauth2/v3/authorization`, + tokenURL: `${FAMILYSEARCH_IDENT_BASE}/cis-web/oauth2/v3/token`, devKey: FAMILYSEARCH_DEVELOPER_KEY, - callbackURL: "http://127.0.0.1:3000/auth/familysearch/callback" + callbackURL: "http://localhost:3000/auth/familysearch/callback", + userProfileURL: `${FAMILYSEARCH_API_BASE}/platform/users/current` }, function(accessToken, refreshToken, profile, done) { // asynchronous verification, for effect... @@ -45,31 +63,22 @@ passport.use(new FamilySearchStrategy({ } )); +const app = express(); +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); - -var app = express(); - -// configure Express -app.configure(function() { - app.set('views', __dirname + '/views'); - app.set('view engine', 'ejs'); - app.use(express.logger()); - app.use(express.cookieParser()); - app.use(express.bodyParser()); - app.use(express.methodOverride()); - app.use(express.session({ secret: 'keyboard cat' })); - // Initialize Passport! Also use passport.session() middleware, to support - // persistent login sessions (recommended). - app.use(passport.initialize()); - app.use(passport.session()); - app.use(app.router); - app.use(express.static(__dirname + '/public')); -}); - +app.use(logger('dev')); +app.use(session({secret: 'keyboard cat', resave: false, saveUninitialized: true})); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(passport.initialize()); +app.use(passport.session()); app.get('/', function(req, res){ - res.render('index', { user: req.user, layout: 'layout' }); + res.render('index', { user: req.user }); }); app.get('/account', ensureAuthenticated, function(req, res){ @@ -97,7 +106,7 @@ app.get('/auth/familysearch', // request. If authentication fails, the user will be redirected back to the // login page. Otherwise, the primary route function function will be called, // which, in this example, will redirect the user to the home page. -app.get('/auth/familysearch/callback', +app.get('/auth/familysearch/callback', passport.authenticate('familysearch', { failureRedirect: '/login' }), function(req, res) { res.redirect('/'); @@ -108,7 +117,9 @@ app.get('/logout', function(req, res){ res.redirect('/'); }); -app.listen(3000); +app.listen(3000, function () { + console.log('Listening on port 3000'); +}); // Simple route middleware to ensure user is authenticated. @@ -120,3 +131,5 @@ function ensureAuthenticated(req, res, next) { if (req.isAuthenticated()) { return next(); } res.redirect('/login') } + +module.exports = app; diff --git a/examples/login/package.json b/examples/login/package.json index 8a20b10..50d3f11 100644 --- a/examples/login/package.json +++ b/examples/login/package.json @@ -1,10 +1,19 @@ { "name": "passport-familysearch-examples-login", "version": "0.0.0", + "private": true, + "scripts": { + "start": "node app.js" + }, "dependencies": { - "express": ">= 0.0.0", - "ejs": ">= 0.0.0", - "passport": ">= 0.0.0", - "passport-familysearch": ">= 0.0.0" + "body-parser": "^1.18.2", + "cookie-parser": "^1.4.3", + "debug": "^3.1.0", + "ejs": "^2.5.7", + "express": "^4.16.2", + "express-session": "^1.15.6", + "morgan": "^1.9.0", + "passport": "^0.4.0", + "passport-familysearch": "*" } } diff --git a/examples/login/views/account.ejs b/examples/login/views/account.ejs index 5fd8666..afb302c 100644 --- a/examples/login/views/account.ejs +++ b/examples/login/views/account.ejs @@ -1,2 +1,6 @@ +<% include layoutTop %> +

ID: <%= user.id %>

Name: <%= user.contactName %>

+ +<% include layoutBottom %> diff --git a/examples/login/views/index.ejs b/examples/login/views/index.ejs index 3053c59..622991f 100644 --- a/examples/login/views/index.ejs +++ b/examples/login/views/index.ejs @@ -1,5 +1,9 @@ +<% include layoutTop %> + <% if (!user) { %>

Welcome! Please log in.

<% } else { %>

Hello, <%= user.contactName %>.

<% } %> + +<% include layoutBottom %> diff --git a/examples/login/views/layout.ejs b/examples/login/views/layout.ejs deleted file mode 100644 index d4db84b..0000000 --- a/examples/login/views/layout.ejs +++ /dev/null @@ -1,21 +0,0 @@ - - - - Passport-FamilySearch Example - - - <% if (!user) { %> -

- Home | - Log In -

- <% } else { %> -

- Home | - Account | - Log Out -

- <% } %> - <%- body %> - - diff --git a/examples/login/views/layoutBottom.ejs b/examples/login/views/layoutBottom.ejs new file mode 100644 index 0000000..308b1d0 --- /dev/null +++ b/examples/login/views/layoutBottom.ejs @@ -0,0 +1,2 @@ + + diff --git a/examples/legacy-login/views/layout.ejs b/examples/login/views/layoutTop.ejs similarity index 91% rename from examples/legacy-login/views/layout.ejs rename to examples/login/views/layoutTop.ejs index d4db84b..39a0336 100644 --- a/examples/legacy-login/views/layout.ejs +++ b/examples/login/views/layoutTop.ejs @@ -16,6 +16,3 @@ Log Out

<% } %> - <%- body %> - - diff --git a/lib/passport-familysearch/index.js b/lib/passport-familysearch/index.js index 626040e..8a2d8c3 100644 --- a/lib/passport-familysearch/index.js +++ b/lib/passport-familysearch/index.js @@ -1,8 +1,7 @@ /** * Module dependencies. */ -var LegacyStrategy = require('./legacy-strategy') - , Strategy = require('./strategy'); +var Strategy = require('./strategy'); /** @@ -13,5 +12,4 @@ require('pkginfo')(module, 'version'); /** * Expose constructors. */ -exports.LegacyStrategy = LegacyStrategy; exports.Strategy = Strategy; diff --git a/lib/passport-familysearch/legacy-strategy.js b/lib/passport-familysearch/legacy-strategy.js deleted file mode 100644 index 4fccff4..0000000 --- a/lib/passport-familysearch/legacy-strategy.js +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Module dependencies. - */ -var util = require('util') - , OAuthStrategy = require('passport-oauth').OAuthStrategy - , InternalOAuthError = require('passport-oauth').InternalOAuthError; - - -/** - * Legacy `Strategy` constructor. - * - * The FamilySearch legacy authentication strategy authenticates requests by delegating - * to FamilySearch using the OAuth 1.0 protocol. - * - * Applications must supply a `verify` callback which accepts a `token`, - * `tokenSecret` and service-specific `profile`, and then calls the `done` - * callback supplying a `user`, which should be set to `false` if the - * credentials are not valid. If an exception occured, `err` should be set. - * - * Options: - * - `consumerKey` FamilySearch Developer Key - * - `consumerSecret` an empty string, as required by FamilySearch - * - `callbackURL` URL to which FamilySearch will redirect the user after obtaining authorization - * - * Examples: - * - * passport.use(new FamilySearchStrategy({ - * consumerKey: '123-456-789', - * consumerSecret: '' - * callbackURL: 'https://www.example.net/auth/familysearch/callback' - * }, - * function(token, tokenSecret, profile, done) { - * User.findOrCreate(..., function (err, user) { - * done(err, user); - * }); - * } - * )); - * - * @param {Object} options - * @param {Function} verify - * @api public - */ -function Strategy(options, verify) { - options = options || {}; - options.requestTokenURL = options.requestTokenURL || 'https://api.familysearch.org/identity/v2/request_token'; - options.accessTokenURL = options.accessTokenURL || 'https://api.familysearch.org/identity/v2/access_token'; - options.userAuthorizationURL = options.userAuthorizationURL || 'https://api.familysearch.org/identity/v2/authorize'; - options.signatureMethod = options.signatureMethod || 'PLAINTEXT'; - options.sessionKey = options.sessionKey || 'oauth:familysearch'; - - OAuthStrategy.call(this, options, verify); - this.name = 'familysearch'; - this._userProfileURL = options.userProfileURL || 'https://api.familysearch.org/identity/v2/user'; - - // FamilySearch's OAuth implementation does not conform to the specification. - // As a workaround, the underlying node-oauth functions are replaced in order - // to deal with the idiosyncrasies. - - this._oauth._buildAuthorizationHeaders = function(orderedParameters) { - var authHeader="OAuth "; - if( this._isEcho ) { - authHeader += 'realm="' + this._realm + '",'; - } - - for( var i= 0 ; i < orderedParameters.length; i++) { - // Whilst the all the parameters should be included within the signature, only the oauth_ arguments - // should appear within the authorization header. - if( this._isParameterNameAnOAuthParameter(orderedParameters[i][0]) ) { - if (orderedParameters[i][0] === 'oauth_signature') { - // JDH: This is a workaround for FamilySearch's non-conformant OAuth - // implementation, which expects the `oauth_signature` value to - // be unencoded - authHeader+= "" + this._encodeData(orderedParameters[i][0])+"=\""+ orderedParameters[i][1]+"\","; - } else { - authHeader+= "" + this._encodeData(orderedParameters[i][0])+"=\""+ this._encodeData(orderedParameters[i][1])+"\","; - } - } - } - - authHeader= authHeader.substring(0, authHeader.length-1); - return authHeader; - } - - this._oauth._isParameterNameAnOAuthParameter= function(parameter) { - // JDH: This is a workaround to force the `oauth_callback` parameter out of - // the Authorization header and into the request body, where - // FamilySearch expects to find it. - if (parameter === 'oauth_callback') { return false; } - var m = parameter.match('^oauth_'); - if( m && ( m[0] === "oauth_" ) ) { - return true; - } - else { - return false; - } - }; -} - -/** - * Inherit from `OAuthStrategy`. - */ -util.inherits(Strategy, OAuthStrategy); - -/** - * Retrieve user profile from FamilySearch. - * - * This function constructs a normalized profile, with the following properties: - * - * - `id` - * - `displayName` - * - * @param {String} token - * @param {String} tokenSecret - * @param {Object} params - * @param {Function} done - * @api protected - */ -Strategy.prototype.userProfile = function(token, tokenSecret, params, done) { - this._oauth.get(this._userProfileURL + '?dataFormat=application/json&sessionId=' + token, token, tokenSecret, function (err, body, res) { - if (err) { return done(new InternalOAuthError('failed to fetch user profile', err)); } - - try { - var jsonData = JSON.parse(body); - var user = jsonData.users[0]; - - var profile = { provider: 'familysearch' }; - profile.id = user.id; - profile.username = user.username; - for (var i = 0; i < user.names.length; i++) { - var name = user.names[i]; - switch (name.type) { - case 'Display': - profile.displayName = name.value; - break; - case 'Family': - profile.name = profile.name || {}; - profile.name.familyName = name.value; - break; - case 'Given': - profile.name = profile.name || {}; - profile.name.givenName = name.value; - break; - } - } - for (var i = 0; i < user.emails.length; i++) { - var email = user.emails[i]; - if (email.value) { - profile.emails = profile.emails || []; - var profileEmail = { - value: email.value - } - if (email.type === 'Primary') { - profileEmail.primary = true; - } - profile.emails.push(profileEmail); - } - } - - profile._raw = body; - profile._json = jsonData; - - done(null, profile); - } catch (e) { - console.error("Failed to load user profile: ", e); - done(e); - } - }); -} - - -/** - * Expose `Strategy`. - */ -module.exports = Strategy; diff --git a/lib/passport-familysearch/strategy.js b/lib/passport-familysearch/strategy.js index 64bc91c..1b66a5d 100644 --- a/lib/passport-familysearch/strategy.js +++ b/lib/passport-familysearch/strategy.js @@ -2,8 +2,8 @@ * Module dependencies. */ var util = require('util') - , OAuth2Strategy = require('passport-oauth').OAuth2Strategy - , InternalOAuthError = require('passport-oauth').InternalOAuthError; + , OAuth2Strategy = require('passport-oauth2').Strategy + , InternalOAuthError = require('passport-oauth2').InternalOAuthError; /** @@ -20,6 +20,7 @@ var util = require('util') * Options: * - `devKey` FamilySearch Developer Key * - `callbackURL` URL to which FamilySearch will redirect the user after granting authorization + * - `signer` Not needed unless FamilySearch specifically asked you to use client_secret. * * Examples: * @@ -47,8 +48,10 @@ function Strategy(options, verify) { options.tokenURL = options.tokenURL || 'https://ident.familysearch.org/cis-web/oauth2/v3/token'; OAuth2Strategy.call(this, options, verify); + this.name = 'familysearch'; this._userProfileURL = options.userProfileURL || 'https://api.familysearch.org/platform/users/current'; + this._signer = options.signer; } /** @@ -56,31 +59,57 @@ function Strategy(options, verify) { */ util.inherits(Strategy, OAuth2Strategy); + +/** + * Wrap FamilySearch-specific signing to be used in the request + * + * @param {Object} req + * @api protected + */ +Strategy.prototype.authenticate = function(req, options) { + if (this._signer) { + this._oauth2._clientSecret = this._signer(); + } + return OAuth2Strategy.prototype.authenticate.apply(this, arguments); +}; + + /** * Return extra FamilySearch-specific parameters to be included in the authorization * request. * * Options: - * - `signer` Not needed unless FamilySearch specifically asked you to use client_secret. + * - `referrer` Used to initiate an OpenID flow with a third party provider + * - `lowBandwidth` Show a minimal low-bandwidth sign-in page instead of full sign in page + * - `userName` Pre-populate the sign-in page with the provided userName * * @param {Object} options * @return {Object} * @api protected */ Strategy.prototype.authorizationParams = function (options) { - var params = {}, - signer = options.signer; - - if (signer) { - params['client_secret'] = signer.clientSecret(); - this._oauth2._clientSecret = params['client_secret']; - } else { - this._oauth2._clientSecret = ''; + const params = {}; + + if(this._signer) { + params.client_secret = this._signer(); + } + + if(options.referrer) { + params.referrer = options.referrer; + } + + if(options.lowBandwidth){ + params.low_bandwidth = true; + } + + if(options.userName) { + params.userName = options.userName; } return params; }; + /** * Retrieve user profile from FamilySearch. * diff --git a/package.json b/package.json index 0dfeea2..6dd82f4 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,15 @@ { "name": "passport-familysearch", - "version": "1.0.0", + "version": "2.0.0", "description": "FamilySearch authentication strategy for Passport.", - "keywords": ["passport", "familysearch", "auth", "authn", "authentication", "identity"], + "keywords": [ + "passport", + "familysearch", + "auth", + "authn", + "authentication", + "identity" + ], "repository": { "type": "git", "url": "git://github.com/jaredhanson/passport-familysearch.git" @@ -10,25 +17,50 @@ "bugs": { "url": "http://github.com/jaredhanson/passport-familysearch/issues" }, - "author": { "name": "Jared Hanson", "email": "jaredhanson@gmail.com", "url": "http://www.jaredhanson.net/" }, + "author": { + "name": "Jared Hanson", + "email": "jaredhanson@gmail.com", + "url": "http://www.jaredhanson.net/" + }, "contributors": [ - { "name": "Logan Allred", "email": "redbugz@gmail.com" }, - { "name": "Tim Shadel", "email": "timshadel@gmail.com", "url": "http://timshadel.com/" } + { + "name": "Logan Allred", + "email": "redbugz@gmail.com" + }, + { + "name": "Tim Shadel", + "email": "timshadel@gmail.com", + "url": "http://timshadel.com/" + } + ], + "licenses": [ + { + "type": "MIT", + "url": "http://www.opensource.org/licenses/MIT" + } ], - "licenses": [ { - "type": "MIT", - "url": "http://www.opensource.org/licenses/MIT" - } ], "main": "./lib/passport-familysearch", "dependencies": { - "pkginfo": "0.2.x", - "passport-oauth": "~0.1.9" + "passport-oauth2": "^1.4.0", + "pkginfo": "^0.4.1" }, "devDependencies": { - "vows": "0.6.x" + "chai": "^4.1.2", + "chai-passport-strategy": "^1.0.1", + "mocha": "^4.0.1", + "nyc": "^11.3.0" }, "scripts": { - "test": "NODE_PATH=lib node_modules/.bin/vows test/*-test.js" + "test": "nyc mocha test/*" + }, + "engines": { + "node": ">= 4" }, - "engines": { "node": ">= 0.4.0" } + "nyc": { + "reporter": [ + "text", + "lcov" + ], + "report-dir": "./reports/coverage" + } } diff --git a/test/fs-extensions-test.js b/test/fs-extensions-test.js new file mode 100644 index 0000000..0c8cde2 --- /dev/null +++ b/test/fs-extensions-test.js @@ -0,0 +1,68 @@ +'use strict'; + +const chai = require('chai'); +chai.use(require('chai-passport-strategy')); +const expect = chai.expect; +const url = require('url'); + +const FamilySearchStrategy = require('../lib/passport-familysearch/strategy'); + +describe('FamilySearchStrategy familysearch.org-specific extensions', function () { + let redirect, params; + const USERNAME = 'f&ñk#!,usern@m(); with spaces and special chars: ;,/?:@&=+$ -_.!~*\'() #'; + + before(function (done) { + const mockSigner = function () { + return 'mockSignedValue'; + }; + const strategy = new FamilySearchStrategy({ + devKey: 'ABC123', + signer: mockSigner + }, function () {}); + + strategy._oauth2._request = function (method, url, headers, post_body, access_token, callback) { + switch (url) { + case 'https://ident.familysearch.org/cis-web/oauth2/v3/token': + const tokenBody = { + access_token: 'mockAccessToken', + token_type: 'family_search' + }; + callback(null, JSON.stringify(tokenBody), undefined); + break; + case 'https://api.familysearch.org/platform/users/current': + const profileBody = '{"users":[{"id":"ABCD-1234","contactName":"Sandbox Account","email":"noreply@familysearch.org","links":{"self":{"href":"https://api-integ.familysearch.org/platform/users/current"}}}]}'; + callback(null, profileBody, undefined); + break; + default: + callback(new Error(`Could not find matching mock URL for ${url}`)); + break; + } + }; + + chai.passport.use(strategy) + .redirect(function (redirectUrl, status) { + redirect = redirectUrl; + params = url.parse(redirectUrl, true).query; + done(); + }) + .authenticate({referrer: 'example.com', lowBandwidth: true, userName: USERNAME}); + }); + + it('should pass through referrer option', function () { + expect(params.referrer).to.eql('example.com'); + }); + + it('should pass through low bandwidth option', function () { + expect(params.low_bandwidth).to.eql('true'); + }); + + it('should pass through userName option', function () { + expect(redirect).to.include(encodeURIComponent(USERNAME)); + expect(params.userName).to.eql(USERNAME); + }); + + it('should set client_secret with custom signing function', function () { + expect(params.client_secret).to.eql('mockSignedValue'); + }); + +}); diff --git a/test/index-test.js b/test/index-test.js index 5dda1ba..bc18c1a 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -1,15 +1,11 @@ -var vows = require('vows'); -var assert = require('assert'); -var util = require('util'); -var familysearch = require('passport-familysearch'); - - -vows.describe('passport-familysearch').addBatch({ - - 'module': { - 'should report a version': function (x) { - assert.isString(familysearch.version); - }, - }, - -}).export(module); +var expect = require('chai').expect; + +var familysearch = require('..'); + +describe('passport-familysearch', function () { + + it('should report a version', function () { + expect(familysearch.version).to.be.a('string'); + }); + +}); diff --git a/test/legacy-strategy-test.js b/test/legacy-strategy-test.js deleted file mode 100644 index 7571f6a..0000000 --- a/test/legacy-strategy-test.js +++ /dev/null @@ -1,173 +0,0 @@ -var vows = require('vows'); -var assert = require('assert'); -var util = require('util'); -var FamilySearchStrategy = require('../lib/passport-familysearch/legacy-strategy'); - - -vows.describe('FamilySearchStrategy').addBatch({ - - 'strategy': { - topic: function() { - return new FamilySearchStrategy({ - consumerKey: 'ABC123', - consumerSecret: '' - }, - function() {}); - }, - - 'should be named familysearch': function (strategy) { - assert.equal(strategy.name, 'familysearch'); - }, - }, - - 'strategy when loading user profile': { - topic: function() { - var strategy = new FamilySearchStrategy({ - consumerKey: 'ABC123', - consumerSecret: '' - }, - function() {}); - - // mock - strategy._oauth.get = function(url, token, tokenSecret, callback) { - if (url == 'https://api.familysearch.org/identity/v2/user?dataFormat=application/json&sessionId=token') { - var body = '{"session":null,"users":[{"username":"api-user-0000","password":null,"names":[{"value":"Micheal Dickenson","type":"Display"},{"value":"Dickenson","type":"Family"},{"value":"Micheal","type":"Given"}],"emails":[{"value":"noreply@ldschurch.org","type":"Primary"},{"value":null,"type":"Alternate"}],"member":{"ward":null,"stake":null,"templeDistrict":null,"id":"0000000000000"},"id":"XXXX-XXX0","requestedId":null}],"authentication":null,"status":null,"version":"2.7.20120531.1616","statusCode":200,"statusMessage":"OK","deprecated":null}'; - callback(null, body, undefined); - } else { - callback(new Error('something is wrong')); - } - } - - return strategy; - }, - - 'when told to load user profile': { - topic: function(strategy) { - var self = this; - function done(err, profile) { - self.callback(err, profile); - } - - process.nextTick(function () { - strategy.userProfile('token', 'token-secret', {}, done); - }); - }, - - 'should not error' : function(err, req) { - assert.isNull(err); - }, - 'should load profile' : function(err, profile) { - assert.equal(profile.provider, 'familysearch'); - assert.equal(profile.id, 'XXXX-XXX0'); - assert.equal(profile.username, 'api-user-0000'); - assert.equal(profile.displayName, 'Micheal Dickenson'); - assert.equal(profile.name.familyName, 'Dickenson'); - assert.equal(profile.name.givenName, 'Micheal'); - assert.equal(profile.emails.length, 1); - assert.equal(profile.emails[0].value, 'noreply@ldschurch.org'); - assert.equal(profile.emails[0].primary, true); - }, - 'should set raw property' : function(err, profile) { - assert.isString(profile._raw); - }, - 'should set json property' : function(err, profile) { - assert.isObject(profile._json); - }, - }, - }, - - 'strategy when loading user profile with a userProfileURL option': { - topic: function() { - var strategy = new FamilySearchStrategy({ - userProfileURL: 'https://sandbox.familysearch.org/identity/v2/user', - consumerKey: 'ABC123', - consumerSecret: '' - }, - function() {}); - - // mock - strategy._oauth.get = function(url, token, tokenSecret, callback) { - if (url == 'https://sandbox.familysearch.org/identity/v2/user?dataFormat=application/json&sessionId=token') { - var body = '{"session":null,"users":[{"username":"api-user-0000","password":null,"names":[{"value":"Micheal Dickenson","type":"Display"},{"value":"Dickenson","type":"Family"},{"value":"Micheal","type":"Given"}],"emails":[{"value":"noreply@ldschurch.org","type":"Primary"},{"value":null,"type":"Alternate"}],"member":{"ward":null,"stake":null,"templeDistrict":null,"id":"0000000000000"},"id":"XXXX-XXX0","requestedId":null}],"authentication":null,"status":null,"version":"2.7.20120531.1616","statusCode":200,"statusMessage":"OK","deprecated":null}'; - callback(null, body, undefined); - } else { - callback(new Error('something is wrong')); - } - } - - return strategy; - }, - - 'when told to load user profile': { - topic: function(strategy) { - var self = this; - function done(err, profile) { - self.callback(err, profile); - } - - process.nextTick(function () { - strategy.userProfile('token', 'token-secret', {}, done); - }); - }, - - 'should not error' : function(err, req) { - assert.isNull(err); - }, - 'should load profile' : function(err, profile) { - assert.equal(profile.provider, 'familysearch'); - assert.equal(profile.id, 'XXXX-XXX0'); - assert.equal(profile.username, 'api-user-0000'); - assert.equal(profile.displayName, 'Micheal Dickenson'); - assert.equal(profile.name.familyName, 'Dickenson'); - assert.equal(profile.name.givenName, 'Micheal'); - }, - 'should set raw property' : function(err, profile) { - assert.isString(profile._raw); - }, - 'should set json property' : function(err, profile) { - assert.isObject(profile._json); - }, - }, - }, - - 'strategy when loading user profile and encountering an error': { - topic: function() { - var strategy = new FamilySearchStrategy({ - consumerKey: 'ABC123', - consumerSecret: '' - }, - function() {}); - - // mock - strategy._oauth.get = function(url, token, tokenSecret, callback) { - callback(new Error('something went wrong')); - } - - return strategy; - }, - - 'when told to load user profile': { - topic: function(strategy) { - var self = this; - function done(err, profile) { - self.callback(err, profile); - } - - process.nextTick(function () { - strategy.userProfile('token', 'token-secret', {}, done); - }); - }, - - 'should error' : function(err, req) { - assert.isNotNull(err); - }, - 'should wrap error in InternalOAuthError' : function(err, req) { - assert.equal(err.constructor.name, 'InternalOAuthError'); - }, - 'should not load profile' : function(err, profile) { - assert.isUndefined(profile); - }, - }, - }, - -}).export(module); diff --git a/test/strategy-test.js b/test/strategy-test.js index 3535b11..d52e6ed 100644 --- a/test/strategy-test.js +++ b/test/strategy-test.js @@ -1,161 +1,119 @@ -var vows = require('vows'); -var assert = require('assert'); -var util = require('util'); -var FamilySearchStrategy = require('../lib/passport-familysearch/strategy'); - -vows.describe('FamilySearchStrategy').addBatch({ - - 'strategy': { - topic: function() { - return new FamilySearchStrategy({ - devKey: 'ABC123' - }, - function() {}); - }, - - 'should be named familysearch': function (strategy) { - assert.equal(strategy.name, 'familysearch'); - } - }, - - 'strategy when loading user profile': { - topic: function() { - var strategy = new FamilySearchStrategy({ - devKey: 'ABC123' - }, - function() {}); - - // mock - strategy._oauth2._request = function(method, url, headers, post_body, access_token, callback) { - if (url == 'https://familysearch.org/platform/users/current') { - var body = '{"users":[{"id":"XXXX-XXX0","contactName":"Micheal Dickenson","email":"noreply@familysearch.org","links":{"self":{"href":"https://familysearch.org/platform/users/current"}}}]}'; - callback(null, body, undefined); - } else { - callback(new Error('something is wrong')); - } - }; - - return strategy; - }, - - 'when told to load user profile': { - topic: function(strategy) { - var self = this; - function done(err, profile) { - self.callback(err, profile); - } - - process.nextTick(function () { - strategy.userProfile('access-token', done); - }); - }, - - 'should not error' : function(err, req) { - assert.isNull(err); - }, - 'should load profile' : function(err, profile) { - assert.equal(profile.provider, 'familysearch'); - assert.equal(profile.id, 'XXXX-XXX0'); - assert.equal(profile.contactName, 'Micheal Dickenson'); - assert.equal(profile.email, 'noreply@familysearch.org'); - }, - 'should set raw property' : function(err, profile) { - assert.isString(profile._raw); - }, - 'should set json property' : function(err, profile) { - assert.isObject(profile._json); +'use strict'; + +const chai = require('chai'); +chai.use(require('chai-passport-strategy')); +const expect = chai.expect; + +const FamilySearchStrategy = require('../lib/passport-familysearch/strategy'); +const InternalOAuthError = require('passport-oauth2').InternalOAuthError; + +describe('FamilySearchStrategy', function () { + let strategy; + + before(function (done) { + strategy = new FamilySearchStrategy({ + devKey: 'ABC123' + }, function () {}); + + chai.passport.use(strategy) + .redirect(function (redirectUrl, status) { + // console.log('redirect', redirectUrl, status); + done(); + }) + .authenticate(); + }); + + it('should be named familysearch', function () { + expect(strategy.name).to.eql('familysearch'); + }); + + it('should load user profile', function (done) { + strategy._oauth2._request = function (method, url, headers, post_body, access_token, callback) { + if (url === 'https://api.familysearch.org/platform/users/current') { + const body = '{"users":[{"id":"XXXX-XXX0","contactName":"Some Person","email":"noreply@familysearch.org","links":{"self":{"href":"https://api.familysearch.org/platform/users/current"}}}]}'; + callback(null, body, undefined); + } else { + callback(new Error('something is wrong')); } - } - }, - - 'strategy when loading user profile with a userProfileURL option': { - topic: function() { - var strategy = new FamilySearchStrategy({ - userProfileURL: 'https://sandbox.familysearch.org/platform/users/current', - devKey: 'ABC123' - }, - function() {}); - - // mock - strategy._oauth2._request = function(method, url, headers, post_body, access_token, callback) { - if (url == 'https://sandbox.familysearch.org/platform/users/current') { - var body = '{"users":[{"id":"XXXX-XXX0","contactName":"Micheal Dickenson","email":"noreply@familysearch.org","links":{"self":{"href":"https://sandbox.familysearch.org/platform/users/current"}}}]}'; - callback(null, body, undefined); - } else { - callback(new Error('something is wrong')); - } - }; - - return strategy; - }, - - 'when told to load user profile': { - topic: function(strategy) { - var self = this; - function done(err, profile) { - self.callback(err, profile); - } - - process.nextTick(function () { - strategy.userProfile('access-token', done); - }); - }, - - 'should not error' : function(err, req) { - assert.isNull(err); - }, - 'should load profile' : function(err, profile) { - assert.equal(profile.provider, 'familysearch'); - assert.equal(profile.id, 'XXXX-XXX0'); - assert.equal(profile.contactName, 'Micheal Dickenson'); - assert.equal(profile.email, 'noreply@familysearch.org'); - }, - 'should set raw property' : function(err, profile) { - assert.isString(profile._raw); - }, - 'should set json property' : function(err, profile) { - assert.isObject(profile._json); + }; + + process.nextTick(function () { + strategy.userProfile('access-token', function (err, profile) { + expect(err).to.not.exist; + expect(profile.provider).to.eql('familysearch'); + expect(profile.id).to.eql('XXXX-XXX0'); + expect(profile.contactName).to.eql('Some Person'); + expect(profile.email).to.eql('noreply@familysearch.org'); + expect(profile._raw).to.be.a('string'); + expect(profile._json).to.be.an('object'); + + done(); + }); + }); + + }); + + it('should load user profile via userProfileURL property', function (done) { + strategy = new FamilySearchStrategy({ + userProfileURL: 'https://api-integ.familysearch.org/platform/users/current', + devKey: 'ABC123' + }, function () {}); + + strategy._oauth2._request = function (method, url, headers, post_body, access_token, callback) { + if (url === 'https://api-integ.familysearch.org/platform/users/current') { + const body = '{"users":[{"id":"ABCD-1234","contactName":"Sandbox Account","email":"noreply@familysearch.org","links":{"self":{"href":"https://api-integ.familysearch.org/platform/users/current"}}}]}'; + callback(null, body, undefined); + } else { + callback(new Error('something is wrong')); } - } - }, - - 'strategy when loading user profile and encountering an error': { - topic: function() { - var strategy = new FamilySearchStrategy({ - devKey: 'ABC123' - }, - function() {}); - - // mock - strategy._oauth2._request = function(method, url, headers, post_body, access_token, callback) { - callback(new Error('something went wrong')); - }; - - return strategy; - }, - - 'when told to load user profile': { - topic: function(strategy) { - var self = this; - function done(err, profile) { - self.callback(err, profile); - } - - process.nextTick(function () { - strategy.userProfile('access-token', done); - }); - }, - - 'should error' : function(err, req) { - assert.isNotNull(err); - }, - 'should wrap error in InternalOAuthError' : function(err, req) { - assert.equal(err.constructor.name, 'InternalOAuthError'); - }, - 'should not load profile' : function(err, profile) { - assert.isUndefined(profile); - } - } - } - -}).export(module); + }; + + process.nextTick(function () { + strategy.userProfile('access-token', function (err, profile) { + expect(err).to.not.exist; + expect(profile.provider).to.eql('familysearch'); + expect(profile.id).to.eql('ABCD-1234'); + expect(profile.contactName).to.eql('Sandbox Account'); + expect(profile.email).to.eql('noreply@familysearch.org'); + expect(profile._raw).to.be.a('string'); + expect(profile._json).to.be.an('object'); + + done(); + }); + }); + + }); + + it('should handle errors when loading user profile', function (done) { + strategy._oauth2._request = function (method, url, headers, post_body, access_token, callback) { + callback(new Error('Profile loading error')); + }; + + process.nextTick(function () { + strategy.userProfile('access-token', function (err, profile) { + expect(err).to.be.an.instanceof(InternalOAuthError); + expect(err).to.match(/Profile loading error/); + expect(profile).to.not.exist; + + done(); + }); + }); + }); + + it('should handle malformed response when loading user profile', function (done) { + strategy._oauth2._request = function (method, url, headers, post_body, access_token, callback) { + callback(null, 'Not Found'); + }; + + process.nextTick(function () { + strategy.userProfile('access-token', function (err, profile) { + expect(err).to.be.an.instanceof(SyntaxError); + expect(err).to.match(/Unexpected token/); + expect(profile).to.not.exist; + + done(); + }); + }); + }); + +});