KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > catalina > authenticator > DigestAuthenticator


1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements. See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17
18
19 package org.apache.catalina.authenticator;
20
21
22 import java.io.IOException JavaDoc;
23 import java.security.MessageDigest JavaDoc;
24 import java.security.NoSuchAlgorithmException JavaDoc;
25 import java.security.Principal JavaDoc;
26 import java.util.StringTokenizer JavaDoc;
27
28 import javax.servlet.http.HttpServletResponse JavaDoc;
29
30
31 import org.apache.catalina.Realm;
32 import org.apache.catalina.connector.Request;
33 import org.apache.catalina.connector.Response;
34 import org.apache.catalina.deploy.LoginConfig;
35 import org.apache.catalina.util.MD5Encoder;
36 import org.apache.commons.logging.Log;
37 import org.apache.commons.logging.LogFactory;
38
39
40
41 /**
42  * An <b>Authenticator</b> and <b>Valve</b> implementation of HTTP DIGEST
43  * Authentication (see RFC 2069).
44  *
45  * @author Craig R. McClanahan
46  * @author Remy Maucherat
47  * @version $Revision: 467222 $ $Date: 2006-10-24 05:17:11 +0200 (mar., 24 oct. 2006) $
48  */

49
50 public class DigestAuthenticator
51     extends AuthenticatorBase {
52     private static Log log = LogFactory.getLog(DigestAuthenticator.class);
53
54
55     // -------------------------------------------------------------- Constants
56

57     /**
58      * The MD5 helper object for this class.
59      */

60     protected static final MD5Encoder md5Encoder = new MD5Encoder();
61
62
63     /**
64      * Descriptive information about this implementation.
65      */

66     protected static final String JavaDoc info =
67         "org.apache.catalina.authenticator.DigestAuthenticator/1.0";
68
69
70     // ----------------------------------------------------------- Constructors
71

72
73     public DigestAuthenticator() {
74         super();
75         try {
76             if (md5Helper == null)
77                 md5Helper = MessageDigest.getInstance("MD5");
78         } catch (NoSuchAlgorithmException JavaDoc e) {
79             e.printStackTrace();
80             throw new IllegalStateException JavaDoc();
81         }
82     }
83
84
85     // ----------------------------------------------------- Instance Variables
86

87
88     /**
89      * MD5 message digest provider.
90      */

91     protected static MessageDigest JavaDoc md5Helper;
92
93
94     /**
95      * Private key.
96      */

97     protected String JavaDoc key = "Catalina";
98
99
100     // ------------------------------------------------------------- Properties
101

102
103     /**
104      * Return descriptive information about this Valve implementation.
105      */

106     public String JavaDoc getInfo() {
107
108         return (info);
109
110     }
111
112
113     // --------------------------------------------------------- Public Methods
114

115
116     /**
117      * Authenticate the user making this request, based on the specified
118      * login configuration. Return <code>true</code> if any specified
119      * constraint has been satisfied, or <code>false</code> if we have
120      * created a response challenge already.
121      *
122      * @param request Request we are processing
123      * @param response Response we are creating
124      * @param config Login configuration describing how authentication
125      * should be performed
126      *
127      * @exception IOException if an input/output error occurs
128      */

129     public boolean authenticate(Request request,
130                                 Response JavaDoc response,
131                                 LoginConfig config)
132         throws IOException JavaDoc {
133
134         // Have we already authenticated someone?
135
Principal JavaDoc principal = request.getUserPrincipal();
136         //String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
137
if (principal != null) {
138             if (log.isDebugEnabled())
139                 log.debug("Already authenticated '" + principal.getName() + "'");
140             // Associate the session with any existing SSO session in order
141
// to get coordinated session invalidation at logout
142
String JavaDoc ssoId = (String JavaDoc) request.getNote(Constants.REQ_SSOID_NOTE);
143             if (ssoId != null)
144                 associate(ssoId, request.getSessionInternal(true));
145             return (true);
146         }
147
148         // NOTE: We don't try to reauthenticate using any existing SSO session,
149
// because that will only work if the original authentication was
150
// BASIC or FORM, which are less secure than the DIGEST auth-type
151
// specified for this webapp
152
//
153
// Uncomment below to allow previous FORM or BASIC authentications
154
// to authenticate users for this webapp
155
// TODO make this a configurable attribute (in SingleSignOn??)
156
/*
157         // Is there an SSO session against which we can try to reauthenticate?
158         if (ssoId != null) {
159             if (log.isDebugEnabled())
160                 log.debug("SSO Id " + ssoId + " set; attempting " +
161                           "reauthentication");
162             // Try to reauthenticate using data cached by SSO. If this fails,
163             // either the original SSO logon was of DIGEST or SSL (which
164             // we can't reauthenticate ourselves because there is no
165             // cached username and password), or the realm denied
166             // the user's reauthentication for some reason.
167             // In either case we have to prompt the user for a logon
168             if (reauthenticateFromSSO(ssoId, request))
169                 return true;
170         }
171         */

172
173         // Validate any credentials already included with this request
174
String JavaDoc authorization = request.getHeader("authorization");
175         if (authorization != null) {
176             principal = findPrincipal(request, authorization, context.getRealm());
177             if (principal != null) {
178                 String JavaDoc username = parseUsername(authorization);
179                 register(request, response, principal,
180                          Constants.DIGEST_METHOD,
181                          username, null);
182                 return (true);
183             }
184         }
185
186         // Send an "unauthorized" response and an appropriate challenge
187

188         // Next, generate a nOnce token (that is a token which is supposed
189
// to be unique).
190
String JavaDoc nOnce = generateNOnce(request);
191
192         setAuthenticateHeader(request, response, config, nOnce);
193         response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
194         // hres.flushBuffer();
195
return (false);
196
197     }
198
199
200     // ------------------------------------------------------ Protected Methods
201

202
203     /**
204      * Parse the specified authorization credentials, and return the
205      * associated Principal that these credentials authenticate (if any)
206      * from the specified Realm. If there is no such Principal, return
207      * <code>null</code>.
208      *
209      * @param request HTTP servlet request
210      * @param authorization Authorization credentials from this request
211      * @param realm Realm used to authenticate Principals
212      */

213     protected static Principal JavaDoc findPrincipal(Request request,
214                                              String JavaDoc authorization,
215                                              Realm realm) {
216
217         //System.out.println("Authorization token : " + authorization);
218
// Validate the authorization credentials format
219
if (authorization == null)
220             return (null);
221         if (!authorization.startsWith("Digest "))
222             return (null);
223         authorization = authorization.substring(7).trim();
224
225         // Bugzilla 37132: http://issues.apache.org/bugzilla/show_bug.cgi?id=37132
226
String JavaDoc[] tokens = authorization.split(",(?=(?:[^\"]*\"[^\"]*\")+$)");
227
228         String JavaDoc userName = null;
229         String JavaDoc realmName = null;
230         String JavaDoc nOnce = null;
231         String JavaDoc nc = null;
232         String JavaDoc cnonce = null;
233         String JavaDoc qop = null;
234         String JavaDoc uri = null;
235         String JavaDoc response = null;
236         String JavaDoc method = request.getMethod();
237
238         for (int i = 0; i < tokens.length; i++) {
239             String JavaDoc currentToken = tokens[i];
240             if (currentToken.length() == 0)
241                 continue;
242
243             int equalSign = currentToken.indexOf('=');
244             if (equalSign < 0)
245                 return null;
246             String JavaDoc currentTokenName =
247                 currentToken.substring(0, equalSign).trim();
248             String JavaDoc currentTokenValue =
249                 currentToken.substring(equalSign + 1).trim();
250             if ("username".equals(currentTokenName))
251                 userName = removeQuotes(currentTokenValue);
252             if ("realm".equals(currentTokenName))
253                 realmName = removeQuotes(currentTokenValue, true);
254             if ("nonce".equals(currentTokenName))
255                 nOnce = removeQuotes(currentTokenValue);
256             if ("nc".equals(currentTokenName))
257                 nc = removeQuotes(currentTokenValue);
258             if ("cnonce".equals(currentTokenName))
259                 cnonce = removeQuotes(currentTokenValue);
260             if ("qop".equals(currentTokenName))
261                 qop = removeQuotes(currentTokenValue);
262             if ("uri".equals(currentTokenName))
263                 uri = removeQuotes(currentTokenValue);
264             if ("response".equals(currentTokenName))
265                 response = removeQuotes(currentTokenValue);
266         }
267
268         if ( (userName == null) || (realmName == null) || (nOnce == null)
269              || (uri == null) || (response == null) )
270             return null;
271
272         // Second MD5 digest used to calculate the digest :
273
// MD5(Method + ":" + uri)
274
String JavaDoc a2 = method + ":" + uri;
275         //System.out.println("A2:" + a2);
276

277         byte[] buffer = null;
278         synchronized (md5Helper) {
279             buffer = md5Helper.digest(a2.getBytes());
280         }
281         String JavaDoc md5a2 = md5Encoder.encode(buffer);
282
283         return (realm.authenticate(userName, response, nOnce, nc, cnonce, qop,
284                                    realmName, md5a2));
285
286     }
287
288
289     /**
290      * Parse the username from the specified authorization string. If none
291      * can be identified, return <code>null</code>
292      *
293      * @param authorization Authorization string to be parsed
294      */

295     protected String JavaDoc parseUsername(String JavaDoc authorization) {
296
297         //System.out.println("Authorization token : " + authorization);
298
// Validate the authorization credentials format
299
if (authorization == null)
300             return (null);
301         if (!authorization.startsWith("Digest "))
302             return (null);
303         authorization = authorization.substring(7).trim();
304
305         StringTokenizer JavaDoc commaTokenizer =
306             new StringTokenizer JavaDoc(authorization, ",");
307
308         while (commaTokenizer.hasMoreTokens()) {
309             String JavaDoc currentToken = commaTokenizer.nextToken();
310             int equalSign = currentToken.indexOf('=');
311             if (equalSign < 0)
312                 return null;
313             String JavaDoc currentTokenName =
314                 currentToken.substring(0, equalSign).trim();
315             String JavaDoc currentTokenValue =
316                 currentToken.substring(equalSign + 1).trim();
317             if ("username".equals(currentTokenName))
318                 return (removeQuotes(currentTokenValue));
319         }
320
321         return (null);
322
323     }
324
325
326     /**
327      * Removes the quotes on a string. RFC2617 states quotes are optional for
328      * all parameters except realm.
329      */

330     protected static String JavaDoc removeQuotes(String JavaDoc quotedString,
331                                          boolean quotesRequired) {
332         //support both quoted and non-quoted
333
if (quotedString.length() > 0 && quotedString.charAt(0) != '"' &&
334                 !quotesRequired) {
335             return quotedString;
336         } else if (quotedString.length() > 2) {
337             return quotedString.substring(1, quotedString.length() - 1);
338         } else {
339             return new String JavaDoc();
340         }
341     }
342
343     /**
344      * Removes the quotes on a string.
345      */

346     protected static String JavaDoc removeQuotes(String JavaDoc quotedString) {
347         return removeQuotes(quotedString, false);
348     }
349
350     /**
351      * Generate a unique token. The token is generated according to the
352      * following pattern. NOnceToken = Base64 ( MD5 ( client-IP ":"
353      * time-stamp ":" private-key ) ).
354      *
355      * @param request HTTP Servlet request
356      */

357     protected String JavaDoc generateNOnce(Request request) {
358
359         long currentTime = System.currentTimeMillis();
360
361         String JavaDoc nOnceValue = request.getRemoteAddr() + ":" +
362             currentTime + ":" + key;
363
364         byte[] buffer = null;
365         synchronized (md5Helper) {
366             buffer = md5Helper.digest(nOnceValue.getBytes());
367         }
368         nOnceValue = md5Encoder.encode(buffer);
369
370         return nOnceValue;
371     }
372
373
374     /**
375      * Generates the WWW-Authenticate header.
376      * <p>
377      * The header MUST follow this template :
378      * <pre>
379      * WWW-Authenticate = "WWW-Authenticate" ":" "Digest"
380      * digest-challenge
381      *
382      * digest-challenge = 1#( realm | [ domain ] | nOnce |
383      * [ digest-opaque ] |[ stale ] | [ algorithm ] )
384      *
385      * realm = "realm" "=" realm-value
386      * realm-value = quoted-string
387      * domain = "domain" "=" <"> 1#URI <">
388      * nonce = "nonce" "=" nonce-value
389      * nonce-value = quoted-string
390      * opaque = "opaque" "=" quoted-string
391      * stale = "stale" "=" ( "true" | "false" )
392      * algorithm = "algorithm" "=" ( "MD5" | token )
393      * </pre>
394      *
395      * @param request HTTP Servlet request
396      * @param response HTTP Servlet response
397      * @param config Login configuration describing how authentication
398      * should be performed
399      * @param nOnce nonce token
400      */

401     protected void setAuthenticateHeader(Request request,
402                                          Response JavaDoc response,
403                                          LoginConfig config,
404                                          String JavaDoc nOnce) {
405
406         // Get the realm name
407
String JavaDoc realmName = config.getRealmName();
408         if (realmName == null)
409             realmName = request.getServerName() + ":"
410                 + request.getServerPort();
411
412         byte[] buffer = null;
413         synchronized (md5Helper) {
414             buffer = md5Helper.digest(nOnce.getBytes());
415         }
416
417         String JavaDoc authenticateHeader = "Digest realm=\"" + realmName + "\", "
418             + "qop=\"auth\", nonce=\"" + nOnce + "\", " + "opaque=\""
419             + md5Encoder.encode(buffer) + "\"";
420         response.setHeader("WWW-Authenticate", authenticateHeader);
421
422     }
423
424
425 }
426
Popular Tags