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