Part One
Continuing on my series on gradual migration from Coldfusion to Node-js today I’ll be writing up about the authentication system that allows me to have one login that tells both node and coldfusion that the user is authenticated. (Sorry about all the > showing up everywhere – Can’t figure out how to get rid of them)
Authentication
Having worked my way through many iterations of this code, I’m going to start with a quick rundown of the process it took me to get to the end result.
- “I can just proxy through all requests to the CF Server, that sends the cookies as well.” Good points of this is that there’s a node module for that (isn’t there always!), bad point is that if you want to have node communicate with coldfusion as well, for example sockets that call logic on CF, you can’t use the cookie from the CF Server if it’s set up correctly to prevent cookie jacking. So this was a no go as essentially I ended up with 3 sessions, Browser to CF, Browser to Nodejs and Nodejs to CF, not good!!
- “OK, so if I write my own proxy code I can control the cookie right?”. Well, not quite though we’re getting there, again with the same origin restriction on cookies this didn’t work so well.
- “Right, so I have to have all communication with CF sent via node and not store the CF cookie on the browser.” This is about right!
How It’s Done
So if you remember last week there was a function and variable I said I’d explain this week, testCookie and req.session.proxyCookie? Well both of those are to do with my authentication solution.
We start off with the basic cookie and session setup for node:
@use cookieParser security.cookieSecret
@use session
cookie:
httpOnly: true
secure: false
maxAge: 3600000 * 8 # hour in ms * 8 = 8 hours
secret: security.cookieSecret
store: sessionstore
saveUninitialized: true
resave: true
Here we’re using a couchbase memcached session store to store our cookies with some extra settings.
Next we have three functions for working with the Cookies these are:
parseCookie = (cookie) ->
_cookie = request.jar()
if cookie? then _cookie.setCookie require('tough-cookie').Cookie.parse(_c), security.proxyAddress for _c in cookie
_cookie
testProxyCookie = (session) ->
try
_foundCFID = false
for key, cookie of parseCookie(session.proxyCookie)._jar.store.idx[security.proxyCookieDomain]['/']
if key.toLowerCase() is 'cfid' and cookie.value then _foundCFID = true
cookie.lastAccessed = new Date().toISOString()
return _foundCFID
catch
return false
@testCookie = (req, resp, cb, isLogin=false) =>
if req.session? and (not testProxyCookie(req.session) or isLogin) and resp.headers and resp.headers['set-cookie']
req.session.proxyCookie = request.jar()
req.session.proxyCookie resp.headers['set-cookie']
@getSessionCookie req, (sidCookie) ->
sessionstore.set sidCookie, req.session, cb
else cb()
@getSessionCookie = (req, cb) ->
cookieParser(security.cookieSecret) req, {}, (parseErr) ->
if parseErr then return next new Error 'Error parsing cookies.'
# Get the SID cookie
EXPRESS_SID_KEY = 'connect.sid'
cb (req.secureCookies and req.secureCookies[EXPRESS_SID_KEY]) or
(req.signedCookies and req.signedCookies[EXPRESS_SID_KEY]) or
(req.cookies and req.cookies[EXPRESS_SID_KEY]), sessionstore
@sessionAuth = (req, res, next) =>
@getSessionCookie req, (sidCookie) =>
req.sessionroomid = sidCookie
if testProxyCookie req.session then next()
else request
uri: "#{security.proxyAddress}index.cfm/users/nodetestlogin.json"
method: 'GET'
, (err, resp, data) => @testCookie req, resp, next, true
@all '*', @sessionAuth
Let’s Break it Down!!
So, to start off when a user hits a page, the `@all ‘*’, @sessionAuth` gets hit. this in turn calls the `getSessionCookie` which if you haven’t guessed loads the cookies sent to node and finds the ‘connect.sid’ cookie value. We then store this cookie value in the request as the req.sessionroomid which we’ll use later for sockets and updating just one client across all his tabs (I think this may be a security hole but I’m yet to go back and review it yet). Then we call `testProxyCookie`, this function does two important things, first, it checks that the cookie we have stored in the session for communicating with CF is valid, and second it updates the lastAccessed value. Updating the lastAccessed value is important as when you send the cookie through CF uses this to detect if your session should have timed out yet or not! If you don’t update this and have say a 30 minute session timeout on CF, then even if you keep contacting CF constantly, 30 minutes after your first contact your session will expire!! Back in `sessionAuth` if the proxy cookie is valid (`testProxyCookie()` returns true) then we continue via `next()`, if not we send a request to the CF server using request and then use `@testCookie` to set the proxyCookie from the result.
In short `@testCookie` reads the response from the CF Server, parses the cookies and then saves the value in the session value, we later use that cookie with request to authenticate to the CF Server. Beyond the above you can setup your node.js server as you normally would and this will take care of all the integration.
Till Next Time (Which I am yet to decide what I’ll write about!).
UPDATE
I found a nasty bug that runs around the storage of tough-cookie objects – basically it doesn’t work! My solution was to store the ‘set-cookie’ headers instead and then add a ‘parseCookie’ method to convert it into the actual cookie jar object. This means that when you make the call to the CF server you do the following:
request
uri: "#{security.proxyAddress}/index.cfm"
method: 'GET'
jar: parseCookie req.session.proxyCookie
, (err, resp, data) => #handle response here!