8.3 Custom Authentication

Java Servlet Programming, 2nd Edition > 8. Security > 8.3 Custom Authentication

 
< BACKCONTINUE >

8.3 Custom Authentication

Normally, client authentication is handled by the web server. The deployment descriptor tells the server which resources are to be restricted to which roles, and the server somehow manages the user/group to role mapping.

This is often good enough. Sometimes, however, the desired security policy cannot be implemented by the server. Maybe the user list needs to be stored in a format that is not readable by the server. Or maybe you want any username to be allowed, as long as it is given with the appropriate "skeleton key" password. To handle these situations, we can use servlets. A servlet can be implemented so that it learns about users from a specially formatted file or a relational database; it can also be written to enforce any security policy you like. Such a servlet can even add, remove, or manipulate user entries something that isn't supported directly in the Servlet API, except through proprietary server extensions.

A servlet uses status codes and HTTP headers to manage its own basic authentication security policy. The servlet receives encoded user credentials in the Authorization header. If it chooses to deny those credentials, it does so by sending the SC_UNAUTHORIZED status code and a WWW-Authenticate header that describes the desired credentials. A web server normally handles these details without involving its servlets, but for a servlet to do its own authorization, it must handle these details itself, while the server is told not to restrict access to the servlet.

The Authorization header, if sent by the client, contains the client's username and password. With the basic authorization scheme, the Authorization header contains the string of username:password encoded in Base64. For example, the username of webmaster with the password try2gueSS is sent in an Authorization header with the value:

Authorization: BASIC d2VibWFzdGVyOnRyeTJndWVTUw

If a servlet needs to, it can send a WWW-Authenticate header to tell the client the authorization scheme and the realm against which users will be verified. A realm is simply a collection of user accounts and protected resources. For example, to tell the client to use basic authentication for the realm Admin, the WWW-Authenticate header is:

WWW-Authenticate: BASIC realm="Admin"

Example 8-8 shows a servlet that performs custom authorization, receiving an Authorization header and sending the SC_UNAUTHORIZED status code and WWW-Authenticate header when necessary. The servlet restricts access to its "top-secret stuff" to those users (and passwords) it recognizes in its user list. For this example, the list is kept in a simple Hashtable and its contents are hard-coded; this would, of course, be replaced with some other mechanism, such as an external relational database, for a production servlet.

To retrieve the Base64-encoded username and password, the servlet needs to use a Base64 decoder. Fortunately, there are several freely available decoders. For this servlet, we will use our own com.oreilly.servlet.Base64Decoder class, not shown here but available at http://www.servlets.com, along with com.oreilly.servlet.Base64Encoder. You can find the details of Base64 encoding in RFC 1521 at http://www.ietf.org/rfc/rfc1521.txt.[1]

[1] You could also use the sun.misc.BASE64Decoder class that accompanies the JDK. Because it is in the sun.* hierarchy means it's unsupported and subject to change, but it's likely already on your system.

Example 8-8. Security in a Servlet
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.Base64Decoder; public class CustomAuth extends HttpServlet {   Hashtable users = new Hashtable();   public void init(ServletConfig config) throws ServletException {     super.init(config);     // Names and passwords are case sensitive!     users.put("Wallace:cheese",     "allowed");     users.put("Gromit:sheepnapper", "allowed");     users.put("Penguin:evil",       "allowed");   }   public void doGet(HttpServletRequest req, HttpServletResponse res)                                throws ServletException, IOException {     res.setContentType("text/plain");     PrintWriter out = res.getWriter();     // Get Authorization header     String auth = req.getHeader("Authorization");     // Do we allow that user?     if (!allowUser(auth)) {       // Not allowed, so report he's unauthorized       res.setHeader("WWW-Authenticate", "BASIC realm=\"users\"");       res.sendError(res.SC_UNAUTHORIZED);       // Could offer to add him to the allowed user list     }     else {       // Allowed, so show him the secret stuff       out.println("Top-secret stuff");     }   }   // This method checks the user information sent in the Authorization   // header against the database of users maintained in the users Hashtable.   protected boolean allowUser(String auth) throws IOException {     if (auth == null) return false;  // no auth     if (!auth.toUpperCase().startsWith("BASIC "))       return false;  // we only do BASIC     // Get encoded user and password, comes after "BASIC "     String userpassEncoded = auth.substring(6);     // Decode it, using any base 64 decoder (we use com.oreilly.servlet)     String userpassDecoded = Base64Decoder.decode(userpassEncoded);     // Check our user list to see if that user and password are "allowed"     if ("allowed".equals(users.get(userpassDecoded)))       return true;     else       return false;   } }

Although the web server is told to grant any client access to this servlet, the servlet sends its top-secret output only to those users it recognizes. With a few modifications, it could allow any user with a trusted skeleton password. Or, like anonymous FTP, it could allow the "anonymous" username with any email address given as the password.

Custom authorization can be used for more than restricting access to a single servlet. Were we to add this logic to our ViewResource servlet, we could implement a custom access policy for an entire set of files; a URL prefix mapping rule could be created so that the secure ViewResource servlet served an entire directory structure of protected files. Were we to create a special subclass of HttpServlet and add this logic to that, we could easily restrict access to every servlet derived from that subclass. Our point is this: with custom authorization, the security policy limitations of the server do not limit the possible security policy implementations of its servlets.

8.3.1 Form-Based Custom Authorization

Servlets also have the ability to perform custom form-based authorization. By doing custom form-based login, a web application can use an elegant HTML login page, with the additional advantage that any security policy can be implemented, all in a completely portable manner. For example, several banking applications require more than a username and password for login; some require an account number, password, and PIN. Such a login can't be accomplished using FORM <auth-method> but can be accomplished with custom form-based authorization. The cost is additional complexity because the form-based login details have to be managed manually.

The steps are relatively straightforward. First, we need the login page. It can be written like any other HTML form. Example 8-9 shows a sample login.html file that generates the form shown in Figure 8-3.

Example 8-9. The login.html File
<HTML> <TITLE>Login</TITLE> <BODY> <FORM ACTION=/servlet/LoginHandler METHOD=POST> <CENTER> <TABLE BORDER=0> <TR><TD COLSPAN=2> <P ALIGN=CENTER> Welcome!<br> Please enter your Account Number,<br>  Password, and PIN to log in. </TD></TR> <TR><TD> <P ALIGN=RIGHT><B>Account:</B> </TD> <TD> <P><INPUT TYPE=TEXT NAME="account" VALUE="" SIZE=15> </TD></TR> <TR><TD> <P ALIGN=RIGHT><B>Password:</B> </TD> <TD> <P><INPUT TYPE=PASSWORD NAME="password" VALUE="" SIZE=15> </TD></TR>                                                             <TR><TD> <P ALIGN=RIGHT><B>PIN:</B> </TD> <TD> <P><INPUT TYPE=PASSWORD NAME="pin" VALUE="" SIZE=15> </TD></TR> <TR><TD COLSPAN=2> <CENTER> <INPUT TYPE=SUBMIT VALUE="  OK   "> </CENTER> </TD></TR> </TABLE> </FORM> </BODY></HTML>

Figure 8-3 displays the form.

Figure 8-3. A friendly banking login form

This form asks the client for her account number, password, and PIN, then submits the information to the LoginHandler servlet that validates the login. We'll see the code for LoginHandler soon, but first we should ask ourselves, "When is the client going to see this login page?" It's clear she can browse to this login page directly, perhaps following a link on the site's front page. But what if she tries to access a protected resource directly without first logging in? In that case, she should be redirected to this login page and, after a successful login, be redirected back to the original target. The process should work as seamlessly as having the browser pop open a window except in this case the site pops open an intermediary page.

Example 8-10 shows a servlet that implements this redirection behavior. It outputs its secret data only if the client's session object indicates she has already logged in. If she hasn't logged in, the servlet saves the request URL in her session for later use, and then redirects her to the login page for validation.

Example 8-10. A Protected Resource
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class ProtectedResource extends HttpServlet {   public void doGet(HttpServletRequest req, HttpServletResponse res)                                throws ServletException, IOException {     res.setContentType("text/plain");     PrintWriter out = res.getWriter();     // Get the session     HttpSession session = req.getSession();     // Does the session indicate this user already logged in?     Object done = session.getAttribute("logon.isDone");  // marker object     if (done == null) {       // No logon.isDone means she hasn't logged in.       // Save the request URL as the true target and redirect to the login page.       session.setAttribute("login.target",                            HttpUtils.getRequestURL(req).toString());       res.sendRedirect("/login.html");       return;     }     // If we get here, the user has logged in and can see the goods     out.println("Unpublished O'Reilly book manuscripts await you!");   } }

This servlet sees if the client has already logged in by checking her session for an object with the name logon.isDone. If such an object exists, the servlet knows that the client has already logged in and therefore allows her to see the secret goods. If it doesn't exist, the client must not have logged in, so the servlet saves the request URL under the name login.target and then redirects the client to the login page. Under custom form-based authorization, all protected resources (or the servlets that serve them) have to implement this behavior. Subclassing, or the use of a utility class, can simplify this task.

Now for the login handler. After the client enters her information on the login form, the data is posted to the LoginHandler servlet shown in Example 8-11. This servlet checks the account number, password, and PIN for validity. If the client fails the check, she is told that access is denied. If the client passes, that fact is recorded in her session object and she is immediately redirected to the original target.

Example 8-11. Handling a Login
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class LoginHandler extends HttpServlet {   public void doPost(HttpServletRequest req, HttpServletResponse res)                                 throws ServletException, IOException {     res.setContentType("text/html");     PrintWriter out = res.getWriter();     // Get the user's account number, password, and pin     String account = req.getParameter("account");     String password = req.getParameter("password");     String pin = req.getParameter("pin");     // Check the name and password for validity     if (!allowUser(account, password, pin)) {       out.println("<HTML><HEAD><TITLE>Access Denied</TITLE></HEAD>");       out.println("<BODY>Your login and password are invalid.<BR>");       out.println("You may want to <A HREF=\"/login.html\">try again</A>");       out.println("</BODY></HTML>");     }     else {       // Valid login. Make a note in the session object.       HttpSession session = req.getSession();       session.setAttribute("logon.isDone", account);  // just a marker object       // Try redirecting the client to the page she first tried to access       try {         String target = (String) session.getAttribute("login.target");         if (target != null) {           res.sendRedirect(target);           return;         }       }       catch (Exception ignored) { }       // Couldn't redirect to the target. Redirect to the site's home page.       res.sendRedirect("/");     }   }   protected boolean allowUser(String account, String password, String pin) {     return true;  // trust everyone   } }

The actual validity check in this servlet is quite simple: it assumes any user credentials are valid. That keeps things simple, so we can concentrate on how the servlet behaves when the login is successful. The servlet saves the user's account number (any old object will do) in the client's session under the name logon.isDone, as a marker that tells all protected resources this client is okay. It then redirects the client to the original target saved as login.target, seamlessly sending her where she wanted to go in the first place. If that fails for some reason, the servlet redirects the user to the site's home page.


Last updated on 3/20/2003
Java Servlet Programming, 2nd Edition, © 2001 O'Reilly

< BACKCONTINUE >


Java servlet programming
Java Servlet Programming (Java Series)
ISBN: 0596000405
EAN: 2147483647
Year: 2000
Pages: 223

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net