KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > dspace > eperson > X509Authentication


1 /*
2  * X509Authentication.java
3  *
4  * Version: $Revision: 1.1 $
5  *
6  * Date: $Date: 2005/10/17 03:35:45 $
7  *
8  * Copyright (c) 2002-2005, Hewlett-Packard Company and Massachusetts
9  * Institute of Technology. All rights reserved.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions are
13  * met:
14  *
15  * - Redistributions of source code must retain the above copyright
16  * notice, this list of conditions and the following disclaimer.
17  *
18  * - Redistributions in binary form must reproduce the above copyright
19  * notice, this list of conditions and the following disclaimer in the
20  * documentation and/or other materials provided with the distribution.
21  *
22  * - Neither the name of the Hewlett-Packard Company nor the name of the
23  * Massachusetts Institute of Technology nor the names of their
24  * contributors may be used to endorse or promote products derived from
25  * this software without specific prior written permission.
26  *
27  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31  * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
32  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
33  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
34  * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
35  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
36  * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
37  * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
38  * DAMAGE.
39  */

40 package org.dspace.eperson;
41
42 import javax.servlet.ServletException JavaDoc;
43 import javax.servlet.http.HttpServletRequest JavaDoc;
44 import javax.servlet.http.HttpServletResponse JavaDoc;
45 import javax.servlet.jsp.PageContext JavaDoc;
46 import javax.servlet.jsp.jstl.fmt.LocaleSupport;
47 import java.sql.SQLException JavaDoc;
48 import java.util.ArrayList JavaDoc;
49 import java.util.Enumeration JavaDoc;
50 import java.io.BufferedInputStream JavaDoc;
51 import java.io.FileInputStream JavaDoc;
52 import java.io.IOException JavaDoc;
53 import java.io.InputStream JavaDoc;
54 import java.security.InvalidKeyException JavaDoc;
55 import java.security.NoSuchAlgorithmException JavaDoc;
56 import java.security.NoSuchProviderException JavaDoc;
57 import java.security.Principal JavaDoc;
58 import java.security.PublicKey JavaDoc;
59 import java.security.cert.Certificate JavaDoc;
60 import java.security.KeyStore JavaDoc;
61 import java.security.SignatureException JavaDoc;
62 import java.security.GeneralSecurityException JavaDoc;
63 import java.security.cert.CertificateException JavaDoc;
64 import java.security.cert.CertificateExpiredException JavaDoc;
65 import java.security.cert.CertificateFactory JavaDoc;
66 import java.security.cert.CertificateNotYetValidException JavaDoc;
67 import java.security.cert.X509Certificate JavaDoc;
68 import java.util.StringTokenizer JavaDoc;
69
70 import org.apache.log4j.Logger;
71 import org.dspace.core.LogManager;
72 import org.dspace.core.ConfigurationManager;
73 import org.dspace.core.Context;
74 import org.dspace.eperson.EPerson;
75 import org.dspace.eperson.AuthenticationMethod;
76 import org.dspace.authorize.AuthorizeException;
77
78 /**
79  * Implicit authentication method that gets credentials from the X.509
80  * client certificate supplied by the HTTPS client when connecting to
81  * this server. The email address in that certificate is taken as the
82  * authenticated user name with no further checking, so be sure
83  * your HTTP server (e.g. Tomcat) is configured correctly to accept only
84  * client certificates it can validate.
85  * <p>
86  * See the <code>AuthenticationMethod</code> interface for more details.
87  * <p>
88  * <b>Configuration:</b><pre>
89  * authentication.x509.keystore.path = <em>path to Java keystore file</em>
90  * authentication.x509.keystore.password = <em>password to access the keystore</em>
91  * authentication.x509.ca.cert = <em>path to certificate file for CA whose client certs to accept.</em>
92  * authentication.x509.autoregister = <em>"true" if E-Person is created automatically for unknown new users.</em>
93  * </pre>
94  * Only one of the "<code>keystore.path</code>" or "<code>ca.cert</code>"
95  * options is required. If you supply a keystore, then all of the "trusted"
96  * certificates in the keystore represent CAs whose client certificates will
97  * be accepted. The <code>ca.cert</code> option only allows a single CA to be named.
98  * <p>
99  * You can configure <em>both</em> a keystore and a CA cert, and both will
100  * be used.
101  * <p>
102  * The <code>autoregister</code> configuration parameter determines what
103  * the <code>canSelfRegister()</code> method returns. It also allows an
104  * EPerson record to be created automatically when the presented
105  * certificate is acceptable but there is no corresponding EPerson.
106  *
107  * @author Larry Stone
108  * @version $Revision: 1.1 $
109  */

110 public class X509Authentication
111     implements AuthenticationMethod
112 {
113
114     /** log4j category */
115     private static Logger log = Logger.getLogger(X509Authentication.class);
116
117     /** public key of CA to check client certs against. */
118     private static PublicKey JavaDoc caPublicKey = null;
119
120     /** key store for CA certs if we use that */
121     private static KeyStore JavaDoc caCertKeyStore = null;
122
123     /**
124      * Initialization:
125      * Set caPublicKey and/or keystore. This loads the information
126      * needed to check if a client cert presented is valid and acceptable.
127      */

128     static
129     {
130         String JavaDoc keystorePath = ConfigurationManager.getProperty("authentication.x509.keystore.path");
131         String JavaDoc keystorePassword = ConfigurationManager.getProperty("authentication.x509.keystore.password");
132         String JavaDoc caCertPath = ConfigurationManager.getProperty("authentication.x509.ca.cert");
133
134         // backward-compatible kludge
135
if (caCertPath == null)
136             caCertPath = ConfigurationManager.getProperty("webui.cert.ca");
137
138         // First look for keystore full of trusted certs.
139
if (keystorePath != null)
140         {
141             if (keystorePassword == null)
142                 keystorePassword = "";
143             try {
144                 KeyStore JavaDoc ks = KeyStore.getInstance("JKS");
145                 ks.load(new FileInputStream JavaDoc(keystorePath),
146                                      keystorePassword.toCharArray());
147                 caCertKeyStore = ks;
148             }
149             catch (IOException JavaDoc e)
150             {
151                 log.error("X509Authentication: Failed to load CA keystore, file="+
152                             keystorePath+", error="+e.toString());
153             }
154             catch (GeneralSecurityException JavaDoc e)
155             {
156                 log.error("X509Authentication: Failed to extract CA keystore, file="+
157                             keystorePath+", error="+e.toString());
158             }
159         }
160
161         // Second, try getting public key out of CA cert, if that's configured.
162
if (caCertPath != null)
163         {
164             try
165             {
166                 InputStream JavaDoc is = new BufferedInputStream JavaDoc(new FileInputStream JavaDoc(caCertPath));
167                 X509Certificate JavaDoc cert = (X509Certificate JavaDoc) CertificateFactory
168                                        .getInstance("X.509").generateCertificate(is);
169                 if (cert != null)
170                     caPublicKey = cert.getPublicKey();
171             }
172             catch (IOException JavaDoc e)
173             {
174                 log.error("X509Authentication: Failed to load CA cert, file="+
175                             caCertPath+", error="+e.toString());
176             }
177             catch (CertificateException JavaDoc e)
178             {
179                 log.error("X509Authentication: Failed to extract CA cert, file="+
180                             caCertPath+", error="+e.toString());
181             }
182         }
183     }
184
185     /**
186      * Return the email address from <code>certificate</code>, or null
187      * if an email address cannot be found in the certificate.
188      * <p>
189      * Note that the certificate parsing has only been tested with certificates
190      * granted by the MIT Certification Authority, and may not work elsewhere.
191      *
192      * @param certificate -
193      * An X509 certificate object
194      * @return - The email address found in certificate, or null if an email
195      * address cannot be found in the certificate.
196      */

197     private static String JavaDoc getEmail(X509Certificate JavaDoc certificate)
198             throws SQLException JavaDoc
199     {
200         Principal JavaDoc principal = certificate.getSubjectDN();
201
202         if (principal == null)
203             return null;
204
205         String JavaDoc dn = principal.getName();
206         if (dn == null)
207             return null;
208
209         StringTokenizer JavaDoc tokenizer = new StringTokenizer JavaDoc(dn, ",");
210         String JavaDoc token = null;
211         while (tokenizer.hasMoreTokens())
212         {
213             int len = "emailaddress=".length();
214
215             token = (String JavaDoc) tokenizer.nextToken();
216
217             if (token.toLowerCase().startsWith("emailaddress="))
218             {
219                 // Make sure the token actually contains something
220
if (token.length() <= len)
221                     return null;
222
223                 return token.substring(len).toLowerCase();
224             }
225         }
226
227         return null;
228     }
229
230     /**
231      * Verify CERTIFICATE against KEY. Return true if and only if CERTIFICATE is
232      * valid and can be verified against KEY.
233      *
234      * @param certificate -
235      * An X509 certificate object
236      * @param key -
237      * PublicKey to check the certificate against.
238      * @return - True if CERTIFICATE is valid and can be verified against KEY,
239      * false otherwise.
240      */

241     private static boolean isValid(Context JavaDoc context,
242                                    X509Certificate JavaDoc certificate)
243     {
244         if (certificate == null)
245             return false;
246
247         // This checks that current time is within cert's validity window:
248
try
249         {
250             certificate.checkValidity();
251         }
252         catch (CertificateException JavaDoc e)
253         {
254             log.info(LogManager.getHeader(context, "authentication",
255                     "X.509 Certificate is EXPIRED or PREMATURE: "+e.toString()));
256             return false;
257         }
258
259         // Try CA public key, if available.
260
if (caPublicKey != null)
261         {
262             try
263             {
264                 certificate.verify(caPublicKey);
265                 return true;
266             }
267             catch (GeneralSecurityException JavaDoc e)
268             {
269                 log.info(LogManager.getHeader(context, "authentication",
270                         "X.509 Certificate FAILED SIGNATURE check: "+e.toString()));
271             }
272         }
273
274         // Try it with keystore, if available.
275
if (caCertKeyStore != null)
276         {
277             try
278             {
279                 Enumeration JavaDoc ke = caCertKeyStore.aliases();
280
281                 while (ke.hasMoreElements())
282                 {
283                     String JavaDoc alias = (String JavaDoc)ke.nextElement();
284                     if (caCertKeyStore.isCertificateEntry(alias))
285                     {
286                         Certificate JavaDoc ca = caCertKeyStore.getCertificate(alias);
287                         try {
288                             certificate.verify(ca.getPublicKey());
289                             return true;
290                         }
291                         catch (CertificateException JavaDoc ce)
292                         {
293                         }
294                     }
295                 }
296                 log.info(LogManager.getHeader(context, "authentication",
297                         "Keystore method FAILED SIGNATURE check on client cert."));
298             }
299             catch (GeneralSecurityException JavaDoc e)
300             {
301                 log.info(LogManager.getHeader(context, "authentication",
302                         "X.509 Certificate FAILED SIGNATURE check: "+e.toString()));
303             }
304
305         }
306         return false;
307     }
308
309     /**
310      * Predicate, can new user automatically create EPerson.
311      * Checks configuration value. You'll probably want this to
312      * be true to take advantage of a Web certificate infrastructure
313      * with many more users than are already known by DSpace.
314      */

315     public boolean canSelfRegister(Context JavaDoc context,
316                                    HttpServletRequest JavaDoc request,
317                                    String JavaDoc username)
318         throws SQLException JavaDoc
319     {
320         return ConfigurationManager
321             .getBooleanProperty("authentication.x509.autoregister");
322     }
323
324     /**
325      * Nothing extra to initialize.
326      */

327     public void initEPerson(Context JavaDoc context, HttpServletRequest JavaDoc request,
328             EPerson eperson)
329         throws SQLException JavaDoc
330     {
331     }
332
333     /**
334      * We don't use EPerson password so there is no reason to change it.
335      */

336     public boolean allowSetPassword(Context JavaDoc context,
337                                     HttpServletRequest JavaDoc request,
338                                     String JavaDoc username)
339         throws SQLException JavaDoc
340     {
341         return false;
342     }
343
344     /**
345      * Returns true, this is an implicit method.
346      */

347     public boolean isImplicit()
348     {
349         return true;
350     }
351
352     /**
353      * No special groups.
354      */

355     public int[] getSpecialGroups(Context JavaDoc context, HttpServletRequest JavaDoc request)
356     {
357         return new int[0];
358     }
359
360     /**
361      * X509 certificate authentication. The client certificate
362      * is obtained from the <code>ServletRequest</code> object.
363      * <ul>
364      * <li>If the certificate is valid, and corresponds to an existing EPerson,
365      * and the user is allowed to login, return success.</li>
366      * <li>If the user is matched but is not allowed to login, it fails.</li>
367      * <li>If the certificate is valid, but there is no corresponding EPerson,
368      * the <code>"authentication.x509.autoregister"</code>
369      * configuration parameter is checked (via <code>canSelfRegister()</code>)
370      * <ul>
371      * <li>If it's true, a new EPerson record is created for the certificate, and
372      * the result is success.</li>
373      * <li>If it's false, return that the user was unknown.</li>
374      * </ul>
375      * </li>
376      * </ul>
377      *
378      * @return One of: SUCCESS, BAD_CREDENTIALS, NO_SUCH_USER, BAD_ARGS
379      */

380     public int authenticate(Context JavaDoc context,
381                             String JavaDoc username,
382                             String JavaDoc password,
383                             String JavaDoc realm,
384                             HttpServletRequest JavaDoc request)
385         throws SQLException JavaDoc
386     {
387         // Obtain the certificate from the request, if any
388
X509Certificate JavaDoc[] certs = null;
389         if (request != null)
390             certs = (X509Certificate JavaDoc[]) request
391                 .getAttribute("javax.servlet.request.X509Certificate");
392
393         if ((certs == null) || (certs.length == 0))
394             return BAD_ARGS;
395         else
396         {
397             // We have a cert -- check it and get username from it.
398
try
399             {
400                 if (!isValid(context, certs[0]))
401                 {
402                     log.warn(LogManager.getHeader(context, "authenticate",
403                         "type=x509certificate, status=BAD_CREDENTIALS (not valid)"));
404                     return BAD_CREDENTIALS;
405                 }
406
407                 // And it's valid - try and get an e-person
408
String JavaDoc email = getEmail(certs[0]);
409                 EPerson eperson = null;
410                 if (email != null)
411                     eperson = EPerson.findByEmail(context, email);
412                 if (eperson == null)
413                 {
414                     // Cert is valid, but no record.
415
if (email != null && canSelfRegister(context, request, null))
416                     {
417                         // Register the new user automatically
418
log.info(LogManager.getHeader(context,
419                                 "autoregister", "from=x.509, email=" + email));
420
421                         // TEMPORARILY turn off authorisation
422
context.setIgnoreAuthorization(true);
423                         eperson = EPerson.create(context);
424                         eperson.setEmail(email);
425                         eperson.setCanLogIn(true);
426                         AuthenticationManager.initEPerson(context,
427                                 request, eperson);
428                         eperson.update();
429                         context.commit();
430                         context.setIgnoreAuthorization(false);
431                         context.setCurrentUser(eperson);
432                         return SUCCESS;
433                     }
434                     else
435                     {
436                         // No auto-registration for valid certs
437
log.warn(LogManager.getHeader(context, "authenticate",
438                                 "type=cert_but_no_record, cannot auto-register"));
439                         return NO_SUCH_USER;
440                     }
441                 }
442
443                 // make sure this is a login account
444
else if (!eperson.canLogIn())
445                 {
446                     log.warn(LogManager.getHeader(context, "authenticate",
447                             "type=x509certificate, email="+email+", canLogIn=false, rejecting."));
448                     return BAD_ARGS;
449                 }
450
451                 else
452                 {
453                     log.info(LogManager.getHeader(context, "login",
454                             "type=x509certificate"));
455                     context.setCurrentUser(eperson);
456                     return SUCCESS;
457                 }
458             }
459             catch (AuthorizeException ce)
460             {
461                 log.warn(LogManager.getHeader(context, "authorize_exception",
462                         ""), ce);
463             }
464
465             return BAD_ARGS;
466         }
467     }
468
469     /**
470      * Return null, since this is an implicit method with no login page.
471      *
472      * @param context
473      * DSpace context, will be modified (ePerson set) upon success.
474      *
475      * @param request
476      * The HTTP request that started this operation, or null if not applicable.
477      *
478      * @param response
479      * The HTTP response from the servlet method.
480      *
481      * @return fully-qualified URL
482      */

483     public String JavaDoc loginPageURL(Context JavaDoc context,
484                             HttpServletRequest JavaDoc request,
485                             HttpServletResponse JavaDoc response)
486     {
487         return null;
488     }
489
490     /**
491      * Return null, since this is an implicit method with no login page.
492      *
493      * @param context
494      * DSpace context, will be modified (ePerson set) upon success.
495      *
496      * @return Message key to look up in i18n message catalog.
497      */

498     public String JavaDoc loginPageTitle(Context JavaDoc context)
499     {
500         return null;
501     }
502 }
503
Popular Tags