Secure Your WebSocket JavaScript Client
Before you add security to your clients, follow the steps in Checklist: Secure Network Traffic with the Gateway and Checklist: Configure Authentication and Authorization to set up security on Kaazing Gateway for your client. The authentication and authorization methods configured on the Gateway influence your client security implementation. In this procedure, we provide an example of the most common implementation.
Before You Begin
This procedure is part of Checklist: Build JavaScript Clients Using Kaazing Gateway:
- Interact with Kaazing Gateway Using the WebSocket API
- Interact with Kaazing Gateway Using the EventSource API
- Migrate JavaScript Applications to Kaazing Gateway 4.x
- Secure Your JavaScript Client
- Display Logs for the JavaScript Client
To Secure Your WebSocket JavaScript Client
This procedure contains the following topics:
- Creating a Basic Challenge Handler
- Creating a Custom Challenge Handler
- Managing Log In Attempts
- Registering Challenge Handlers at Locations
- Using Wildcards to Match Sub-domains and Paths
- Negotiating a Challenge
Authenticating your client involves implementing a challenge handler to respond to authentication challenges from the Gateway. If your challenge handler is responsible for obtaining user credentials, then you will also need to implement a login handler.
Creating a Basic Challenge Handler
A challenge handler is a constructor used in an application to respond to authentication challenges from the Gateway when the application attempts to access a protected resource. Each of the resources protected by the Gateway is configured with a different authentication scheme (for example, Basic, Application Basic, Application Negotiate, or Application Token), and your application requires a challenge handler for each of the schemes that it will encounter or a single challenge handler that will respond to all challenges. Also, you can add a dispatch challenge handler to route challenges to specific challenge handlers according to the URI of the requested resource.
For information about each authentication scheme type, see Configure the HTTP Challenge Scheme.
Clients with a single challenge handling strategy for all 401 challenges can simply set a specific challenge handler as the default using webSocketFactory.setChallengeHandler()
. The following is an example of how to implement a single challenge handler for all challenges:
function setup(webSocketFactory) { // Configure a Basic Challenge Handler var basicHandler = new BasicChallengeHandler(); basicHandler.loginHandler = function(callback) { // Create static credentials for test var credentials = new PasswordAuthentication("joe", "welcome"); callback(credentials); } webSocketFactory.setChallengeHandler(basicHandler); }
The preceding example uses static credentials, but you will want to create a login handler to obtain individual user credentials. Here is an example using a popup login dialog to respond to login challenges and obtain user credentials:
function setupSSO(webSocketFactory) { /* Respond to authentication challenges with popup login dialog */ var basicHandler = new BasicChallengeHandler(); basicHandler.loginHandler = function(callback) { popupLoginDialog(callback); } webSocketFactory.setChallengeHandler(basicHandler); } function popupLoginDialog(callback) { //popup dialog to obtain credentials var popup = document.getElementById("logindiv"); popup.style.display = "block"; var login = document.getElementById("login"); var cancel = document.getElementById("cancel"); //"OK" button was clicked, invoke callback function with credentials for login login.onclick = function() { var username = document.getElementById("username"); var password = document.getElementById("password"); var credentials = new PasswordAuthentication(username.value, password.value); //clear user input username.value = ""; password.value = ""; //hide popup popup.style.display = "none"; callback(credentials); } //"Cancel" button has been clicked, invoke callback function with null argument to cancel login cancel.onclick = function() { var username = document.getElementById("username"); var password = document.getElementById("password"); //clear user input username.value = ""; password.value = ""; //hide popup popup.style.display = "none"; callback(null); } }
This example is taken from the out of the box JavaScript WebSocket Demo at http://localhost:8001/demo/. The code for the demo can be viewed in GATEWAY_HOME/demo/javascript/. For more information on challenge handlers and how to configure location-specific challenge handling strategies, see the JavaScript Client API.
To have the out of the box JavaScript WebSocket demo prompt you for authentication, open the Gateway configuration file (GATEWAY_HOME/conf/gateway-config.xml), remove the HTML comments surrounding the <authorization-constraint>
child of the <echo>
service, restart the Gateway, and then try the demo. You will be prompted for user credentials.
This BasicChallengeHandler
can be instantiated using a new BasicChallengeHandler()
. After instantiating BasicChallengeHandler
, the loginHandler
function can be implemented to handle the authentication challenge. By default, loginHandler
will send an empty PasswordAuthentication
.
var basicHandler = new BasicChallengeHandler(); basicHandler.loginHandler = function(callback) { callback(new PasswordAuthentication("global", "credentials")); }; webSocketFactory.setChallengeHandler(basicHandler);
Creating a Custom Challenge Handler
The ChallengeHandler
object contains two methods, canHandle() and handle(). You create a custom challenge handler by using these methods to determine whether a challenge handler can handle the authentication scheme in the 401 response, and then passing the request to the challengeRequest object.
var myChallengeHander = function() { this.canHandle = function(challengeRequest) { // Return true if challengeRequest.authenticationScheme matches your scheme. return challengeRequest != null && "token" == challengeRequest.authenticationScheme.trim().toLowerCase(); } this.handle = function(challengeRequest, callback) { var challengeResponse = null; if (challengeRequest.location != null) { var token = getToken(); if (token != null) { // Set the token to challengeResponse challengeResponse = new ChallengeResponse("Token "+token, null); } } // Invoke callback function with challenge response callback(challengeResponse); } }
The first time the user visits the web page containing your challenge handler, the user will not have the token. Your application can obtain the token using whatever method best suits your users and authentication process. For example, you could prompt the user for credentials or use a third-party token provider. Once the token is obtained, the token can be used for all subsequent visits to the page.
Once this function has determined that it can handle the challenge, it calls ChallengeRequest and a callback to handle the challenge. The ChallengeRequest is an immutable object representing the challenge presented by the server that contains a constructor from the protected URI location triggering the challenge, and an entire server-provided 'WWW-Authenticate:' string.
Once you have created a custom challenge handler, you can set it as the default challenge handler to be used for all HTTP requests.
webSocketFactory.setChallengeHandler = function(challengeHandler) { if (challengeHandler == null) { throw new Error("challengeHandler not defined"); } webSocketFactory.setChallengeHandler(new myChallengeHander()); }
Managing Log In Attempts
When it is not possible for the Kaazing Gateway client to create a challenge response, the client must return null
to the Gateway to stop the Gateway from continuing to issue authentication challenges.
The following example is modified code taken from the JavaScript demo (GATEWAY_HOME/demo/javascript/) and demonstrates how to stop the Gateway from issuing further challenges.
var maxRetries = 2; var retry = 0; function setupSSO(factory) { /* Respond to authentication challenges with popup login dialog */ var basicHandler = new BasicChallengeHandler(); basicHandler.loginHandler = function(callback) { if (retry++ >= maxRetries) { callback(null); // abort authentication process if reaches max retries } else { popupLoginDialog(callback); } } factory.setChallengeHandler(basicHandler); } ...
Registering Challenge Handlers at Locations
Client applications with location-specific challenge handling strategies can register a DispatchChallengeHandler object, on which location-specific ChallengeHandler objects are then registered. The result is that whenever a request matching one of the specific locations encounters a 401 challenge from the server, the corresponding ChallengeHandler object is invoked to handle the challenge. For example:
var basicHandler1 = new BasicChallengeHandler(); basicHandler1.loginHandler = function(callback) { popupLoginDialog(callback); }; var basicHandler2 = new BasicChallengeHandler(); basicHandler2.loginHandler = function(callback) { callback(new PasswordAuthentication("username", "password")) }; var dispatchHandler = new DispathChallengeHandler(); dispatchHandler.register("ws://myserver.example.com", basicHandler1); dispatchHandler.register("ws://anotherserver.example.com", basicHandler2); webSocketFactory.setChallengeHandler(dispatchHandler); // Enable the ChallengeHandler by using this webSocketFactory to create a new WebSocket object var websocket = webSocketFactory.createWebSocket("ws://myserver.example.com");
Using Wildcards to Match Sub-domains and Paths
You can use wildcards (“*”) when registering locations using DispatchChallengeHandler. Some examples of locationDescription values with wildcards are:
- */ matches all requests to any host on port 80 (default port), with no user information or path specified.
- *.hostname.com:8000 matches all requests to port 8000 on any sub-domain of hostname.com, but not hostname.com itself.
- server.hostname.com:*/* matches all requests to a particular server on any port on any path but not the empty path.
Negotiating a Challenge
A Negotiate challenge handler handles initial empty "Negotiate" challenges from the Gateway. It uses other candidate challenge handlers to assemble an initial context token to send to the Gateway, and is responsible for creating a challenge response that can delegate to the appropriate candidate.
In addition, you can register more specific NegotiableChallengeHandler objects with this initial NegotiateChallengeHandler to handle initial Negotiate challenges and subsequent challenges associated with specific Negotiation mechanism types and object identifiers.
Use DispatchChallengeHandler to register a NegotiateChallengeHandler at a specific location. The NegotiateChallengeHandler has a NegotiableChallengeHandler instance registered as one of the potential negotiable alternative challenge handlers.
var negotiableHandler = new NegotiableChallengeHandler(); var negotiableHandler.loginHandler = function(callback) {...}; var negotiateHandler = new NegotiateChallengeHandler(); negotiateHandler.register(negotiableHandler); webSocketFactory.setChallengeHandler(negotiateHandler);
Next Step
You have completed the JavaScript client examples.