Secure Your Java and Android Clients
This topic provides information on how to add user authentication functionality to Kaazing Java JMS and Android JMS clients. The Kaazing Java JMS and Android JMS 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.
Before You Begin
This procedure is part of Checklist: Build Java JMS Clients and Checklist: Build Android JMS Clients.
To Secure Your Java and Android Clients
This section includes the following topics:
- Overview of Challenge Handlers
- Challenge Handler Class Imports
- Creating a Basic Challenge Handler
- Creating a Custom Challenge Handler
- Overriding Default Challenge Handler Implementations
- Managing Log In Attempts
- Authentication and Connections
- Registering Challenge Handlers at Locations
- Using Wildcards to Match Sub Domains and Paths
- Creating Kerberos Challenge Handlers
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:
- 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".
- 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.
- 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.net.auth.BasicChallengeHandler; import com.kaazing.net.auth.ChallengeHandler; import com.kaazing.net.auth.LoginHandler; import com.kaazing.net.ws.WebSocketFactory;
Here is an example of all the WebSocket imports, including challenge handlers and kerberos challenge handlers:
import com.kaazing.gateway.jms.client.demo.LoginDialogFragment.LoginDialogListener; // dialog for user credentials 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:
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:
connectionFactory = JmsConnectionFactory.createConnectionFactory(); WebSocketFactory webSocketFactory = connectionFactory.getWebSocketFactory(); BasicChallengeHandler challengeHandler = BasicChallengeHandler.create(); challengeHandler.setLoginHandler(loginHandler); webSocketFactory.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 a Challenge Handler and Login Handler, let's use the Kaazing Android JMS Tutorial app available on Github at https://github.com/kaazing/java.client.tutorials/tree/develop/android/jms.
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:
canHandle(ChallengeRequest challengeRequest)
determines if the challenge handler can handle the authentication scheme required by the Gateway (for example, Basic, Application Basic, Negotiate, Application Negotiate, or Application Token). The method takes a ChallengeRequest object containing a challenge and returns true if the challenge handler has the potential to respond meaningfully to the challenge. If this method determines that the challenge handler can handle the authentication scheme, it returns true and thehandle()
method is used. If this method returns false, the ChallengeHandler class (that contains all of the registered individual ChallengeHandler objects) continues looking for a ChallengeHandler to handle the request.handle(ChallengeRequest challengeRequest)
handles the authentication challenge by returning a challenge response. Typically, the challenge response invokes a login handler to collect user credentials and transforms that information into a ChallengeResponse object. The ChallengeResponse sends the credentials to the Gateway in an Authorization header and notifies the Gateway on what challenge handler to use for future requests. Ifhandle()
cannot create a challenge response, it returnsnull
.
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:
- Add a JAR file with your
BasicChallengeHandler
implementation to your classpath parameter before the Kaazing Gateway Java client libraries. - 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()); } ...
Authentication and Connections
Both WebSocketFactory
and JMSConnectionFactory
are used when adding a challenge handler to a Java or Android client's JMS connection to the Gateway. In the following code example, the challenge handler is initiated during the connect event for the JMS connection (lines 27-29, 45-52):
... import com.kaazing.net.auth.BasicChallengeHandler; import com.kaazing.net.auth.ChallengeHandler; import com.kaazing.net.auth.LoginHandler; ... public class JmsPanel extends javax.swing.JPanel implements ActionListener, MessageListener, ExceptionListener { ... private ChallengeHandler createChallengeHandler(String location) { final LoginHandler loginHandler = new LoginHandler() { private String username; private char[] password; @Override public PasswordAuthentication getCredentials() { try { LoginDialog dialog = new LoginDialog(Frame.getFrames()[0]); if (dialog.isCanceled()) { return null; } username = dialog.getUsername(); password = dialog.getPassword(); } catch (Exception e) { e.printStackTrace(); } return new PasswordAuthentication(username, password); } }; BasicChallengeHandler challengeHandler = BasicChallengeHandler.create(); challengeHandler.setLoginHandler(loginHandler); return challengeHandler; } ... public void actionPerformed(ActionEvent arg0) { try { if (arg0.getSource() == connect) { final ExceptionListener applet = this; Thread connectThread = new Thread() { @Override public void run() { try { String url = location.getText(); logMessage("CONNECT: " + url); if (connectionFactory instanceof JmsConnectionFactory) { JmsConnectionFactory stompConnectionFactory = (JmsConnectionFactory)connectionFactory; // initialize the login handler for the target location ChallengeHandler challengeHandler = createChallengeHandler(url); stompConnectionFactory.setGatewayLocation(new URI(url)); WebSocketFactory webSocketFactory = stompConnectionFactory.getWebSocketFactory(); webSocketFactory.setDefaultChallengeHandler(challengeHandler); webSocketFactory.setDefaultRedirectPolicy(HttpRedirectPolicy.SAME_DOMAIN); } ...
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:
- */ 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.
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:
- a default Kerberos KDC service location, using
setDefaultLocation(java.net.URI)
, or - a mapping from a Kerberos Realm to at least one Kerberos KDC service location using
setRealmLocation(String, java.net.URI)
.
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).