Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Dan Wilson's 2011 (North Carolina) with:

ColdFusion 10 - WebSocket Authentication And The onWSAuthenticate() Event Handler

By Ben Nadel on
Tags: ColdFusion

As we saw yesterday, neither session cookies nor session scope are available during a ColdFusion 10 WebSocket request. Authentication and state management have to take place directly inside of the WebSocket control flow. To enable this, ColdFusion 10 has provided the client-side authenticate() method and the server-side event handler, onWSAuthenticate(). ColdFusion 10 also allows for single-sign-on; however, this requires the use of CFLogin - something with which I have very little experience.

NOTE: At the time of this writing, ColdFusion 10 was in public beta.


 
 
 

 
  
 
 
 

Most of the ColdFusion 10 WebSocket events are handled on the server using a Channel Listener. In the following demo (and in my previous demos), I use the ColdFusion component, WSApplication.cfc, as my channel listener. This listener pipes incoming requests into custom event handlers that I have defined in my Application.cfc. The WebSocket authentication event is not specific to any particular channel - it is only specific to a given client. As such, WebSocket authentication does not require a channel listener; it takes place directly in the Application.cfc ColdFusion framework component using the event handler, onWSAuthenticate().

When onWSAuthenticate() is invoked, you are provided with a username, a password, and a user object. This user object will be available for every WebSocket request made by the associated client. As such, any changes that you make to the user object within the onWSAuthenticate() event handler will be available in any subsequent requests made by the same client.

There's nothing magical about WebSocket authentication. Just as with your typical user authentication, we're simply setting flags in one request and then checking for those flags on subsequent requests. There's nothing more to it than that - ColdFusion isn't doing anything implicitly with your authentication code other than using the return() value as a means to trigger a success or error response on the client.

To demonstrate WebSocket authentication, I've created a simple demo that denies Publish events until the user has authenticated. While I don't use a database for this request, you can see that my onWSAuthenticate() event handler looks up the user credentials in a server-side cache. Upon authentication, a userID property is stored in the user's connection info - this userID is then available for all subsequent requests from the same client.

Application.cfc - Our ColdFusion Application Framework Component

  • <cfscript>
  • // NOTE: CFScript tags added for Gist color-coding. Remove.
  •  
  • component
  • output="false"
  • hint="I define the application settings and event handlers."
  • {
  •  
  •  
  • // Define the application settings.
  • this.name = hash( getCurrentTemplatePath() );
  • this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 );
  • this.sessionManagement = false;
  •  
  • // Set up the WebSocket channels.
  • this.wsChannels = [
  • {
  • name: "demo",
  • cfcListener: "WSApplication"
  • }
  • ];
  •  
  •  
  • // I initialize the application.
  • function onApplicationStart(){
  •  
  • // Set up our cache of user accounts. Obviously, this would
  • // normally be in a database; but for this demo, we'll just
  • // use a simple in-memory store.
  • application.accounts = [
  • {
  • id: 1,
  • username: "ben",
  • password: hash( "benpw" )
  • },
  • {
  • id: 2,
  • username: "sarah",
  • password: hash( "sarahpw" )
  • },
  • {
  • id: 3,
  • username: "tricia",
  • password: hash( "triciapw" )
  • }
  • ];
  •  
  • // Return true so the request can process.
  • return( true );
  •  
  • }
  •  
  •  
  • // I handle WebSocket authentication requests. Since WebSocket
  • // requests do not send any Cookies over the wire, we have to
  • // handle authorization and state-management with a separate
  • // set of features.
  • function onWSAuthenticate( username, password, connection ){
  •  
  • // Check to see if this credentials are correct.
  • var index = arrayFind(
  • application.accounts,
  • function( account ){
  •  
  • // Return true if the username/password match.
  • return(
  • (account.username == username) &&
  • (account.password == hash( password ))
  • );
  •  
  • }
  • );
  •  
  • // Check to see if we found a matching record.
  • if (!index){
  •  
  • // NO matching record found! The provided credentials,
  • // were not valid. Simply return false in order to
  • // signify the failure (and prevent the "authenticate")
  • // event on the client.
  • return( false );
  •  
  • }
  •  
  • // Flag the client as authenticated (this is for
  • // programmatic use - this does not seem to affect the way
  • // the code implicitly reacts to subsequent requests).
  • connection.authenticated = true;
  •  
  • // Store the user's record ID with the connection information.
  • // This information will be available across all channels for
  • // all requests made by this client.
  • connection.userID = application.accounts[ index ].id;
  •  
  • // Return true to signify a successful authentication. This
  • // will trigger the "authenticate" event on the client.
  • return( true );
  •  
  • }
  •  
  •  
  • // I initialize the incoming WebSocket request. In this case
  • // we're just gonna run through a number of scopes and data
  • // points to see if they exist during a WebSocket request.
  • function onWSRequestStart( type, channel, user ){
  •  
  • // If this is a call to publish, let's check to see if the
  • // user has been authenticated.
  • if (type == "publish"){
  •  
  • // Check for the user ID
  • if (
  • isNull( user.userID ) ||
  • !user.userID
  • ){
  •  
  • // This user is NOT authenticated.
  • logData( "Publish denied - user not authenticated." );
  •  
  • // Return false so the publish request is cancelled.
  • return( false );
  •  
  • }
  •  
  • // If we made it this far, the user is authenticated!
  • // Log the user ID.
  • logData( "Publish accepted for User ID #user.userID#" );
  •  
  • }
  •  
  • // If we made it this far, return true so that the request
  • // may be fully processed.
  • return( true );
  •  
  • }
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // I log the arguments to the text file for debugging.
  • function logData( data ){
  •  
  • // Create a log file path for debugging.
  • var logFilePath = (
  • getDirectoryFromPath( getCurrentTemplatePath() ) &
  • "log.txt"
  • );
  •  
  • // Dump to TXT file.
  • writeDump( var=data, output=logFilePath );
  •  
  • }
  •  
  •  
  • }
  •  
  • // NOTE: CFScript tags added for Gist color-coding. Remove.
  • </cfscript>

As you can see, once the onWSAuthenticate() event handler is invoked, the property, userID, is available in the subsequent onWSRequestStart() event handler. I can then use the existence of this property to determine if the WebSocket client has been authenticated. ColdFusion 10 provides the flag, "authenticated", as part of the persistent connection object. You can set this in the onWSAuthenticate() event handler if you want to and then use it in subsequent WebSocket events; but, like I said before, ColdFusion doesn't do anything implicitly with this value - managing requests is left up to your discretion.

To make use of this authentication code, I've set up a simple client-side demo that can publish a static message with and without authentication. In each case, the request and response is logged to the console.

index.cfm - Our Client-Side User Interface (UI)

  • <!--- Turn of debugging output. It can't help us in WebSockets. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!---
  • We need to pass the Application name to the ColdFusion WebSocket
  • so that it knows which memory space to use. To use this, we'll
  • pass it through with the HTML element.
  • --->
  • <cfset appName = getApplicationMetaData().name />
  •  
  • <!doctype html>
  • <html data-app-name="<cfset writeOutput( appName ) />">
  • <head>
  • <meta charset="utf-8">
  • <title>Using ColdFusion 10 WebSockets With RequireJS</title>
  •  
  • <!--
  • Load the script loader and boot-strapping code. In this
  • demo, the "main" JavaScript file acts as a Controller for
  • the following Chat interface.
  • -->
  • <script
  • type="text/javascript"
  • src="./js/lib/require/require.js"
  • data-main="./js/main">
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • Please Authenticate Your WebSocket Requests
  • </h1>
  •  
  • <form>
  • <input type="text" name="username" size="20" />
  • <input type="text" name="password" size="20" />
  • <input type="submit" value="Authenticate" />
  • </form>
  •  
  • <p>
  • <a href="#" class="publish">Publish something</a>
  • </p>
  •  
  • </body>
  • </html>

Again, I am using RequireJS as the dependency manager for this ColdFusion 10 WebSocket example. This way, I can use my AMD-compliant ColdFusion 10 WebSocket module. For this demo, I have updated ColdFusionWebSocket() module to return a Deferred object from the authenticate() method. The underlying WebSocket uses success and error events to manage authentication; but, since authentication felt very much like a request/response relationship, I thought the most useful approach would be to provide the Promise of a result on which you could directly bind resolve and reject event handlers.

As you will see in the following code, the Deferred-based authentication provides a very natural, intuitive approach to authentication.

main.js - Our Client-Side Controller And Application Bootstrap

  • // Define the paths to be used in the script mappings. Also, define
  • // the named module for certain libraries that are AMD compliant.
  • require.config({
  • baseUrl: "js/",
  • paths: {
  • "domReady": "lib/require/domReady",
  • "jquery": "lib/jquery/jquery-1.7.1",
  • "order": "lib/require/order",
  • "text": "lib/require/text",
  • }
  • });
  •  
  •  
  • // Load the application. In order for the demo controller to
  • // run, we need to wait for jQuery and the CFWebSocket module to
  • // become available.
  • require(
  • [
  • "jquery",
  • "../../../cfwebsocket",
  • "domReady"
  • ],
  • function( $, ColdFusionWebSocket ){
  •  
  •  
  • // Cache the DOM elements that we'll need in this demo.
  • var dom = {};
  • dom.form = $( "form" );
  • dom.username = $( "input[ name = 'username' ]" );
  • dom.password = $( "input[ name = 'password' ]" );
  • dom.publish = $( "a.publish" );
  •  
  • // Create an instance of our ColdFusion WebSocket module
  • // and subscribe to the "Demo" channel.
  • var socket = new ColdFusionWebSocket( "demo" );
  •  
  •  
  • // Listen for published messages on the "Demo" channel.
  • socket.on(
  • "message",
  • "demo",
  • function( event, data ){
  •  
  • console.log( "Published:", data );
  •  
  • }
  • );
  •  
  •  
  • // Listen for publish errors.
  • socket.on(
  • "error",
  • function( event, message ){
  •  
  • console.log( "Error:", message );
  •  
  • }
  • );
  •  
  •  
  • // Bind to the form submission so we can pipe the request
  • // through our ColdFusion WebSocket connection.
  • dom.form.submit(
  • function( event ){
  •  
  • // Prevent the form submission.
  • event.preventDefault();
  •  
  • // Get the user's credentials.
  • var username = dom.username.val();
  • var password = dom.password.val();
  •  
  • console.log( "Authenticating..." );
  •  
  • // Authenticate! This will return a promise that
  • // we can bind to.
  • var login = socket.authenticate( username, password );
  •  
  • // Look at the success and error handlers to see if
  • // the authentication worked.
  • login.then(
  • function(){
  • console.log( "Authenticate Success!" );
  • },
  • function(){
  • console.log( "Authenticate Failure!" );
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // Bind to the publish link so we can try publishing when
  • // we have different authentication states.
  • dom.publish.click(
  • function( event ){
  •  
  • // Kill the default click behavior - this is not a
  • // real link.
  • event.preventDefault();
  •  
  • // Publish something!
  • socket.publish( "demo", "This is a test message." );
  •  
  • }
  • );
  •  
  •  
  • }
  • );

As you can see in the above code, my ColdFusionWebSocket() module provides an authenticate() method. This method returns a Deferred promise on which you can bind success and fail event handlers. In this case, I'm simply logging those events out to the console.

As I'm making WebSocket requests from the client to the server, the Application.cfc ColdFusion framework component is logging information to the log file. You can see this in the video; it demonstrates how the authentication information is persisted on the server-side across client requests.

ColdFusion 10 WebSockets have their own session management. This exists outside of the normal ColdFusion session management; but, it works in almost the same way - we have a persistent, server-side object associated with a given client. This object can then be mutated over time. The onWSAuthenticate() event handler gives us an easy way to work authentication into the management of that session object. And, hopefully, my ColdFusionWebSocket() module makes this easier as well.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.