Java & NT Authentication

Elisabeth builds a JNI wrapper that lets the Java Authentication and Authorization Service and Windows NT authenticate specific users.


February 01, 2001
URL:http://www.drdobbs.com/jvm/java-nt-authentication/184404500

Feb01: Java Q&A

Elisabeth is a graduate student in computer science at the University of Virginia. She can be contacted at [email protected].


Many Internet sites require usernames and passwords for authentication. The site must store a username/ password pair for each user of the site, and users must remember one for each secure site they visit. There's an easier solution with intranet sites, however. Since many organizations run on a Windows NT domain, users must remember an NT username/ password for their workstations. Because only internal users can see intranet sites, there is a ready-made database for authentication on the network.

This is straightforward in languages such as C++ or Visual Basic, which are platform dependent with built-in interfaces to Windows API functions. With Java, however, it is more difficult. Because of Java's platform independence, it makes no sense to include interfaces to Windows functions. Microsoft does this with its version of Java, but to take advantage of it, the code must run on Microsoft's VM. Sun has created the Java Authentication and Authorization Service (JAAS), a standard extension to Java 1.3. Unfortunately, so far the NT module for the JAAS gets the NT permissions of users who are logged on to the machine that calls it, rather than authenticating specific users.

Thus, the only flexible way to authenticate NT users with Java (without getting down to sockets) is to write a wrapper for the Windows method that performs the authentication, and call it from the Java program. In this article, I'll show how to write just such a wrapper and call it using Java Native Interface (JNI). I'll also call that wrapper from another (possibly nonNT) machine using Remote Method Invocation (RMI) and create services, so the object and the RMI registry will run automatically; see Figure 1.

These techniques may also be of interest to anyone trying to use a Windows Foundation Class (WFC) function, in that I'll describe how to write the code to interface between Java and C++, which can be modified easily to make any of those functions available to Java programmers.

The LogonUserA Method

To log on to an NT domain, Windows NT workstations use the LogonUserA method found in Advapi32.dll. I use this method to authenticate the intranet site in the same way NT performs network authentication. The method declaration looks like this:

BOOL LogonUserA(LPTSTR lpszUsername, LPTSTR lpszDomain, LPTSTR lpszPassword, DWORD dwLogonType, DWORD dwLogonProvider, PHANDLE phToken);

The function returns True if the authentication were successful; otherwise, False. All of the type definitions for the LogonUserA declaration can be found in windows.h. Listing One shows the arguments to the function. A note about permissions: LogonUserA requires a special NT permission, SE_TCB_NAME. This is an operating-system privilege, so you must give OS privileges to any process that calls the method. There are two ways to do this: Make your class into a service, or run it under a user who has operating-system privileges. To give a user OS privileges, first make him a local administrator, then open musrmgr, go to Policies|User Rights, check Show Advanced User Rights, choose Act As Part Of The Operating System from the pull-down menu, and add the user under whom the process will run. This method obviously raises some security concerns and should be used with care.

Calling LogonUserA with JNI

All native code presented here is written in C++. However, you can also implement the techniques in other languages, including C and Visual Basic.

1. Write a Java class that calls the native method. You can give the method any name you want in your class because you will call the Windows function from a C or C++ wrapper, not from Java directly. Declare the method in the class, but that's all — do not define it, just put a semicolon at the end of the declaration. The native keyword will tell the compiler that no definition is needed. My method declaration looked like this:

private static native boolean logonUserA(String userName, String userPass);

2. Compile your .java file, then compile the C++ header file from your .class file. This gives you the declaration for the method that you must implement in the wrapper, telling the C++ code to make it available for export to JNI. This declaration is slightly different from the normal declaration to export a function in a DLL using only C++, which is why you must write a C++ wrapper class. To create the header file, use the javah utility, which can be found in /jdk1.x/bin along with java.exe. Because JDK 1.0 used a different API for native-method interfacing, you must also specify -jni, like this: javah -jni JavaNTLogon. This gives you the file javantlogon.h. If your class is in a package, specify the package when you run javah, and it appears in the name of the header file my_package_javantlogon.h. So, it's easiest if you put your class in the right package to begin with. (If you do change packages, run javah again.) Also, be careful how long the package, class, and method names are — if they are too long, they will be truncated when they are exported, and you will get an error.

Notice that in the method declaration in the header file there are two parameters that you didn't declare. These are present in any function called through JNI, and they are automatically passed, so you don't have to worry about them. The first is an environment interface pointer, which gives access to certain methods for communicating between the two languages (changing types, throwing exceptions, and so on). The second is a jclass pointer, which is the C++ equivalent of the this pointer for the calling Java object.

3. Write the C++ wrapper for the DLL. This class should include the header file output by javah, jni.h, and windows.h. It should also implement the method declared in the header file output by javah. This is where you call the Windows method you're trying to access. Listing Two is the code for this method.

First, use a typedef to tell the compiler what the declaration of the WFC method looks like:

typedef BOOL (CALLBACK* LPFNDLLFUNC1) (LPTSTR, LPTSTR, LPTSTR, DWORD, DWORD, PHANDLE);

Then call LoadLibrary on the DLL the function is in HINSTANCE hDLL = LoadLibrary("Advapi32.dll");. Check to see if hDLL is nonNULL (whether the library was loaded). If it is, get the export address of the function in the DLL: LPFNDLLFUNC1 f1 = (LPFNDLLFUNC1)GetProcAddress(hDLL, "LogonUserA");.

Again, ensure this is nonNULL, then use the pointer to the procedure address just like a function, and call LogonUserA as if it were a function in your code:

ReturnVal = f1(lpszUsername, lpszDomain, lpszPassword, dwLogonType, dwLogonProvider, phToken);

RMI, LogonUserA, and Other Machines

RMI is a way to use your machine to call a method on a remote machine (what a surprise). It has two sides — client and server. Your Java class will register an instance of itself with the server, and the client will ask the server to reference that class's methods; see Figure 1.

1. Write an interface to declare remotely accessible methods. This interface should extend java.rmi.Remote, and must declare any methods you want your calling class to be able to see. In this case, the only method needed is logonUser(String, String). My interface is called LogonUser, so when I get an object from the RMI registry it is of type LogonUser.

2. Implement the remotely accessible methods in your Java class. That is, make logonUser() call the native method logonUserA() — the one you created earlier.

3. Create the stub and skeleton for your Java class. First run javac on the .java source, then run rmic.exe on the .class file. (rmic.exe is found in the bin folder of the JDK, along with java.exe, javac.exe, and the like.) Copy the stub to the client program's machine and set the machine's classpath so that any applications that call your class can access the stub.

4. Ensure that the RMI registry is running. If your object is the only one bound to the registry, you can create the registry in that code via java.rmi.registry.LocateRegistry.createRegistry(). Otherwise, you will probably want to create the registry via rmiregistry.exe, which is found in the same place as rmic.exe. The registry is only active as long as rmiregistry.exe is running.

5. Register an instance of the interface with the RMI registry. I put the following code into a main() method of my class, so that I would be able to run my class as a service. You do not have to put it in the same class, but you must have some way to register your class.

First, create an object of the type of your interface. You do this by calling a constructor for your class LogonUser user = new JavaNTLogon();.

Then you will bind this instance to the RMI registry via java.rmi.Naming.bind() or java.rmi.Naming.rebind():

java.rmi.Naming.bind("http://localhost:1099/LogonUser", user);

Registering your object with the RMI registry gives the registry a handle to the instance of the object you created. Thus, it is valid only while the process that bound it is running.

6. Write the client application for the remote object. First, copy the stub for the remote object to the client machine, and make sure it is in the classpath. Then, create a Java method that will call your object. Listing Three presents this method.

Services, the RMI Registry, and Your Class

Srvany.exe is a utility included in the Windows NT Resource Kit. It can be registered with NT as a service and used to invoke another executable to create a service out of that executable. This way, you avoid doing the extra work of writing your own service.

Srvany works by calling any specified executable, then reporting that it is running as a service. Unfortunately, it has no way of knowing whether the executable was started successfully. Also, if the service is stopped, the executable's process is simply terminated. There is no way to cleanup gracefully. If this is a problem, you can create your own service to keep the registry object bound. There are several tutorials for writing services available on the Web, and Microsoft's Windows API documentation is helpful in writing them. To use Srvany, you first register SRVANY.EXE as a service. This is done by using the instsrv utility, also in the NT Resource Kit. The general syntax is instsrv [servicename] [path]\SRVANY.EXE, where servicename is whatever you choose to name your service, and path is the path where SRVANY .EXE is located. You need to do this for each of the services.

>instsrv rmiregistry c:\NTRESKIT\SRVANY.EXE
>instsrv LogonUser c:\NTRESKIT\SRVANY.EXE

Next, open the system registry using regedt32 .exe. Go to HKEY_LOCAL_MACHINE| SYSTEM|CurrentControlSet|Services. Here you will find a list of all the services currently installed on your machine. Go to the first service you just installed, rmiregistry, and create a Parameters key. In this key, create an Application value with type REG_SZ where the string for the value is [path]\rmiregistry.exe. Do the same for LogonUser, using [path]\java.exe. Then, also in the LogonUser Parameters key, add an AppParameters value of type REG_SZ where the string is the rest of what you would type on the command line; that is, classpath and qualified class name.

Next, you may want to set LogonUser to depend on rmiregistry so that when LogonUser is run, there will be a registry existing with which it can instantiate itself. To do this, use another NT Resource Kit utility, SC.EXE. At a command prompt, simply type >sc config LogonUser depend= rmiregistry

Finally, configure your new services in the Control Panel|Services applet the same way you would configure any other service. If you want it to run whenever the machine is restarted, set Startup to automatic. You will probably want to leave it under the system account; otherwise, ensure that the account you choose has OS privileges.

References

Further information is available on the Web. The NT LogonUserA method documentation is at http://msdn.microsoft.com/library/psdk/winbase/accclsrv_9cfm.htm. Sun has tutorials on JNI and RMI. For JNI, see http://java.sun.com/docs/books/tutorial/native1.1/index.html. For RMI, go to http://java.sun.com/docs/books/tutorial/rmi/index.html.

DDJ

Listing One

LPTSTR lpszUsername // string that specifies the user name
LPTSTR lpszDomain   // string that specifies the domain or server
LPTSTR lpszPassword // string that specifies the password
DWORD dwLogonType   // specifies type of logon operation
    Values include:
    LOGON32_LOGON_INTERACTIVE   //returns impersonation token with user's 
             // credentials. the token can then be used to run other processes
             //as that user.
    LOGON32_LOGON_NETWORK   //fastest, but does not return credentials       
DWORD dwLogonProvider   // specifies the logon provider
   Values include:
    LOGON32_PROVIDER_DEFAULT
    LOGON32_PROVIDER_WINNT40    
PHANDLE phToken         //pointer variable to receive impersonation token 
handle. Remember to allocate space for this--LogonUserA does not it for you.

Back to Article

Listing Two

// You will want to check to ensure you have deallocated memory through 
// every error path. I have left some of this out for readability.

#include "logon_JavaNTLogon.h"  // Header file output by the RMI registry
#include <windows.h>
#include <iostream.h>
#include <string.h>

//tells the compiler the function signature to expect.
typedef BOOL (CALLBACK* LPFNDLLFUNC1)(LPTSTR, LPTSTR, LPTSTR, DWORD, 
                                                         DWORD, PHANDLE);
JNIEXPORT jboolean JNICALL Java_logon_JavaNTLogon_logonUserA
    (JNIEnv * env, jclass obj, jstring userName, jstring userPass)
{
  //declare all these variables we need
  jboolean successful=0;

  HINSTANCE hDLL;         // Handle to DLL
  LPFNDLLFUNC1 f1;        // Function pointer
  BOOL ReturnVal;
  LPTSTR lpszUsername = new char[12];  
  LPTSTR lpszDomain = "BFUSA";         
  LPTSTR lpszPassword = new char[30];  
  DWORD dwLogonType = LOGON32_LOGON_INTERACTIVE; 
  DWORD dwLogonProvider = LOGON32_PROVIDER_DEFAULT;      
  PHANDLE phToken = (PHANDLE)malloc(sizeof(HANDLE)); 
                                                                 
  //convert the java strings to UTF-8 strings so 
  // the dll can deal with them
  const char * Username = env->GetStringUTFChars(userName, 0);
  const char * Userpass = env->GetStringUTFChars(userPass, 0);

  //copy them into non-constant arrays for the function call
  strcpy(lpszUsername, Username);
  strcpy(lpszPassword, Userpass);

hDLL = LoadLibrary("Advapi32.dll");
  if (hDLL != NULL)
  {
   f1 = (LPFNDLLFUNC1)GetProcAddress(hDLL, "LogonUserA");
   if (!f1)
   {
        // handle the error
    jclass newExcCls = env->FindClass("java/lang/Exception");
    if (newExcCls == 0) { 
          return 0;
       }
        env->ThrowNew(newExcCls, 
             "couldn't find the Windows LogonUserA function");
   }
   else
   {  // try to call the function
      ReturnVal = f1(lpszUsername, lpszDomain, lpszPassword, dwLogonType, 
         dwLogonProvider, phToken);
      successful = (jboolean)ReturnVal;
   }
  }
  else
  {
    jclass newExcCls = env->FindClass("java/lang/Exception");
       if (newExcCls == 0)
       { 
          return 0;
       }
       env->ThrowNew(newExcCls, "couldn't load Advapi32.dll in native code");
  }
  FreeLibrary(hDLL);
  free(lpszUsername);
  free(lpszPassword);
  free(phToken);
  env->ReleaseStringUTFChars(userName, Username);
  env->ReleaseStringUTFChars(userPass, Userpass);
  return successful;
}

Back to Article

Listing Three

public static boolean logonUser(String userName, String password) 
throws RemoteException
{
    boolean isValid = false;
    // There must be a security manager for certain classes to be
    // served through RMI.  If you are getting security exceptions, you
    // may have to define your own security manager.
    if (System.getSecurityManager() == null) {
        System.setSecurityManager(new java.rmi.RMISecurityManager());
    }
    try {
        // Specify the name of the server the remote logon object is on,
        // the port that the remote RMI registry is listening on, and the
        // name of the remote object we want to access.  Format is just
        // like a URL.  1099 is the default port for RMI.
        String remoteServer = "//myServer:1099/LogonUser";

        // Get a handle on the remote logon object by doing an RMI lookup.
        LogonUser user = (LogonUser) java.rmi.Naming.lookup(remoteServer);

        // Call the NT authentication method on the remote object. True means
        // the username and password was successfully authenticated, False
        // means it wasn't.
        isValid = user.validateUser(userName, password);
       
    } catch (Exception e) {
        e.printStackTrace();
    }
    return isValid;
}

Back to Article

Feb01: Java Q&A

Figure 1: Execution sequence. 1. The LogonUser service calls the Java class that handles user authentication requests. 2. The Java code registers an instance of itself with the RMI registry, so any machine can try to log a user onto a site using the network. 3. A client application needs to use the LogonUser object. 4. The registry returns the object. 5. Now, the client can conceptually talk directly to the Java code on the server. It sends a username and password. 6. The Java code, through JNI, calls platform-dependent C++ code to authenticate the user. 7. The C++ code calls the LogonUserA Windows API function in Advapi32.dll. 8. LogonUserA returns True or False to the C++ code. 9. The C++ code returns True or False to the Java code. 10. The Java code returns True or False to the client.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.