Skip to content

Instantly share code, notes, and snippets.

@panjiesw
Created December 3, 2013 13:03
Show Gist options
  • Save panjiesw/7768779 to your computer and use it in GitHub Desktop.
Save panjiesw/7768779 to your computer and use it in GitHub Desktop.
Authentication middleware for actionHero, using MongoDB as database (via Mongoskin) and actionHero's built in Redis support as cache. Heavily based on tutorial by Evan Tahler: http://blog.evantahler.com/blog/authentication-with-actionHero-again.html
###
# #Hash Initializer
# __src/initializers/hash.coffee__
#
# Provides various hash function definition.
# *********************************************
###
crypto = require("crypto")
exports.hash = (api, next) ->
api.hash =
len: 128
iterations: 12000
encode: (str, salt, callback) ->
if 3 is arguments.length
crypto.pbkdf2 str, salt, @iterations, @len, (err, hash) ->
return callback(err) if err
callback null, hash
else
self = this
callback = salt
crypto.randomBytes @len, (err, salt) ->
return callback(err) if err
salt = salt.toString("base64")
crypto.pbkdf2 str, salt, self.iterations, self.len, (err, hash) ->
return callback(err) if err
callback null, salt, hash
match: (str, salt, match, callback) ->
@encode str, salt, (err, hash) ->
return callback(err) if err
callback null, match.toString('hex') is hash.toString('hex')
next()
###
# #Middleware Initializer
# __src/initializers/middleware.coffee__
#
# Provides various middleware initializers definition.
# Middlewares will be run before each request executed,
# by adding them into `api.actions.preProcessors` which is actionHero's exposed prepocessor api.
# *********************************************
###
exports.middleware = (api, next) ->
###
# Check incoming request's http method against
# provided `restrictMethod` property of an action.
# Example:
# ```
# # In some action
# ...
# restrictMethod: ['POST'] # Any request with method other than defined here will be rejected
# ...
# ```
# *********************************************
###
restrictMethodMiddleware = (connection, actionTemplate, next) ->
methods =
allowed: actionTemplate.restrictMethod
current: connection.rawConnection.method
api.log "Checking allowed methods, ", 'debug', methods
if actionTemplate.restrictMethod
unless methods.current in methods.allowed
connection.error = "Method not allowed."
return next connection, no
next connection, yes
api.actions.preProcessors.push restrictMethodMiddleware
###
# Add `authenticate` property to actions and do request check
# for authenticated session based on its value (if it is truthy
# then the action can't be accessed by unauthenticated user).
# *********************************************
###
authenticationMiddleware = (connection, actionTemplate, next) ->
if actionTemplate.authenticate is yes
return api.session.checkAuth connection, (session) ->
# Expose the session object as connection parameter.
connection.params.session = session
next connection, yes
, next
next connection, yes
api.actions.preProcessors.push authenticationMiddleware
next()
###
# #MongoDB Initializer
# __src/initializers/mongo.coffee__
#
# Provides MongoDB connection initialization and shortcut functions.
# *********************************************
###
mongoskin = require 'mongoskin'
exports.mongo = (api, next) ->
config = api.configData.mongo
###
# Expose some of mongo driver object to `api.mongo` so we can use them
# again later without requiring the driver.
###
api.mongo =
ObjectID: mongoskin.ObjectID
ObjectId: mongoskin.ObjectID
Timestamp: mongoskin.Timestamp
router: mongoskin.router
Cursor: mongoskin.Cursor
GridStore: mongoskin.GridStore
api.mongo._start = (api_, next) ->
api.mongo.db = mongoskin.db(
"#{config.user}:#{config.pass}@#{config.host}:#{config.port}/#{config.db}",
w: 1
)
api.mongo.db.open (err, db) ->
if err
api.log "Error opening MongoDB Connection", 'error'
api.log err.stack, 'error'
process.exit 1
else
api.log "MongoDB connection opened", 'debug'
next()
api.mongo._teardown = (api, next) ->
api.mongo.db.close (err, db) ->
api.log "MongoDB connection closed", 'debug'
next()
next()
###
# #Session Initializer
# __src/initializers/session.coffee__
#
# Provides session initializer definitions.
# The session is saved in Redis cache.
# *********************************************
###
exports.session = (api, next) ->
###
# Session variable. `duration` is how long the session persists
# until it's removed, which is 1 hour in this case.
# *********************************************
###
api.session =
prefix: "__session"
duration: 60 * 60 * 1000
###
# Save a session object to cache.
# @connection: actionHero connection object
# @session: session value to be saved, must be able to be JSON.stringify'd
# @callback: callback function with `error` value as parameter, if any
# *********************************************
###
api.session.save = (connection, session, callback) ->
key = api.session.prefix + connection.id
api.cache.save key, session, api.session.duration, (error) ->
if typeof callback is 'function'
callback error
###
# Load a session object from cache.
# @connection: actionHero connection object
# @callback: callback function with parameter of `error, session, expireTimestamp, createdAt, readAt`
# *********************************************
###
api.session.load = (connection, callback) ->
key = api.session.prefix + connection.id
api.cache.load key, (error, session, expireTimestamp, createdAt, readAt) ->
if typeof callback is 'function'
callback error, session, expireTimestamp, createdAt, readAt
###
# Generate a session login for authenticated user.
# The idea here is to save some of the user's object like
# `_id`, `username` or `email`, etc. into the cache. So next time
# a request is coming to some _need-to-be-authenticated_ action, we can
# reference the user without hitting the mongo database.
# @connection: actionHero connection object
# @userid: user's _id
# @callback: callback function with parameter of `error`
# *********************************************
###
api.session.generateAtAuth = (connection, user, callback) ->
session =
loggedIn: yes
userid: user._id.toString() # Hold the user's _id so we can reference them again later.
username: user.username
email: user.email
loggedInAt: new Date().getTime()
api.session.save connection, session, (error) ->
callback error
###
# Check if incoming request is authenticated.
# @connection: actionHero connection object
# @success: callback function to call if the request is authenticated,
# likley to be an action's callback
# @failure: callback function to call if the check is failed,
# likley to yield to action
###
api.session.checkAuth = (connection, success, failure) ->
api.session.load connection, (err, session) ->
session = {} if session is null
unless session.loggedIn is true
connection.error = "Unauthorized."
connection.rawConnection.responseHttpCode = 401
# Prevent next action to render
return failure connection, no
success session
next()
###
# #User Actions
# __src/actions/user.coffee__
#
# Expose actions for user resource,
# including action for authentication and authorization.
# *********************************************
###
###
# Authenticate a user.
# [POST] {"login":"value", "password":"secret"} /authenticate
#
# Required:
# @login: username or valid email address
# @password: a min 6 chars password of this user
# *********************************************
###
exports.authenticate =
name: "authenticate"
description: "Authenticate a user."
inputs:
required: ['login', 'password']
optional: []
outputExample: {}
restrictMethod: ['POST']
version: 1.0
run: (api, connection, next) ->
api.user.authenticate connection, (err, token) ->
if err
connection.error = "Authentication failed, access denied."
else
connection.response.token = token if token
next connection, yes
###
# Test secured action.
# [GET] /secure
# *********************************************
###
exports.secure =
name: "secure"
description: "An example of secured action, need to authenticate first."
inputs:
required: []
optional: []
outputExample:
authenticated: yes
username: "someone"
id: "529dd0df525c373410000042"
authenticate: yes
run: (api, connection, next) ->
connection.response.authenticated = yes
connection.response.username = connection.params.session.username
connection.response.id = connection.params.session.userid
next connection, yes
@alexparker
Copy link

Hey great gist - but i got caught on line 29 of user.coffee - where are you setting/defining the api.user.authenticate method?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment