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

Posted March 13, 2012 at 10:25 AM by Ben Nadel

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.


You Might Also Be Interested In:



Reader Comments

There are no comments posted for this web log entry.

Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 23, 2013 at 9:52 PM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Muhmmadibn Did you figure out a solution to launching PDFs? I am running into the same issues myself. There is no way to close the PDF or go back once you launch it. Thanks in advance! ... read »
May 23, 2013 at 6:06 PM
The Girl Who Broke My Heart, And Made Me A Better Person
Good day,ladies and gentle men, my name is Dr AMADI the great spell caster in Africa, i have help so many people for different kind of problems,who say there is no solution to problems on earth, that ... read »
May 23, 2013 at 4:26 PM
ColdFusion QueryAppend( qOne, qTwo )
@Heather, Glad people are still getting value out of this! ... read »
May 23, 2013 at 3:49 PM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@WebManWalking, I meant the code at the bottom (not the video). I did try to experiment with an intermediary variable, like: value = users.id[ i ]; arrayContains( userIDs, value ); ... but t ... read »
May 23, 2013 at 11:06 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, Are you talking about As Number: YES As String: YES As Java: YES? If so, that's with 3 different ways of referencing the constant 1, not users.id[1]. Query object references(*) are what seem ... read »
May 23, 2013 at 9:55 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Dan, According to the CF Admin, I'm running Java "1.6.0_45". As far as the DB column, in the database it's an INT. I'll see if I can dig into what CF sees it as. @WebManWalking, But h ... read »
May 23, 2013 at 9:49 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, I think the problem is that we're used to loose typing in ColdFusion, like JavaScript. If a value is a number but it's needed in an expression to be a string, noooo problem. I've encountered ... read »
May 23, 2013 at 9:47 AM
ColdFusion QueryAppend( qOne, qTwo )
You rock! Thank you, thank you, thank you!!! ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools