I'm trying to use node-openid (via passport-google) to authenticate my users with their Google credentials. It works fine on my development machine but when I deploy it to Heroku with 2 dynos, it works when one dyno handles the entire OpenID conversation and fails when the conversation is started on one dyno and completed on the second. In that case I get the following error:
2013-01-15T15:18:24+00:00 app[web.2]: Failed to verify assertion (message: Invalid association handle)
2013-01-15T15:18:24+00:00 app[web.2]: at Strategy.authenticate.identifier (/app/node_modules/passport-google/node_modules/passport-openid/lib/passport-openid/strategy.js:143:36)
...
What's the right way to handle this? Should I be saving the conversation state in a database somehow so that both dynos have access to it?
Update:
Here's the code I used to solve the problem by storing the assocation in MongoDB.
var
GoogleStrategy = require('passport-google').Strategy;
// We have to save the OpenID state in the database so it's available to both
// dynos.
db.collection('OpenID').ensureIndex({expires: 1}, {expireAfterSeconds: 0},
function(err, result) {
if (err) {
throw new Error('Error setting TTL index on OpenID collection.');
}
});
// Use the GoogleStrategy within Passport.
// Strategies in passport require a `validate` function, which accept
// credentials (in this case, an OpenID identifier and profile), and invoke a
// callback with a user object.
strategy = new GoogleStrategy({
returnURL: 'http://localhost:3000/auth/google/return',
realm: 'http://localhost:3000/'
},
function(identifier, profile, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
// To keep the example simple, the user's Google profile is returned to
// represent the logged-in user. In a typical application, you would want
// to associate the Google account with a user record in your database,
// and return that user instead.
profile.identifier = identifier;
return done(null, profile);
});
}
);
strategy.saveAssociation(function(handle, provider, algorithm, secret, expiresIn, done) {
db.collection("OpenID").insert({
handle: handle,
provider: provider,
algorithm: algorithm,
secret: secret,
expires: new Date(Date.now() + 1000 * expiresIn)
}, done);
});
strategy.loadAssociation(function(handle, done) {
db.collection("OpenID").findOne({handle: handle}, function (error, result) {
if (error)
return done(error);
else
return done(null, result.provider, result.algorithm, result.secret);
});
});
You should probably check out the Storing association state section in the node-openid README. By default, session state is stored in-memory on the individual dynos and that's what's causing you problems.
Override the saveAssociation()
and loadAssociation()
mixins to use whatever backing store your application is currently using. There's more documentation in the passport-openid source code.