Apache Jakarta and Beyond: A Java Programmeramp;#039;s Introduction [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Apache Jakarta and Beyond: A Java Programmeramp;#039;s Introduction [Electronic resources] - نسخه متنی

Larne Pekowsky

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید







17.8. Certificate-Based Authentication


Users with certificates can now go to the protected "example" Web application but will still need to log in. To make the certificate also act as a login credential, a few more changes are needed. First, the login-config method must be changed as follows:


<login-config>
<auth-method>CLIENT-CERT</auth-method>
</login-config>

Next, the realm must be told to accept the user that the certificate will present. Unfortunately a major problem is encountered at this point.

In Tomcat 4 the MemoryRealm, which is a slight variation of the UserDatabaseRealm, could be configured to handle certificate-based authentication by providing the "distinguished name" of the client certificate as the username, as in


<user
username="CN=Jane Q. Websurfer, O=My Browser, ST=NY, C=US"
password="
roles="example"/>

The password is empty in this case because it is not used; the certificate itself acts as the password. In Tomcat 5, however, the equal sign (=) is no longer a valid character within an XML attribute. There is no way to circumvent this by using an escape character or other XML trick. The inescapable fact is there is no way to specify a certificate-based username in tomcat-users.xml. The JDBCRealm and others have no provision for the CLIENT-CERT login method at all.

If Tomcat were a closed source project, this would be a dead end. There would be no hope for certificate-based authentication short of writing to the company and asking them to fix the problem in their next release sometime next year. Fortunately, Tomcat is open source, so it is possible for industrious developers to fix the problem themselves. Now we'll look at one way to do this. In addition to enabling certificate-based authentication, this will also illustrate some of the joysand idiosyncrasiesof working with a large open source toolkit like Tomcat.

A logical starting point is the existing implementation of UserDatabaseRealm. The name of this class is listed in server.xml, and it is easy enough to find the Java code for this class in the Tomcat source.

Although there is documentation on how realms work, it is somewhat vague as to exactly what methods will be called under different circumstances. Therefore, the first step will be to add some print statements to see how the methods are invoked. This can be done most easily by extending UserDatabaseRealm.

Looking at the UserDatabaseRealm shows three methods that look like they may be relevant: authenticate(), getPassword(), and getPrincipal(). The first has a good deal of code, and the other two return null. The extended class will implement these three methods.

The most immediately obvious possibility would be to make these methods print their arguments and then call up to the super class, as in


public class ModifiedUserDatabaseRealm
extends UserDatabaseRealm
{
public Principal authenticate(String username,
String credentials)
{
System.out.println("authenticate() called with" +
"username = " + username +
"credentials = " + credentials);
return super.authenticate(username,credentials);
}
}

As the inner workings of this realm are explored and fixed, it is reasonable to expect that many changes will need to be made to this code, and Tomcat will need to be restarted each time to pick up the changes. This sounds like a job for BeanShell, discussed in Chapter 16. Rather than implementing the realm methods in Java, it will be much faster to make ModifiedUserDatabaseRealm load and invoke BeanShell scripts, which can be quickly changed on the fly without any recompilation or restarting. The resulting version is shown in Listing 17.1.


Listing 17.1. A realm implementation that calls BeanShell


package com.awl.toolbook.chapter17;
import java.security.Principal;
import javax.naming.Context;
import org.apache.catalina.realm.UserDatabaseRealm;
import org.apache.catalina.UserDatabase;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.ServerFactory;
import bsh.Interpreter;
public class ModifiedUserDatabaseRealm
extends UserDatabaseRealm
{
public Principal authenticate(String username,
String credentials)
{
try {
Interpreter i = new Interpreter();
i.source("realm.bsh");
i.set("parent",this);
i.set("database",database);
i.set("username",username);
i.set("credentials",credentials);
return
(Principal)
i.eval("authenticate(username,credentials)");
} catch (Throwable t) {
System.out.println("Error calling BeanShell");
t.printStackTrace(System.err);
}
return null;
}
protected String getPassword(String username) {
try {
Interpreter i = new Interpreter();
i.source("realm.bsh");
i.set("parent",this);
i.set("database",database);
i.set("username",username);
return (String)
i.eval("getPassword(username)");
} catch (Throwable t) {
System.out.println("Error calling BeanShell");
t.printStackTrace(System.err);
}
return null;
}
/**
* Return the Principal associated with the given
* user name.
*/
protected Principal getPrincipal(String username) {
try {
Interpreter i = new Interpreter();
i.source("realm.bsh");
i.set("parent",this);
i.set("database",database);
i.set("username",username);
return (Principal)
i.eval("getPrincipal(username)");:
} catch (Throwable t) {
System.out.println("Error calling BeanShell");
t.printStackTrace(System.err);
}
return null;
}
}

Each method works essentially this way: by loading realm.bsh, setting some values in the global namespace, and calling a BeanShell function. The parameters to each function are set as global variables, which is the easiest way to pass them to the BeanShell functions. The dictionary is a class variable used by the authenticate(), which may also be needed in the other two methods. The parent variable points to the superclass of the current instance of ModifiedUserDatabaseRealm, which will enable the BeanShell scripts to call back into Java.

This class can then be compiled and moved to server/classes, which is the appropriate location for classes needed by Tomcat's server functions globally and at startup. The BeanShell jar file must also be moved into server/lib.

Tomcat can now be reconfigured to use this new class as its realm:


<Realm
className=
"com.awl.toolbook.chapter17.ModifiedUserDatabaseRealm"
debug="0"
resourceName="UserDatabase"/>

Tomcat can now be started, and it comes up without any complaints or error messages, indicating that there is no problem with the new realm class. Attempting to access example/181 results in the following error in the log:


java.io.FileNotFoundException:
tomcat/realm.bsh (No such file or directory)

This is good: It means that the new realm class is trying to load the realm.bsh that has not yet been created. So the next step is to create the file, which is shown in Listing 17.2.


Listing 17.2. The BeanShell file with realm methods


import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import javax.naming.Context;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.Group;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Role;
import org.apache.catalina.ServerFactory;
import org.apache.catalina.User;
import org.apache.catalina.UserDatabase;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.util.StringManager;
setAccessibility(true);
Principal authenticate(String username,
String credentials)
{
System.out.println("authenticate() called");
System.out.println("Username = " + username);
System.out.println("Credential = " + credentials);
return parent.authenticate(username,credentials);
}
getPassword(String username) {
System.out.println("getPassword() called");
System.out.println("Username = " + username);
return (null);
}
getPrincipal(String username) {
System.out.println("getPrincipal() called");
System.out.println("Username = " + username);
return (null);
}

All the import statements were taken directly from UserDatabaseRealm. Although none of these are used yet, they may be needed as development progresses. In addition, the import of GenericPrincipal was added because it lives in the same package as UserDatabaseRealm and so did not need to be imported initially.

The call to setAccessibility(true) allows BeanShell to access private and protected members and methods. This is needed to allow the scripts to call back into all the methods it may need.

The rest of the code is straightforward; it simply prints some diagnostic information and calls back into the parent.

This new code can be tested immediately just by clicking reload on the browser. This time the error message is replaced by the following message:


getPrincipal() called
Username = EMAILADDRESS=jane@nosuchdomain.com,
CN=Jane J. Websurfer, O=My Browser, L=New York, ST=New York

This is an excellent result; it indicates that getPrincipal() is the method called and that all the information from the certificate is made available to this method. It is no wonder the default implementation does not work for client-based authentication because getPrincipal() returns null.

Now that it is clear how the realm handler is invoked, it is possible to start implementing a fix. The first task will be to extract a meaningful name from the username, and CN seems like a good choice. There are many ways this information could be extracted. One obvious choice would be to use one of the regular expression packages discussed in Chapter 13. To accomplish this, jakarta-oro-2.0.7.jar must be copied into server/lib, and the definition of getPrincipal() in the BeanShell script must be changed as follows:


import org.apache.oro.text.regex.*;
getPrincipal(String username) {
System.out.println("getPrincipal() called");
System.out.println("Username = " + username);
Perl5Compiler compiler = new Perl5Compiler();
Pattern pattern =
compiler.compile("CN=([^,]*)");
PatternMatcherInput input =
new PatternMatcherInput(username);
Perl5Matcher m = new Perl5Matcher();
if(m.contains(input,pattern)) {
MatchResult r = m.getMatch();
System.out.println("CN = " +
r.group(1));
}
return (null);
}

Note the new import statement.

When the browser's reload button is pressed, an unpleasant surprise shows up in catalina.out: the following error message:


Sourced file: inline evaluation of:
"getPrincipal(username);" :
Typed variable declaration : Class:
Perl5Compiler not found in namespace
: at Line: 43 : in file: realm.bsh : Perl5Compiler

This is unexpected; the jar file is available, and the class was properly imported. The only possible explanation is that although Tomcat can load BeanShell itself from server/lib, for some reason the BeanShell interpreter is unable to access jars in that directory. The only possible fix would be to move the jar to a directory that is more widely available to Tomcat, and indeed moving the jar to common/lib fixes the problem. On the next reload the logs report


CN = Jane J. Websurfer

We now have a simple name that can be used to authenticate the user. Tomcat can be notified of this user by adding an entry to tomcat-users.xml, just as was done for users using regular password authentication.


<user
username="Jane J.Websurfer"
password="ignore"
roles="example"/>

At this juncture it is worth pointing out that this format of the file is extremely sensitive. To an extent this is to be expected because all XML files require strict adherence to certain rules. The way Tomcat handles invalid files may be unexpected, however. If there is a problem with an entryfor example, if username were mistyped as usrnameTomcat would silently and without warning discard the entry, both from the database in memory and from the file itself! A perplexed administrator trying to find out why Jane cannot log into the system might be surprised to discover her entry had mysteriously vanished.

All that remains is to modify getPrincipal() to find this user and return an appropriate Principal object. It is not immediately clear how to do this, but once again the open source nature of Tomcat comes to the rescue. The original code for authenticate() in UserDatabaseRealm also returns a principal, using information from the dictionary object. That code can easily be extracted and used in getPrincipal(), and the result is


getPrincipal(String username) {
System.out.println("getPrincipal() called");
System.out.println("Username = " + username);
Perl5Compiler compiler = new Perl5Compiler();
Pattern pattern =
compiler.compile("CN=([^,]*)");
PatternMatcherInput input =
new PatternMatcherInput(username);
Perl5Matcher m = new Perl5Matcher();
// If there is no CN, abort
if(!m.contains(input,pattern)) {
return null;
}
MatchResult r = m.getMatch();
String name = r.group(1);
User user = database.findUser(name);
if (user == null) {
return (null);
}
ArrayList combined = new ArrayList();
Iterator roles = user.getRoles();
while (roles.hasNext()) {
Role role = (Role) roles.next();
String rolename = role.getRolename();
if (!combined.contains(rolename)) {
combined.add(rolename);
}
}
Iterator groups = user.getGroups();
while (groups.hasNext()) {
Group group = (Group) groups.next();
roles = group.getRoles();
while (roles.hasNext()) {
Role role = (Role) roles.next();
String rolename = role.getRolename();
if (!combined.contains(rolename)) {
combined.add(rolename);
}
}
}
return(new GenericPrincipal(parent,
user.getUsername(),
user.getPassword(),
combined));
}

This should do the trick, but regrettably it results in another error:


Sourced file: inline evaluation of:
"getPrincipal(username);" :
Typed variable declaration :
Class: User not found in namespace : at
Line: 58 : in file: realm .bsh : User

The User class, which is a class internal to Tomcat, cannot be found. This is a little more troubling.

The next step is to see where it is defined, and by exploring the jar files in both common/lib and server/lib, the class is discovered in common/lib/catalina.jar. It is tempting to try to fix this by copying it to common/lib, as was done with the ORO classes. Unfortunately, this breaks Tomcat to the point where it will no longer run. The other alternative is to copy all the jar files from server/lib to common/ilb. This is unquestionably an ugly solution, but it will do for the moment.

The good news is that that is the last thing that needs to be done. On the next browser reload "Jane Q. Websurfer" will successfully be granted access to the protected content, and this will happen securely, seamlessly, and without ever needing to type a password.

From here the new code could be rolled back into ModifiedUserDatabaseRealm, or even back into the original class.


/ 207