Authentication with ActionHero Again

actionhero javascript node.js 
2013-06-10
↞ See all posts


Update @ 2014–05–11: As of ActionHero v8.0.8, connection.id is no-longer static for all web requests, in favor of connection.rawConnection.fingerprint. This post has been updated

Intro

I had previously written about authenticating with ActionHero, but that post is out of date as of actionHero v6.0.0. There have been some breaking API changes in actionHero which changed how connections work.

Also, that first post was an overly complex example requiring a mysql database and ORM. As most folks are looking for an archetypical example of how to authenticate, I thought that it would be best to make it as simple as possible.

Notes

  • We use actionHero’s cache methods which probably should not be used in production for this purpose. You can substitute the database of your choice within your own application.
  • Note that only 2 actions are needed, one to create the user and one to log in.
  • For HTTP clients, actionHero drops a session cookie which sets the connection.rawConnection.fingerprint. More information can be found here. Logging-in will bind the session to the id of the http client, which is set in a cookie.
  • We create some common session methods to save and load a session in the cache for the connection which can be located and modified by actions.
  • note that when calling the actionCounter, the session.actionCounter is increased and stored. This is just so we can test that evrything it working.

Setup

  • Create a new actionHero project as described on www.actionHerojs.com
  • create 3 new files
  • ./node_modules/.bin/actionHero generateAction — name user
  • ./node_modules/.bin/actionHero generateAction — name authenticatedAction
  • ./node_modules/.bin/actionHero generateInitializer — name session

initializers/session.js

1exports.session = function (api, next) { 2 api.session = { 3 prefix: "__session:", 4 duration: 60 * 60 * 1000, // 1 hour 5 }; 6 7 api.session.connectionKey = function (connection) { 8 if (connection.type === "web") { 9 return api.session.prefix + connection.rawConnection.fingerprint; 10 } else { 11 return api.session.prefix + conneciton.id; 12 } 13 }; 14 15 api.session.save = function (connection, session, next) { 16 var key = api.session.connectionKey(connection); 17 api.cache.save(key, session, api.session.duration, function (error) { 18 if (typeof next == "function") { 19 next(error); 20 } 21 }); 22 }; 23 24 api.session.load = function (connection, next) { 25 var key = api.session.connectionKey(connection); 26 api.cache.load( 27 key, 28 function (error, session, expireTimestamp, createdAt, readAt) { 29 if (typeof next == "function") { 30 next(error, session, expireTimestamp, createdAt, readAt); 31 } 32 }, 33 ); 34 }; 35 36 api.session.delete = function (connection, next) { 37 var key = api.session.connectionKey(connection); 38 api.cache.destroy(key, function (error) { 39 next(error); 40 }); 41 }; 42 43 api.session.generateAtLogin = function (connection, next) { 44 var session = { 45 loggedIn: true, 46 loggedInAt: new Date().getTime(), 47 }; 48 api.session.save(connection, session, function (error) { 49 next(error); 50 }); 51 }; 52 53 api.session.checkAuth = function ( 54 connection, 55 successCallback, 56 failureCallback, 57 ) { 58 api.session.load(connection, function (error, session) { 59 if (session === null) { 60 session = {}; 61 } 62 if (session.loggedIn !== true) { 63 connection.error = "You need to be authorized for this action"; 64 failureCallback(connection, true); // likley to be an action's callback 65 } else { 66 successCallback(session); // likley to yiled to action 67 } 68 }); 69 }; 70 71 next(); 72};

actions/user.js

1var crypto = require("crypto"); 2var redisPrefix = "__users-"; 3var caluculatePassowrdHash = function (password, salt) { 4 return crypto 5 .createHash("sha256") 6 .update(salt + password) 7 .digest("hex"); 8}; 9var cacheKey = function (connection) { 10 return ( 11 redisPrefix + connection.params.email.replace("@", "_").replace(".", "_") 12 ); 13}; 14 15exports.userAdd = { 16 name: "userAdd", 17 description: "userAdd", 18 inputs: { 19 required: ["email", "password", "firstName", "lastName"], 20 optional: [], 21 }, 22 blockedConnectionTypes: [], 23 outputExample: {}, 24 run: function (api, connection, next) { 25 if (connection.params.password.length < 6) { 26 connection.error = "password must be longer than 6 chars"; 27 next(connection, true); 28 } else { 29 var passwordSalt = api.utils.randomString(64); 30 var passwordHash = caluculatePassowrdHash( 31 connection.params.password, 32 passwordSalt, 33 ); 34 var user = { 35 email: connection.params.email, 36 firstName: connection.params.firstName, 37 lastName: connection.params.lastName, 38 passwordSalt: passwordSalt, 39 passwordHash: passwordHash, 40 }; 41 console.log(cacheKey(connection)); 42 api.cache.save(cacheKey(connection), user, function (error) { 43 connection.error = error; 44 connection.response.userCreated = true; 45 next(connection, true); 46 }); 47 } 48 }, 49}; 50 51exports.logIn = { 52 name: "logIn", 53 description: "logIn", 54 inputs: { 55 required: ["email", "password"], 56 optional: [], 57 }, 58 blockedConnectionTypes: [], 59 outputExample: {}, 60 run: function (api, connection, next) { 61 connection.response.auth = false; 62 console.log(cacheKey(connection)); 63 api.cache.load(cacheKey(connection), function (err, user) { 64 if (err) { 65 connection.error = err; 66 next(connection, true); 67 } else if (user == null) { 68 connection.error = "User not found"; 69 next(connection, true); 70 } else { 71 var passwordHash = caluculatePassowrdHash( 72 connection.params.password, 73 user.passwordSalt, 74 ); 75 if (passwordHash !== user.passwordHash) { 76 connection.error = "incorrect password"; 77 next(connection, true); 78 } else { 79 api.session.generateAtLogin(connection, function () { 80 connection.response.auth = true; 81 next(connection, true); 82 }); 83 } 84 } 85 }); 86 }, 87};

actions/authenticatedAction.js

1exports.action = { 2 name: "authenticatedAction", 3 description: "authenticatedAction", 4 inputs: { 5 required: [], 6 optional: [], 7 }, 8 blockedConnectionTypes: [], 9 outputExample: {}, 10 run: function (api, connection, next) { 11 api.session.checkAuth( 12 connection, 13 function (session) { 14 if (session.actionCounter == null) { 15 session.actionCounter = 0; 16 } 17 session.actionCounter++; 18 connection.response.authenticated = true; 19 connection.response.session = session; 20 api.session.save(connection, session, function () { 21 next(connection, true); 22 }); 23 }, 24 next, 25 ); 26 }, 27};

Run it!

1http://localhost:8080/api/userAdd?email=evan@evantahler.com&password=password&firstName=Evan&lastName=tahler 2http://localhost:8080/api/logIn?email=evan@evantahler.com&password=password 3http://localhost:8080/api/authenticatedAction

All the error cases work as expected (password miss-match, trying to visit authenticatedAction before logging in, etc.)

What this example doesn’t do

  • edit and delete users
  • check that a user still exists in api.session.checkAuth
  • uses a real ORM/database
Hi, I'm Evan

I write about Technology, Software, and Startups. I use my Product Management, Software Engineering, and Leadership skills to build teams that create world-class digital products.

Get in touch