Secure Your Java and Android Clients

This topic provides information on how to add user authentication functionality to Java and Android clients. The Java and Android Client APIs use the same authentication classes and methods.

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.

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.

Note: This procedure is part of Checklist: Build Android Clients and Checklist: Build Java Clients.

To Secure Your Java and Android Clients

This section includes the following topics:

Overview of Challenge Handlers

A challenge handler is responsible for producing responses to authentication challenges from the Gateway. The challenge handler process is as follows:

  1. When an attempt to access a URI protected by the Gateway is made, the Gateway responds with an authentication request, indicating that credentials need to be provided before access to the resource is granted. The specific type of challenge is indicated in a HTTP header called "WWW-Authenticate".
  2. The authentication request and the header are converted into a ChallengeRequest (as defined in RFC 2617) and sent to a challenge handler registered in the client application for authentication challenge responses.
  3. The ChallengeResponse credentials generated by a registered challenge handler are included in a replay of the original request to the Gateway, which allows access to the resource (assuming the credentials are sufficient).

Authenticating your Java 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 implement a login handler.

Challenge Handler Class Imports

To use a challenge handler in your Java client you must add the following imports:

import com.kaazing.gateway.jms.client.demo.LoginDialogFragment.LoginDialogListener;
import com.kaazing.net.auth.BasicChallengeHandler;
import com.kaazing.net.auth.ChallengeHandler;
import com.kaazing.net.auth.LoginHandler;

Here is an example of all the JMS imports, including challenge handlers and login handlers:

import com.kaazing.gateway.jms.client.demo.LoginDialogFragment.LoginDialogListener;
import com.kaazing.gateway.jms.client.ConnectionDisconnectedException;
import com.kaazing.gateway.jms.client.JmsConnectionFactory;
import com.kaazing.net.auth.BasicChallengeHandler;
import com.kaazing.net.auth.ChallengeHandler;
import com.kaazing.net.auth.LoginHandler;
import com.kaazing.net.ws.WebSocketFactory;

Creating a Basic Challenge Handler

Clients with a single challenge handling strategy for authentication requests can set a specific challenge handler as the default using the setDefaultChallengeHandler() method in the WebSocketFactory class. For example, here is how a Challenge Handler is set on a Kaazing JMS connection:

private JmsConnectionFactory connectionFactory;
...
connectionFactory = JmsConnectionFactory.createConnectionFactory();
WebSocketFactory webSocketFactory = connectionFactory.getWebSocketFactory();
webSocketFactory.setDefaultChallengeHandler(createChallengehandler());

Each WebSocket created from the factory can have its own Challenge Handler associated with it:

wsFactory = WebSocketFactory.createWebSocketFactory();
WebSocket ws = wsFactory.createWebSocket(location);
BasicChallengeHandler challengeHandler = BasicChallengeHandler.create();
challengeHandler.setLoginHandler(loginHandler);
ws.setChallengeHandler(challengeHandler);

Note: The challenge handler API is very flexible and there are many different ways to implement challenge handlers to suit the needs of your client application. For more detailed information on challenge handlers, see the Java Client API.

Creating a Login Handler

A login handler is responsible for obtaining credentials from an arbitrary source, such as a dialog presented to the user. Login handlers can be associated with one or more challenge handlers (ChallengeHandler objects) to ensure that when a challenge handler requires credentials for a challenge response (ChallengeResponse), the work is delegated to a login handler.

To demonstrate how you can integrate a Challenge Handler and Login Handler into you application, we will use the Kaazing Android JMS tutorial app available here https://github.com/kaazing/java.client.tutorials/tree/develop/android/jms. Follow the steps in the repo to clone or download the project and build it in Android Studio. You can look at the code of the project on Github or in Android Studio to see how a Challenge Handler and Login Handler are integrated.

First, the Challenge Handler is set on the JMS connection when the connection is created, in the main program file JMSDemoActivity.java:

import com.kaazing.gateway.jms.client.demo.LoginDialogFragment.LoginDialogListener;
import com.kaazing.gateway.jms.client.ConnectionDisconnectedException;
import com.kaazing.gateway.jms.client.JmsConnectionFactory;
import com.kaazing.net.auth.BasicChallengeHandler;
import com.kaazing.net.auth.ChallengeHandler;
import com.kaazing.net.auth.LoginHandler;
import com.kaazing.net.ws.WebSocketFactory;
...
    public void onCreate(Bundle savedInstanceState) {
     ...
     if (connectionFactory == null) {
      try {
       connectionFactory = JmsConnectionFactory.createConnectionFactory();
       WebSocketFactory webSocketFactory = connectionFactory.getWebSocketFactory();
       webSocketFactory.setDefaultChallengeHandler(createChallengehandler());
      } catch (JMSException e) {
       e.printStackTrace();
       logMessage("EXCEPTION: " + e.getMessage());
      }
     }
     ...
     

Next, a LoginDialogFragment.java file is added to the project to define the popup dialog that users will see when a challenge handler is used and a login handler must be used to collect user credentials. Note the class LoginDialogFragment, as it will be called from the main program, and note that the username and password are obtained and returned via getUsername() and getPassword():

package com.kaazing.gateway.jms.client.demo;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.widget.EditText;

public class LoginDialogFragment extends DialogFragment {
	
	private String username;
	private String password;
	private LoginDialogListener listener;
	private boolean cancelled;
	
	public Dialog onCreateDialog(Bundle savedInstanceState) {
		
		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
		LayoutInflater layoutInflaor = getActivity().getLayoutInflater();
		builder.setView(layoutInflaor.inflate(R.layout.login, null)).setPositiveButton(R.string.ok_label, new DialogInterface.OnClickListener() {
			
			public void onClick(DialogInterface dialog, int which) {
				EditText usernameText = (EditText)LoginDialogFragment.this.getDialog().findViewById(R.id.username);
				EditText passwordText = (EditText)LoginDialogFragment.this.getDialog().findViewById(R.id.password);
				username = usernameText.getText().toString();
				password = passwordText.getText().toString();
				LoginDialogFragment.this.getDialog().dismiss();
				listener.onDismissed();
				
			}
		})
        .setNegativeButton(R.string.cancel_label, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
            	cancelled = true;
                LoginDialogFragment.this.getDialog().cancel();
                listener.onDismissed();
            }
        });
		return builder.create();
		
	}
	
	public String getUsername() {
		return username;
	}
	
	public String getPassword() {
		return password;
	}
	
	public boolean isCancelled() {
		return cancelled;
	}
	
	public void setListener(LoginDialogListener listener) {
		this.listener = listener;
	}
	
	public interface LoginDialogListener {
		public void onDismissed();
	}
}

Finally, in the main program in JMSDemoActivity.java, the Challenge Handler and Login Handler are set in a function named createChallengehandler(), and the popup dialog is launched, collects the user credentials, and returns them to the Gateway.

private ChallengeHandler createChallengehandler() {
  final LoginHandler loginHandler = new LoginHandler() {
    private String username;
    private char[] password;
    @Override
    public PasswordAuthentication getCredentials() {
      try {
        final Semaphore semaphore = new Semaphore(1);
        		
        // Acquire semaphore so that subsequent acquire will block until released.
        // This is used to wait until the login dialog is dismissed
        semaphore.acquire();
        final LoginDialogFragment loginDialog = new LoginDialogFragment();
        loginDialog.setListener(new LoginDialogListener() {
          public void onDismissed() {
            semaphore.release();
          }
        });
        runOnUiThread(new Runnable() {				
          public void run() {
            loginDialog.show(getSupportFragmentManager(), "Login Dialog Fragment");
            loginDialog.getFragmentManager().executePendingTransactions();
            loginDialog.getDialog().setCanceledOnTouchOutside(false);
          }
        });
            	
        // wait until the dialog is dismissed
        semaphore.acquire();
            	
        if (loginDialog.isCancelled()) {
          return null;
        }
            	
        username = loginDialog.getUsername();
        password = loginDialog.getPassword().toCharArray();
      } catch (Exception e) {
        e.printStackTrace();
      }
      return new PasswordAuthentication(username, password);
    }
  };
  BasicChallengeHandler challengeHandler = BasicChallengeHandler.create();
  challengeHandler.setLoginHandler(loginHandler);
  return challengeHandler;
}

Creating a Custom Challenge Handler

There are two methods used in ChallengeHandler:

For information about each authentication scheme type, see Configure the HTTP Challenge Scheme.

Overriding Default Challenge Handler Implementations

After you have developed your own challenge handler, you can install it for future use. For example, to install your own implementation of BasicChallengeHandler for a Java client:

  1. Add a JAR file with your BasicChallengeHandler implementation to your classpath parameter before the Kaazing Gateway Java client libraries.
  2. Ensure the JAR file contains the following file inside:
    META-INF/services/com.kaazing.gateway.client.security.BasicChallengeHander. The contents of the file should consist of a single line listing the fully-qualified name of your new implementation class (for example, fully.qualified.challenge.handler.impl.MyChallengeHandler). For more information, see the Service Loader documentation.

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 demonstrates how to stop the Gateway from issuing further challenges.

/**
* Sets up the login handler for responding to "Application Basic" or "Application Negotiate" challenges.
*/
private static int maxRetries = 2; //max retries allowed for wrong credentials
private int retry = 0;    // retry counter

private void setupLoginHandler(final Frame parentFrame, String locStr) {
  wsFactory = WebSocketFactory.createWebSocketFactory();
  int index = locStr.indexOf("://");
  @Override
  public PasswordAuthentication getCredentials() {
    try {
      if (retry++ >= maxRetries) {
        return null;    // stop authentication process if max retry reached
      }
      LoginDialog dialog = new LoginDialog(parentFrame);
      if (dialog.isCanceled()) {
        retry = 0;    // user stopped authentication, reset retry counter
        return null;  // stop authentication process
      }
      username = dialog.getUsername();
      password = dialog.getPassword();

      updateButtonsForConnected();
      log("CONNECTED");
      retry = 0;    //reset retry counter;

      // Receive messages using WebSocketMessageReader.
      final WebSocketMessageReader messageReader = webSocket.getMessageReader();
    }
  } 
  catch (Exception e1) {
    retry = 0;     //reset retry counter
    e1.printStackTrace();
    log("EXCEPTION: "+e1.getMessage());  }
...

Registering Challenge Handlers at Locations

When authentication challenges arrive for specific URI locations, the DispatchChallengeHandler is used to route challenges to the appropriate challenge handlers. This allows clients to use specific challenge handlers to handle specific types of challenges at different URI locations.

Here is an example of registering a specific location for a challenge handler:

LoginHandler someServerLoginHandler = ...
NegotiateChallengeHandler  nch = NegotiateChallengeHandler.create();
NegotiableChallengeHandler nblch = NegotiableChallengeHandler.create();
DispatchChallengeHandler   dch = DispatchChallengeHandler.create();

WebSocketFactory                wsFactory = WebSocketFactory.createWebSocketFactory();
wsFactory.setDefaultChallengeHandler(dch.register("ws://host.example.com",
        nch.register(nblch).setLoginHandler(someServerLoginHandler)
);
 // register more alternatives to negotiate here.
)

Using Wildcards to Match Sub Domains and Paths

You can use wildcards (“*”) when registering locations using locationDescription in DispatchChallengeHandler. Some examples of locationDescription values with wildcards are:

Creating Kerberos Challenge Handlers

The following examples demonstrate different implementations of Kerberos challenge handlers. When registered with the DispatchChallengeHandler, a KerberosChallengeHandler directly responds to Negotiate challenges where Kerberos-generated authentication credentials are required. In addition, you can use a KerberosChallengeHandler indirectly in conjunction with a NegotiateChallengeHandler to assist in the construction of a challenge response using object identifiers. For more information, see the Java Client API.

This abstract class captures common requirements for a number of implementation flavors for Kerberos, including Microsoft's SPNEGO implementation, and a SPNEGO Kerberos v5 GSS implementation.

To successfully use a KerberosChallengeHandler, one must know one or more Kerberos KDC service locations and optionally (if not defaulted to "HTTP/requestedURIHostname") provide the name of the specific service being requested.

For the KDC service location, one must establish either:

For the non-defaulted service name being requested, one can configure the service name using setServiceName(String).

For example, one may install negotiate and a kerberos challenge handler that work together to handle a challenge as:

import com.kaazing.net.auth.BasicChallengeHandler;
import com.kaazing.net.auth.DispatchChallengeHandler;
import com.kaazing.net.auth.KerberosChallengeHandler;
import com.kaazing.net.auth.LoginHandler;
import com.kaazing.net.auth.NegotiateChallengeHandler;
.
.
.
LoginHandler someServerLoginHandler = ...; // perhaps this pops a dialog

KerberosChallengeHandler kerberosChallengeHandler =
        KerberosChallengeHandler.create()
                .setDefaultLocation(URI.create("ws://kb.hostname.com/kerberos5"))
                .setRealmLocation("ATHENA.MIT.EDU", URI.create("ws://athena.hostname.com/kerberos5"))
                .setServiceName("HTTP/servergw.hostname.com")
                        .setLoginHandler(someServerLoginHandler)

NegotiateChallengeHandler negotiateChallengeHandler = NegotiateChallengeHandler.create()
        .register(kerberosChallengeHandler);

WebSocketFactory wsFactory = WebSocketFactory.createWebSocketFactory();
wsFactory.setDefaultChallengeHandler(WebSocketDemoChallengeHandler.create()
        .register("ws://gateway.kaazing.wan:8001/echo", negotiateChallengeHandler)
        .register("ws://gateway.kaazing.wan:8001/echo/*", negotiateChallengeHandler));

At this point, any user attempting to access servergw.hostname.com:8000/echo will be challenged using a KerberosChallengeHandler instance. If the user enters credentials with the ATHENA.MIT.EDU realm the realm-specific athena.hostname.com KDC will be used to ask for Kerberos credentials for the challenge response. If the user enters credentials with any other realm the kb.hostname.com KDC will be used to ask for Kerberos credentials. All requests to either KDC will be for the service name HTTP/servergw.hostname.com (indicating access to that HTTP server is the service for which Kerberos credentials are being requested).