Created
December 3, 2013 13:03
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ### | |
| # #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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ### | |
| # #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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ### | |
| # #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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ### | |
| # #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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ### | |
| # #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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey great gist - but i got caught on line 29 of user.coffee - where are you setting/defining the
api.user.authenticatemethod?