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
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.
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};
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};
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};
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.)
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