I was looking into ways in which users can be authenticated before they start to receive or send messages over a WebSocket channel in ColdFusion. I found that there are two ways in which this can be done. The first approach is to call the 'authenticate' method on the socket object. The other way is to use cflogin to authenticate the user. The authentication level can be taken a step further in various channel listener functions. By implementing these listener functions, some sort business logic can be devised wherein the logged in user with certain credentials can be allowed to subscribe or publish to a channel.
As explained in my previous post, you can define multiple channels in Application.cfc by providing the name of the channel in this.wschannels variable. By default, these channels are associated with the channel listener – ‘CFIDE.websocket.ChannelListener’. There are various methods defined in this ChannelListener, you can override the default functionality by extending this channel listener. The user defined channel listener can then implement some of the methods that give more control on various functionalities (explained below). Once this is in place, you can let the application know that you are using a custom channel listener by providing a value to the attribute cfclistener for the channel:
socket.subscribe("myChannel",{age:25},myChannelHandler);
socket.publish("myChannel",”message_here”, {name:'Sagar'});
Since we have already authenticated the user and assigned a role; the connectionInfo object which is available for the methods mentioned above would be containing these values (assigned earlier in onWSAuthenticate). One can check for the same and allow\disallow the user to subscribe\publish by returning a boolean value.
The other way of authenticating users is to specify useCFAuth=true in the cfwebsocket tag. Here a user is authenticated using cflogin and cfloginuser tag and this will make the connectionInfo struct available in listener functions. Also, since the user is authenticated using cflogin you need not invoke the authenticate method on the socket object.
As explained in my previous post, you can define multiple channels in Application.cfc by providing the name of the channel in this.wschannels variable. By default, these channels are associated with the channel listener – ‘CFIDE.websocket.ChannelListener’. There are various methods defined in this ChannelListener, you can override the default functionality by extending this channel listener. The user defined channel listener can then implement some of the methods that give more control on various functionalities (explained below). Once this is in place, you can let the application know that you are using a custom channel listener by providing a value to the attribute cfclistener for the channel:
component {
this.name = "WebSocketAPP";
this.wschannels = [{name="myChannel", cfclistener = "ChannelListenerComponent"}];
}
Once a connection is established with the WebSocket server, it would be a good idea to authenticate the user. The authenticate method on the socket object can be called by providing the username and password details:
function messageHandler(msg) {
if(msg.type == "response") {
if(msg.reqType == "welcome") {
//authenticate once the connection is established successfully
socket.authenticate("admin","admin");
}
console.log('In message handler ' + msg.reqType);
}
if(msg.code == -1) {
console.log(msg.msg);
}
}
The JavaScript function 'messageHandler' would be called whenever a message or an acknowledgement from the server is received. As you can see the authenticate method is called on the websocket object (socket) with username and password. When this method is called the onWSAuthenticate method defined in Application.cfc would be invoked. In this method the credentials can be validated and a struct variable connectionInfo can be set:
function onWSAuthenticate(String username, String password, Struct connectionInfo) {
if(username == password) {
connectionInfo.authenticated = "YES";
connectionInfo.role = "admin";
return true;
}
connectionInfo.authenticated = "NO";
return false;
}
}
As you can see this method takes an additional argument connectionInfo. On this object the key ‘authenticated’ can be set to 'Yes' to indicate that the user has been authenticated. Now the channel listener functions can check for the authentication of the user by referring to these keys. For example, the ChannelListenerComponent (mentioned above) can extend 'CFIDE.websocket.ChannelListener' and override the methods allowSubscribe, allowPublish and check whether the user is allowed to subscribe or publish:
component extends="CFIDE.websocket.ChannelListener" {
public boolean function allowSubscribe(Struct subscriberInfo) {
if(subscriberInfo.connectionInfo.authenticated == 'YES' && subscriberInfo.age > 18)
return true;
return false;
}
public boolean function allowPublish(Struct publisherInfo) {
if(publisherInfo.connectionInfo.role == 'admin' && publisherInfo.name == 'Sagar')
return true;
return false;
}
}
The methods 'allowSubscribe' and 'allowPublish' have a single parameter of type struct, containing the subscriber\publisher information. These are invoked when the user calls the subscribe or the publish method on the socket object. While calling these methods additional information such as age or name can also be included:socket.subscribe("myChannel",{age:25},myChannelHandler);
socket.publish("myChannel",”message_here”, {name:'Sagar'});
Since we have already authenticated the user and assigned a role; the connectionInfo object which is available for the methods mentioned above would be containing these values (assigned earlier in onWSAuthenticate). One can check for the same and allow\disallow the user to subscribe\publish by returning a boolean value.
The other way of authenticating users is to specify useCFAuth=true in the cfwebsocket tag. Here a user is authenticated using cflogin and cfloginuser tag and this will make the connectionInfo struct available in listener functions. Also, since the user is authenticated using cflogin you need not invoke the authenticate method on the socket object.
To be clear, is this portion in the JS there to handle a bad auth?
ReplyDeleteif(msg.code == -1) {
If so, is there a listing of what codes mean what? Is -1 ALWAYS for a failed auth, or is it for errors in general? If it is for errors in general, wouldn't you have to write code to handle auth errors versus other errors?
It is not just for failed auth. If there is any error then the code would be -1. It means that you can use it for all error handling. For example, if the channel that you are subscribing to is not specified in this.wschannels then this would result in an error and again code would be -1.
ReplyDeleteSo how do you tell then? Are there further details in the event handler?
ReplyDeleteIt's just the code and the 'msg'. I get your point that errors may result from application setting or authorization or from any other source, then all refer to the same code and msg would contain the error message. Right now the errors are not categorized.
ReplyDeleteIs the msg something specific though? So one could do a string check? It isn't "clean" as some other number, but it's still possible.
ReplyDeleteAs the application is built some checks can be added. If say the channel that you're trying to publish doesn't exist then msg would be 'Channel channel_name is not defined'. In case of authorization failure 'Access denied', for syntax error in Channel listener - 'Verify the syntax... '. Though this is not a complete list, but something can be built using string comparison.
ReplyDelete