001: /*
002: * Copyright 1999,2004 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.apache.catalina.authenticator;
018:
019: import java.io.IOException;
020: import java.security.MessageDigest;
021: import java.security.NoSuchAlgorithmException;
022: import java.security.Principal;
023: import java.util.Hashtable;
024: import java.util.StringTokenizer;
025:
026: import javax.servlet.http.HttpServletRequest;
027: import javax.servlet.http.HttpServletResponse;
028:
029: import org.apache.commons.logging.Log;
030: import org.apache.commons.logging.LogFactory;
031:
032: import org.apache.catalina.HttpRequest;
033: import org.apache.catalina.HttpResponse;
034: import org.apache.catalina.Realm;
035: import org.apache.catalina.deploy.LoginConfig;
036: import org.apache.catalina.util.MD5Encoder;
037:
038: /**
039: * An <b>Authenticator</b> and <b>Valve</b> implementation of HTTP DIGEST
040: * Authentication (see RFC 2069).
041: *
042: * @author Craig R. McClanahan
043: * @author Remy Maucherat
044: * @version $Revision: 1.6 $ $Date: 2004/04/22 21:48:32 $
045: */
046:
047: public class DigestAuthenticator extends AuthenticatorBase {
048: private static Log log = LogFactory
049: .getLog(DigestAuthenticator.class);
050:
051: // -------------------------------------------------------------- Constants
052:
053: /**
054: * Indicates that no once tokens are used only once.
055: */
056: protected static final int USE_ONCE = 1;
057:
058: /**
059: * Indicates that no once tokens are used only once.
060: */
061: protected static final int USE_NEVER_EXPIRES = Integer.MAX_VALUE;
062:
063: /**
064: * Indicates that no once tokens are used only once.
065: */
066: protected static final int TIMEOUT_INFINITE = Integer.MAX_VALUE;
067:
068: /**
069: * The MD5 helper object for this class.
070: */
071: protected static final MD5Encoder md5Encoder = new MD5Encoder();
072:
073: /**
074: * Descriptive information about this implementation.
075: */
076: protected static final String info = "org.apache.catalina.authenticator.DigestAuthenticator/1.0";
077:
078: // ----------------------------------------------------------- Constructors
079:
080: public DigestAuthenticator() {
081: super ();
082: try {
083: if (md5Helper == null)
084: md5Helper = MessageDigest.getInstance("MD5");
085: } catch (NoSuchAlgorithmException e) {
086: e.printStackTrace();
087: throw new IllegalStateException();
088: }
089: }
090:
091: // ----------------------------------------------------- Instance Variables
092:
093: /**
094: * MD5 message digest provider.
095: */
096: protected static MessageDigest md5Helper;
097:
098: /**
099: * No once hashtable.
100: */
101: protected Hashtable nOnceTokens = new Hashtable();
102:
103: /**
104: * No once expiration (in millisecond). A shorter amount would mean a
105: * better security level (since the token is generated more often), but at
106: * the expense of a bigger server overhead.
107: */
108: protected long nOnceTimeout = TIMEOUT_INFINITE;
109:
110: /**
111: * No once expiration after a specified number of uses. A lower number
112: * would produce more overhead, since a token would have to be generated
113: * more often, but would be more secure.
114: */
115: protected int nOnceUses = USE_ONCE;
116:
117: /**
118: * Private key.
119: */
120: protected String key = "Catalina";
121:
122: // ------------------------------------------------------------- Properties
123:
124: /**
125: * Return descriptive information about this Valve implementation.
126: */
127: public String getInfo() {
128:
129: return (info);
130:
131: }
132:
133: // --------------------------------------------------------- Public Methods
134:
135: /**
136: * Authenticate the user making this request, based on the specified
137: * login configuration. Return <code>true</code> if any specified
138: * constraint has been satisfied, or <code>false</code> if we have
139: * created a response challenge already.
140: *
141: * @param request Request we are processing
142: * @param response Response we are creating
143: * @param config Login configuration describing how authentication
144: * should be performed
145: *
146: * @exception IOException if an input/output error occurs
147: */
148: public boolean authenticate(HttpRequest request,
149: HttpResponse response, LoginConfig config)
150: throws IOException {
151:
152: // Have we already authenticated someone?
153: Principal principal = ((HttpServletRequest) request
154: .getRequest()).getUserPrincipal();
155: //String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
156: if (principal != null) {
157: if (log.isDebugEnabled())
158: log.debug("Already authenticated '"
159: + principal.getName() + "'");
160: // Associate the session with any existing SSO session in order
161: // to get coordinated session invalidation at logout
162: String ssoId = (String) request
163: .getNote(Constants.REQ_SSOID_NOTE);
164: if (ssoId != null)
165: associate(ssoId, getSession(request, true));
166: return (true);
167: }
168:
169: // NOTE: We don't try to reauthenticate using any existing SSO session,
170: // because that will only work if the original authentication was
171: // BASIC or FORM, which are less secure than the DIGEST auth-type
172: // specified for this webapp
173: //
174: // Uncomment below to allow previous FORM or BASIC authentications
175: // to authenticate users for this webapp
176: // TODO make this a configurable attribute (in SingleSignOn??)
177: /*
178: // Is there an SSO session against which we can try to reauthenticate?
179: if (ssoId != null) {
180: if (log.isDebugEnabled())
181: log.debug("SSO Id " + ssoId + " set; attempting " +
182: "reauthentication");
183: // Try to reauthenticate using data cached by SSO. If this fails,
184: // either the original SSO logon was of DIGEST or SSL (which
185: // we can't reauthenticate ourselves because there is no
186: // cached username and password), or the realm denied
187: // the user's reauthentication for some reason.
188: // In either case we have to prompt the user for a logon
189: if (reauthenticateFromSSO(ssoId, request))
190: return true;
191: }
192: */
193:
194: // Validate any credentials already included with this request
195: HttpServletRequest hreq = (HttpServletRequest) request
196: .getRequest();
197: HttpServletResponse hres = (HttpServletResponse) response
198: .getResponse();
199: String authorization = request.getAuthorization();
200: if (authorization != null) {
201: principal = findPrincipal(hreq, authorization, context
202: .getRealm());
203: if (principal != null) {
204: String username = parseUsername(authorization);
205: register(request, response, principal,
206: Constants.DIGEST_METHOD, username, null);
207: return (true);
208: }
209: }
210:
211: // Send an "unauthorized" response and an appropriate challenge
212:
213: // Next, generate a nOnce token (that is a token which is supposed
214: // to be unique).
215: String nOnce = generateNOnce(hreq);
216:
217: setAuthenticateHeader(hreq, hres, config, nOnce);
218: hres.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
219: // hres.flushBuffer();
220: return (false);
221:
222: }
223:
224: // ------------------------------------------------------ Protected Methods
225:
226: /**
227: * Parse the specified authorization credentials, and return the
228: * associated Principal that these credentials authenticate (if any)
229: * from the specified Realm. If there is no such Principal, return
230: * <code>null</code>.
231: *
232: * @param request HTTP servlet request
233: * @param authorization Authorization credentials from this request
234: * @param realm Realm used to authenticate Principals
235: */
236: protected static Principal findPrincipal(
237: HttpServletRequest request, String authorization,
238: Realm realm) {
239:
240: //System.out.println("Authorization token : " + authorization);
241: // Validate the authorization credentials format
242: if (authorization == null)
243: return (null);
244: if (!authorization.startsWith("Digest "))
245: return (null);
246: authorization = authorization.substring(7).trim();
247:
248: StringTokenizer commaTokenizer = new StringTokenizer(
249: authorization, ",");
250:
251: String userName = null;
252: String realmName = null;
253: String nOnce = null;
254: String nc = null;
255: String cnonce = null;
256: String qop = null;
257: String uri = null;
258: String response = null;
259: String method = request.getMethod();
260:
261: while (commaTokenizer.hasMoreTokens()) {
262: String currentToken = commaTokenizer.nextToken();
263: int equalSign = currentToken.indexOf('=');
264: if (equalSign < 0)
265: return null;
266: String currentTokenName = currentToken.substring(0,
267: equalSign).trim();
268: String currentTokenValue = currentToken.substring(
269: equalSign + 1).trim();
270: if ("username".equals(currentTokenName))
271: userName = removeQuotes(currentTokenValue);
272: if ("realm".equals(currentTokenName))
273: realmName = removeQuotes(currentTokenValue, true);
274: if ("nonce".equals(currentTokenName))
275: nOnce = removeQuotes(currentTokenValue);
276: if ("nc".equals(currentTokenName))
277: nc = currentTokenValue;
278: if ("cnonce".equals(currentTokenName))
279: cnonce = removeQuotes(currentTokenValue);
280: if ("qop".equals(currentTokenName))
281: qop = removeQuotes(currentTokenValue);
282: if ("uri".equals(currentTokenName))
283: uri = removeQuotes(currentTokenValue);
284: if ("response".equals(currentTokenName))
285: response = removeQuotes(currentTokenValue);
286: }
287:
288: if ((userName == null) || (realmName == null)
289: || (nOnce == null) || (uri == null)
290: || (response == null))
291: return null;
292:
293: // Second MD5 digest used to calculate the digest :
294: // MD5(Method + ":" + uri)
295: String a2 = method + ":" + uri;
296: //System.out.println("A2:" + a2);
297:
298: String md5a2 = md5Encoder.encode(md5Helper
299: .digest(a2.getBytes()));
300:
301: return (realm.authenticate(userName, response, nOnce, nc,
302: cnonce, qop, realmName, md5a2));
303:
304: }
305:
306: /**
307: * Parse the username from the specified authorization string. If none
308: * can be identified, return <code>null</code>
309: *
310: * @param authorization Authorization string to be parsed
311: */
312: protected String parseUsername(String authorization) {
313:
314: //System.out.println("Authorization token : " + authorization);
315: // Validate the authorization credentials format
316: if (authorization == null)
317: return (null);
318: if (!authorization.startsWith("Digest "))
319: return (null);
320: authorization = authorization.substring(7).trim();
321:
322: StringTokenizer commaTokenizer = new StringTokenizer(
323: authorization, ",");
324:
325: while (commaTokenizer.hasMoreTokens()) {
326: String currentToken = commaTokenizer.nextToken();
327: int equalSign = currentToken.indexOf('=');
328: if (equalSign < 0)
329: return null;
330: String currentTokenName = currentToken.substring(0,
331: equalSign).trim();
332: String currentTokenValue = currentToken.substring(
333: equalSign + 1).trim();
334: if ("username".equals(currentTokenName))
335: return (removeQuotes(currentTokenValue));
336: }
337:
338: return (null);
339:
340: }
341:
342: /**
343: * Removes the quotes on a string. RFC2617 states quotes are optional for
344: * all parameters except realm.
345: */
346: protected static String removeQuotes(String quotedString,
347: boolean quotesRequired) {
348: //support both quoted and non-quoted
349: if (quotedString.length() > 0 && quotedString.charAt(0) != '"'
350: && !quotesRequired) {
351: return quotedString;
352: } else if (quotedString.length() > 2) {
353: return quotedString.substring(1, quotedString.length() - 1);
354: } else {
355: return new String();
356: }
357: }
358:
359: /**
360: * Removes the quotes on a string.
361: */
362: protected static String removeQuotes(String quotedString) {
363: return removeQuotes(quotedString, false);
364: }
365:
366: /**
367: * Generate a unique token. The token is generated according to the
368: * following pattern. NOnceToken = Base64 ( MD5 ( client-IP ":"
369: * time-stamp ":" private-key ) ).
370: *
371: * @param request HTTP Servlet request
372: */
373: protected String generateNOnce(HttpServletRequest request) {
374:
375: long currentTime = System.currentTimeMillis();
376:
377: String nOnceValue = request.getRemoteAddr() + ":" + currentTime
378: + ":" + key;
379:
380: byte[] buffer = md5Helper.digest(nOnceValue.getBytes());
381: nOnceValue = md5Encoder.encode(buffer);
382:
383: // Updating the value in the no once hashtable
384: nOnceTokens.put(nOnceValue,
385: new Long(currentTime + nOnceTimeout));
386:
387: return nOnceValue;
388: }
389:
390: /**
391: * Generates the WWW-Authenticate header.
392: * <p>
393: * The header MUST follow this template :
394: * <pre>
395: * WWW-Authenticate = "WWW-Authenticate" ":" "Digest"
396: * digest-challenge
397: *
398: * digest-challenge = 1#( realm | [ domain ] | nOnce |
399: * [ digest-opaque ] |[ stale ] | [ algorithm ] )
400: *
401: * realm = "realm" "=" realm-value
402: * realm-value = quoted-string
403: * domain = "domain" "=" <"> 1#URI <">
404: * nonce = "nonce" "=" nonce-value
405: * nonce-value = quoted-string
406: * opaque = "opaque" "=" quoted-string
407: * stale = "stale" "=" ( "true" | "false" )
408: * algorithm = "algorithm" "=" ( "MD5" | token )
409: * </pre>
410: *
411: * @param request HTTP Servlet request
412: * @param response HTTP Servlet response
413: * @param config Login configuration describing how authentication
414: * should be performed
415: * @param nOnce nonce token
416: */
417: protected void setAuthenticateHeader(HttpServletRequest request,
418: HttpServletResponse response, LoginConfig config,
419: String nOnce) {
420:
421: // Get the realm name
422: String realmName = config.getRealmName();
423: if (realmName == null)
424: realmName = request.getServerName() + ":"
425: + request.getServerPort();
426:
427: byte[] buffer = md5Helper.digest(nOnce.getBytes());
428:
429: String authenticateHeader = "Digest realm=\"" + realmName
430: + "\", " + "qop=\"auth\", nonce=\"" + nOnce + "\", "
431: + "opaque=\"" + md5Encoder.encode(buffer) + "\"";
432: // System.out.println("Authenticate header value : "
433: // + authenticateHeader);
434: response.setHeader("WWW-Authenticate", authenticateHeader);
435:
436: }
437:
438: }
|