001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/authenticator/DigestAuthenticator.java,v 1.10 2001/10/19 16:23:57 craigmcc Exp $
003: * $Revision: 1.10 $
004: * $Date: 2001/10/19 16:23:57 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 1999 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: * [Additional notices, if required by prior licensing conditions]
061: *
062: */
063:
064: package org.apache.catalina.authenticator;
065:
066: import java.io.IOException;
067: import java.security.Principal;
068: import java.security.MessageDigest;
069: import java.security.NoSuchAlgorithmException;
070: import java.util.Hashtable;
071: import java.util.StringTokenizer;
072: import javax.servlet.http.HttpServletRequest;
073: import javax.servlet.http.HttpServletResponse;
074: import org.apache.catalina.HttpRequest;
075: import org.apache.catalina.HttpResponse;
076: import org.apache.catalina.Realm;
077: import org.apache.catalina.Session;
078: import org.apache.catalina.deploy.LoginConfig;
079: import org.apache.catalina.util.MD5Encoder;
080:
081: /**
082: * An <b>Authenticator</b> and <b>Valve</b> implementation of HTTP DIGEST
083: * Authentication (see RFC 2069).
084: *
085: * @author Craig R. McClanahan
086: * @author Remy Maucherat
087: * @version $Revision: 1.10 $ $Date: 2001/10/19 16:23:57 $
088: */
089:
090: public class DigestAuthenticator extends AuthenticatorBase {
091:
092: // -------------------------------------------------------------- Constants
093:
094: /**
095: * Indicates that no once tokens are used only once.
096: */
097: protected static final int USE_ONCE = 1;
098:
099: /**
100: * Indicates that no once tokens are used only once.
101: */
102: protected static final int USE_NEVER_EXPIRES = Integer.MAX_VALUE;
103:
104: /**
105: * Indicates that no once tokens are used only once.
106: */
107: protected static final int TIMEOUT_INFINITE = Integer.MAX_VALUE;
108:
109: /**
110: * The MD5 helper object for this class.
111: */
112: protected static final MD5Encoder md5Encoder = new MD5Encoder();
113:
114: /**
115: * Descriptive information about this implementation.
116: */
117: protected static final String info = "org.apache.catalina.authenticator.DigestAuthenticator/1.0";
118:
119: // ----------------------------------------------------------- Constructors
120:
121: public DigestAuthenticator() {
122: super ();
123: try {
124: if (md5Helper == null)
125: md5Helper = MessageDigest.getInstance("MD5");
126: } catch (NoSuchAlgorithmException e) {
127: e.printStackTrace();
128: throw new IllegalStateException();
129: }
130: }
131:
132: // ----------------------------------------------------- Instance Variables
133:
134: /**
135: * MD5 message digest provider.
136: */
137: protected static MessageDigest md5Helper;
138:
139: /**
140: * No once hashtable.
141: */
142: protected Hashtable nOnceTokens = new Hashtable();
143:
144: /**
145: * No once expiration (in millisecond). A shorter amount would mean a
146: * better security level (since the token is generated more often), but at
147: * the expense of a bigger server overhead.
148: */
149: protected long nOnceTimeout = TIMEOUT_INFINITE;
150:
151: /**
152: * No once expiration after a specified number of uses. A lower number
153: * would produce more overhead, since a token would have to be generated
154: * more often, but would be more secure.
155: */
156: protected int nOnceUses = USE_ONCE;
157:
158: /**
159: * Private key.
160: */
161: protected String key = "Catalina";
162:
163: // ------------------------------------------------------------- Properties
164:
165: /**
166: * Return descriptive information about this Valve implementation.
167: */
168: public String getInfo() {
169:
170: return (this .info);
171:
172: }
173:
174: // --------------------------------------------------------- Public Methods
175:
176: /**
177: * Authenticate the user making this request, based on the specified
178: * login configuration. Return <code>true</code> if any specified
179: * constraint has been satisfied, or <code>false</code> if we have
180: * created a response challenge already.
181: *
182: * @param request Request we are processing
183: * @param response Response we are creating
184: * @param login Login configuration describing how authentication
185: * should be performed
186: *
187: * @exception IOException if an input/output error occurs
188: */
189: public boolean authenticate(HttpRequest request,
190: HttpResponse response, LoginConfig config)
191: throws IOException {
192:
193: // Have we already authenticated someone?
194: Principal principal = ((HttpServletRequest) request
195: .getRequest()).getUserPrincipal();
196: if (principal != null)
197: return (true);
198:
199: // Validate any credentials already included with this request
200: HttpServletRequest hreq = (HttpServletRequest) request
201: .getRequest();
202: HttpServletResponse hres = (HttpServletResponse) response
203: .getResponse();
204: String authorization = request.getAuthorization();
205: if (authorization != null) {
206: principal = findPrincipal(hreq, authorization, context
207: .getRealm());
208: if (principal != null) {
209: String username = parseUsername(authorization);
210: register(request, response, principal,
211: Constants.DIGEST_METHOD, username, null);
212: return (true);
213: }
214: }
215:
216: // Send an "unauthorized" response and an appropriate challenge
217:
218: // Next, generate a nOnce token (that is a token which is supposed
219: // to be unique).
220: String nOnce = generateNOnce(hreq);
221:
222: setAuthenticateHeader(hreq, hres, config, nOnce);
223: hres.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
224: // hres.flushBuffer();
225: return (false);
226:
227: }
228:
229: // ------------------------------------------------------ Protected Methods
230:
231: /**
232: * Parse the specified authorization credentials, and return the
233: * associated Principal that these credentials authenticate (if any)
234: * from the specified Realm. If there is no such Principal, return
235: * <code>null</code>.
236: *
237: * @param request HTTP servlet request
238: * @param authorization Authorization credentials from this request
239: * @param login Login configuration describing how authentication
240: * should be performed
241: * @param realm Realm used to authenticate Principals
242: */
243: protected static Principal findPrincipal(
244: HttpServletRequest request, String authorization,
245: Realm realm) {
246:
247: //System.out.println("Authorization token : " + authorization);
248: // Validate the authorization credentials format
249: if (authorization == null)
250: return (null);
251: if (!authorization.startsWith("Digest "))
252: return (null);
253: authorization = authorization.substring(7).trim();
254:
255: StringTokenizer commaTokenizer = new StringTokenizer(
256: authorization, ",");
257:
258: String userName = null;
259: String realmName = null;
260: String nOnce = null;
261: String nc = null;
262: String cnonce = null;
263: String qop = null;
264: String uri = null;
265: String response = null;
266: String opaque = null;
267: String method = request.getMethod();
268:
269: while (commaTokenizer.hasMoreTokens()) {
270: String currentToken = commaTokenizer.nextToken();
271: int equalSign = currentToken.indexOf('=');
272: if (equalSign < 0)
273: return null;
274: String currentTokenName = currentToken.substring(0,
275: equalSign).trim();
276: String currentTokenValue = currentToken.substring(
277: equalSign + 1).trim();
278: if ("username".equals(currentTokenName))
279: userName = removeQuotes(currentTokenValue);
280: if ("realm".equals(currentTokenName))
281: realmName = removeQuotes(currentTokenValue);
282: if ("nonce".equals(currentTokenName))
283: nOnce = removeQuotes(currentTokenValue);
284: if ("nc".equals(currentTokenName))
285: nc = currentTokenValue;
286: if ("cnonce".equals(currentTokenName))
287: cnonce = removeQuotes(currentTokenValue);
288: if ("qop".equals(currentTokenName))
289: qop = removeQuotes(currentTokenValue);
290: if ("uri".equals(currentTokenName))
291: uri = removeQuotes(currentTokenValue);
292: if ("response".equals(currentTokenName))
293: response = removeQuotes(currentTokenValue);
294: }
295:
296: if ((userName == null) || (realmName == null)
297: || (nOnce == null) || (uri == null)
298: || (response == null))
299: return null;
300:
301: // Second MD5 digest used to calculate the digest :
302: // MD5(Method + ":" + uri)
303: String a2 = method + ":" + uri;
304: //System.out.println("A2:" + a2);
305:
306: String md5a2 = md5Encoder.encode(md5Helper
307: .digest(a2.getBytes()));
308:
309: return (realm.authenticate(userName, response, nOnce, nc,
310: cnonce, qop, realmName, md5a2));
311:
312: }
313:
314: /**
315: * Parse the username from the specified authorization string. If none
316: * can be identified, return <code>null</code>
317: *
318: * @param authorization Authorization string to be parsed
319: */
320: protected String parseUsername(String authorization) {
321:
322: //System.out.println("Authorization token : " + authorization);
323: // Validate the authorization credentials format
324: if (authorization == null)
325: return (null);
326: if (!authorization.startsWith("Digest "))
327: return (null);
328: authorization = authorization.substring(7).trim();
329:
330: StringTokenizer commaTokenizer = new StringTokenizer(
331: authorization, ",");
332:
333: while (commaTokenizer.hasMoreTokens()) {
334: String currentToken = commaTokenizer.nextToken();
335: int equalSign = currentToken.indexOf('=');
336: if (equalSign < 0)
337: return null;
338: String currentTokenName = currentToken.substring(0,
339: equalSign).trim();
340: String currentTokenValue = currentToken.substring(
341: equalSign + 1).trim();
342: if ("username".equals(currentTokenName))
343: return (removeQuotes(currentTokenValue));
344: }
345:
346: return (null);
347:
348: }
349:
350: /**
351: * Removes the quotes on a string.
352: */
353: protected static String removeQuotes(String quotedString) {
354: if (quotedString.length() > 2) {
355: return quotedString.substring(1, quotedString.length() - 1);
356: } else {
357: return new String();
358: }
359: }
360:
361: /**
362: * Generate a unique token. The token is generated according to the
363: * following pattern. NOnceToken = Base64 ( MD5 ( client-IP ":"
364: * time-stamp ":" private-key ) ).
365: *
366: * @param request HTTP Servlet request
367: */
368: protected String generateNOnce(HttpServletRequest request) {
369:
370: long currentTime = System.currentTimeMillis();
371:
372: String nOnceValue = request.getRemoteAddr() + ":" + currentTime
373: + ":" + key;
374:
375: byte[] buffer = md5Helper.digest(nOnceValue.getBytes());
376: nOnceValue = md5Encoder.encode(buffer);
377:
378: // Updating the value in the no once hashtable
379: nOnceTokens.put(nOnceValue,
380: new Long(currentTime + nOnceTimeout));
381:
382: return nOnceValue;
383: }
384:
385: /**
386: * Generates the WWW-Authenticate header.
387: * <p>
388: * The header MUST follow this template :
389: * <pre>
390: * WWW-Authenticate = "WWW-Authenticate" ":" "Digest"
391: * digest-challenge
392: *
393: * digest-challenge = 1#( realm | [ domain ] | nOnce |
394: * [ digest-opaque ] |[ stale ] | [ algorithm ] )
395: *
396: * realm = "realm" "=" realm-value
397: * realm-value = quoted-string
398: * domain = "domain" "=" <"> 1#URI <">
399: * nonce = "nonce" "=" nonce-value
400: * nonce-value = quoted-string
401: * opaque = "opaque" "=" quoted-string
402: * stale = "stale" "=" ( "true" | "false" )
403: * algorithm = "algorithm" "=" ( "MD5" | token )
404: * </pre>
405: *
406: * @param request HTTP Servlet request
407: * @param resonse HTTP Servlet response
408: * @param login Login configuration describing how authentication
409: * should be performed
410: * @param nOnce nonce token
411: */
412: protected void setAuthenticateHeader(HttpServletRequest request,
413: HttpServletResponse response, LoginConfig config,
414: String nOnce) {
415:
416: // Get the realm name
417: String realmName = config.getRealmName();
418: if (realmName == null)
419: realmName = request.getServerName() + ":"
420: + request.getServerPort();
421:
422: byte[] buffer = md5Helper.digest(nOnce.getBytes());
423:
424: String authenticateHeader = "Digest realm=\"" + realmName
425: + "\", " + "qop=\"auth\", nonce=\"" + nOnce + "\", "
426: + "opaque=\"" + md5Encoder.encode(buffer) + "\"";
427: // System.out.println("Authenticate header value : "
428: // + authenticateHeader);
429: response.setHeader("WWW-Authenticate", authenticateHeader);
430:
431: }
432:
433: }
|