001: /*
002: jGuard is a security framework based on top of jaas (java authentication and authorization security).
003: it is written for web applications, to resolve simply, access control problems.
004: version $Name: $
005: http://sourceforge.net/projects/jguard/
006:
007: Copyright (C) 2004 Charles GAY
008:
009: This library is free software; you can redistribute it and/or
010: modify it under the terms of the GNU Lesser General Public
011: License as published by the Free Software Foundation; either
012: version 2.1 of the License, or (at your option) any later version.
013:
014: This library is distributed in the hope that it will be useful,
015: but WITHOUT ANY WARRANTY; without even the implied warranty of
016: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017: Lesser General Public License for more details.
018:
019: You should have received a copy of the GNU Lesser General Public
020: License along with this library; if not, write to the Free Software
021: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
022:
023:
024: jGuard project home page:
025: http://sourceforge.net/projects/jguard/
026:
027: */
028: package net.sf.jguard.jee.authentication.callbacks;
029:
030: import java.io.IOException;
031: import java.io.UnsupportedEncodingException;
032: import java.security.cert.X509Certificate;
033: import java.util.Arrays;
034: import java.util.Iterator;
035: import java.util.List;
036:
037: import javax.security.auth.Subject;
038: import javax.security.auth.callback.Callback;
039: import javax.security.auth.callback.CallbackHandler;
040: import javax.security.auth.callback.NameCallback;
041: import javax.security.auth.callback.PasswordCallback;
042: import javax.security.auth.callback.UnsupportedCallbackException;
043: import javax.servlet.FilterChain;
044: import javax.servlet.ServletException;
045: import javax.servlet.ServletRequest;
046: import javax.servlet.ServletResponse;
047: import javax.servlet.http.HttpServletRequest;
048: import javax.servlet.http.HttpServletResponse;
049: import javax.servlet.http.HttpSession;
050:
051: import net.sf.jguard.ext.SecurityConstants;
052: import net.sf.jguard.ext.authentication.callbacks.CertificatesCallback;
053: import net.sf.jguard.ext.authentication.callbacks.JCaptchaCallback;
054: import net.sf.jguard.ext.authentication.certificates.CertificateConverter;
055: import net.sf.jguard.jee.authentication.http.HttpAuthenticationUtils;
056: import net.sf.jguard.jee.authentication.http.HttpConstants;
057:
058: import org.apache.commons.logging.Log;
059: import org.apache.commons.logging.LogFactory;
060: import org.bouncycastle.util.encoders.Base64;
061:
062: import com.octo.captcha.service.CaptchaService;
063:
064: /**
065: * handle grabbing credentials from an HTTP request.
066: * @author <a href="mailto:diabolo512@users.sourceforge.net ">Charles Gay</a>
067: */
068: public class HttpCallbackHandler implements CallbackHandler {
069:
070: private static final String JAVAX_SERVLET_REQUEST_X509CERTIFICATE = "javax.servlet.request.X509Certificate";
071: private static final String DIGEST_REALM = "Digest realm=\"";
072: public static final String AUTHORIZATION = "Authorization";
073: private static final String BASIC_REALM = "Basic realm=\"";
074: private static final String NO_CACHE_AUTHORIZATION = "no-cache=\"Authorization\"";
075: private static final String CACHE_CONTROL = "Cache-Control";
076: private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
077: private static final String BASIC = "Basic ";
078: private static final String ISO_8859_1 = "ISO-8859-1";
079: /** Logger for this class */
080: private static final Log logger = LogFactory
081: .getLog(HttpCallbackHandler.class);
082: private HttpServletRequest httpRequest;
083: private HttpServletResponse httpResponse;
084: private String authScheme = HttpConstants.FORM_AUTH;
085: private static String loginField = "login";
086: private static String passwordField = "password";
087: private boolean afterRegistration;
088:
089: /**
090: * constructor required by javadoc of the CallbackHandler interface.
091: */
092: public HttpCallbackHandler() {
093:
094: }
095:
096: /**
097: * constructor.
098: * @param request
099: * @param response
100: * @param authScheme
101: */
102: public HttpCallbackHandler(HttpServletRequest request,
103: HttpServletResponse response, String authScheme) {
104: this .httpRequest = request;
105: this .httpResponse = response;
106: this .authScheme = authScheme;
107:
108: }
109:
110: /**
111: * extract from the HttpServletRequest client's credentials.
112: * if those are not recognised, we put the challenge in the
113: * HttpServletResponse.
114: */
115: public void handle(Callback[] callbacks) throws IOException,
116: UnsupportedCallbackException {
117: boolean httpRelatedAuthScheme = false;
118: logger.debug("authSchemeItem=" + authScheme);
119: String[] schemes = authScheme.split(",");
120: List authSchemes = Arrays.asList(schemes);
121: Iterator itAutSchemes = authSchemes.iterator();
122: while (itAutSchemes.hasNext()) {
123: String scheme = (String) itAutSchemes.next();
124: //FORM, BASIC, and DIGEST are mutual exclusive
125: if (!httpRelatedAuthScheme
126: && HttpConstants.FORM_AUTH.equalsIgnoreCase(scheme)) {
127: grabFormCredentials(this .httpRequest, callbacks);
128: httpRelatedAuthScheme = true;
129: } else if (!httpRelatedAuthScheme
130: && HttpConstants.BASIC_AUTH
131: .equalsIgnoreCase(scheme)) {
132: grabBasicCredentials(this .httpRequest, callbacks);
133: httpRelatedAuthScheme = true;
134: } else if (!httpRelatedAuthScheme
135: && HttpConstants.DIGEST_AUTH
136: .equalsIgnoreCase(scheme)) {
137: grabDigestCredentials(this .httpRequest, callbacks);
138: httpRelatedAuthScheme = true;
139: }
140: //CLIENT_CERT can be used with another authentication mechanism
141: //defined above
142: if (HttpConstants.CLIENT_CERT_AUTH.equalsIgnoreCase(scheme)) {
143: boolean certificatesFound = grabClientCertCredentials(
144: this .httpRequest, callbacks);
145: if (!certificatesFound) {
146: logger.info(" X509 certificates are not found ");
147: }
148: }
149: }
150:
151: }
152:
153: public HttpServletRequest getHttpRequest() {
154: return httpRequest;
155: }
156:
157: public void setHttpRequest(HttpServletRequest httpRequest) {
158: this .httpRequest = httpRequest;
159: }
160:
161: public HttpServletResponse getHttpResponse() {
162: return httpResponse;
163: }
164:
165: public void setHttpResponse(HttpServletResponse httpResponse) {
166: this .httpResponse = httpResponse;
167: }
168:
169: /**
170: * construct a header value to simulate a Basic authentication with the provided credentials.
171: * @param login
172: * @param password
173: * @param encoding
174: * @return header
175: */
176: public static String buildBasicAuthHeader(String login,
177: String password, String encoding) {
178: if (encoding == null) {
179: encoding = HttpCallbackHandler.ISO_8859_1;
180: }
181: StringBuffer decodedString = new StringBuffer();
182: decodedString.append(login);
183: decodedString.append(" : ");
184: decodedString.append(password);
185: String encodedString;
186: try {
187: encodedString = new String(Base64.encode(decodedString
188: .toString().getBytes(encoding)));
189: } catch (UnsupportedEncodingException e) {
190: encodedString = new String(Base64.encode(decodedString
191: .toString().getBytes()));
192: }
193: StringBuffer header = new StringBuffer();
194: header.append(HttpCallbackHandler.BASIC);
195: header.append(encodedString);
196: header.append("==");
197: return header.toString();
198: }
199:
200: /**
201: * send to the client the BASIC challenge into the response, according to the RFC 2617.
202: * @param response reponse send to the Client
203: * @param realmName realm owned by the server => specify what kind of credential the user should provide
204: */
205: public static void buildBasicChallenge(
206: HttpServletResponse response, String realmName) {
207: StringBuffer responseValue = new StringBuffer();
208: responseValue.append(HttpCallbackHandler.BASIC_REALM);
209: responseValue.append(realmName);
210: responseValue.append("\"");
211: response.setHeader(HttpCallbackHandler.WWW_AUTHENTICATE,
212: responseValue.toString());
213: response.setHeader(HttpCallbackHandler.CACHE_CONTROL,
214: HttpCallbackHandler.NO_CACHE_AUTHORIZATION);
215: response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
216: }
217:
218: /**
219: * parse into the HttpServletRequest the user and password field,
220: * and authenticate the user with these credentials using the <b>NON SECURE</> BASIC method.
221: * @param request request send by the client.
222: * @param callbacks
223: * @return authentication's result. <i>true</i> for authentication success, <i>false</i> otherwise.
224: */
225: private boolean grabBasicCredentials(HttpServletRequest request,
226: Callback[] callbacks) {
227: boolean result = false;
228: String login = "";
229: String password = "";
230: //user and password are encoded in Base64
231: String encodedLoginAndPwd = request
232: .getHeader(HttpCallbackHandler.AUTHORIZATION);
233:
234: if (encodedLoginAndPwd == null || encodedLoginAndPwd.equals("")) {
235: login = SecurityConstants.GUEST;
236: password = SecurityConstants.GUEST;
237:
238: } else {
239: encodedLoginAndPwd = encodedLoginAndPwd.substring(6).trim();
240: String decodedLoginAndPassword = null;
241:
242: String encoding = request.getCharacterEncoding();
243: if (encoding == null) {
244: encoding = HttpCallbackHandler.ISO_8859_1;
245: }
246: logger.debug(encoding);
247:
248: try {
249: decodedLoginAndPassword = new String(Base64
250: .decode(encodedLoginAndPwd.getBytes()),
251: encoding);
252: } catch (UnsupportedEncodingException e) {
253: e.printStackTrace();
254: logger.error(" encoding " + encoding
255: + " is not supported by the platform ");
256: }
257:
258: String[] parts = decodedLoginAndPassword.split(":");
259: if (parts.length == 2) {
260: login = parts[0].trim();
261: password = parts[1].trim();
262:
263: result = true;
264: }
265: if ((login == "" && password == "") || (parts.length == 0)) {
266: login = SecurityConstants.GUEST;
267: password = SecurityConstants.GUEST;
268: }
269:
270: }
271:
272: fillBasicCredentials(callbacks, login, password);
273: return result;
274: }
275:
276: /**
277: * grab user credentials from request in the 'form' authentication metod.
278: * @param request request send by the client
279: * @param callbacks
280: * @return authentication result : <b>true</b> when authentication succeed,<b>false</b> when authentication fails.
281: */
282: private boolean grabFormCredentials(HttpServletRequest request,
283: Callback[] callbacks) {
284: boolean result = false;
285: HttpSession session = request.getSession();
286:
287: for (int i = 0; i < callbacks.length; i++) {
288: if (callbacks[i] instanceof NameCallback) {
289: NameCallback nc = (NameCallback) callbacks[i];
290: String login = httpRequest.getParameter(loginField);
291: nc.setName(login);
292: } else if (callbacks[i] instanceof PasswordCallback) {
293: PasswordCallback pc = (PasswordCallback) callbacks[i];
294: String strPwd = httpRequest.getParameter(passwordField);
295: if (strPwd != null && strPwd != "") {
296: pc.setPassword(strPwd.toCharArray());
297: } else {
298: pc.setPassword(null);
299: }
300: } else if (callbacks[i] instanceof JCaptchaCallback) {
301: JCaptchaCallback pc = (JCaptchaCallback) callbacks[i];
302: pc
303: .setCaptchaAnswer(httpRequest
304: .getParameter(SecurityConstants.CAPTCHA_ANSWER));
305: pc.setCaptchaService((CaptchaService) session
306: .getServletContext().getAttribute(
307: SecurityConstants.CAPTCHA_SERVICE));
308: Subject subject = ((HttpAuthenticationUtils) session
309: .getAttribute(HttpConstants.AUTHN_UTILS))
310: .getSubject();
311: if (subject == null || this .isAfterRegistration()) {
312: pc.setSkipJCaptchaChallenge(true);
313: }
314:
315: pc.setSessionID(session.getId());
316: }
317: }
318: result = true;
319:
320: return result;
321: }
322:
323: /**
324: * grab user credentials from request in the 'digest' authentication metod.
325: * @param request request send by the client
326: * @param callbacks
327: * @return authentication result : <b>true</b> when authentication succeed,<b>false</b> when authentication fails.
328: */
329: private boolean grabDigestCredentials(HttpServletRequest request,
330: Callback[] callbacks) {
331: boolean result = false;
332: String login = "";
333: String password = "";
334: //all users must be authenticated
335: //unless when the user send a wrong login or/and password
336: //=> he is redirected to the logonPage
337: if (login == null || password == null) {
338: login = SecurityConstants.GUEST;
339: password = SecurityConstants.GUEST;
340: } else {
341: //TODO implements digest authentication
342: result = true;
343: }
344: return result;
345: }
346:
347: /**
348: * grab user credentials from request in the 'clientCert' authentication metod.
349: * @param request
350: * @param callbacks
351: * @return <code>true</code> if successfull, <code>false</code> otherwise
352: */
353: private boolean grabClientCertCredentials(
354: HttpServletRequest request, Callback[] callbacks) {
355: if (!request.isSecure()) {
356: logger
357: .warn(" certificate-based authentication MUST be do in secure mode ");
358: logger
359: .warn(" but connection is do with the non secured protocol "
360: + request.getScheme());
361: return false;
362: }
363:
364: X509Certificate[] certificates = null;
365: javax.security.cert.X509Certificate[] oldCerts = null;
366: Object[] objects = (Object[]) request
367: .getAttribute(HttpCallbackHandler.JAVAX_SERVLET_REQUEST_X509CERTIFICATE);
368:
369: if (objects == null || objects.length == 0) {
370: return false;
371: }
372:
373: if (objects instanceof X509Certificate[]) {
374: certificates = (X509Certificate[]) objects;
375: //convert old X509 certificates into new X509 certificates
376: } else if (objects instanceof javax.security.cert.X509Certificate[]) {
377: oldCerts = (javax.security.cert.X509Certificate[]) objects;
378: List newCerts = null;
379: for (int i = 0; i < oldCerts.length; i++) {
380: newCerts = Arrays.asList(certificates);
381: newCerts.add(CertificateConverter
382: .convertOldToNew(oldCerts[i]));
383: }
384: certificates = (X509Certificate[]) newCerts.toArray();
385: } else {
386: logger
387: .warn(" X509certificates are needed but not provided by the client ");
388: return false;
389: }
390: fillCertCredentials(callbacks, certificates);
391:
392: return true;
393: }
394:
395: private void fillBasicCredentials(Callback[] callbacks,
396: String login, String password) {
397: for (int i = 0; i < callbacks.length; i++) {
398: if (callbacks[i] instanceof NameCallback) {
399: NameCallback nc = (NameCallback) callbacks[i];
400: nc.setName(login);
401:
402: } else if (callbacks[i] instanceof PasswordCallback) {
403: PasswordCallback pc = (PasswordCallback) callbacks[i];
404: pc.setPassword(password.toCharArray());
405: } else if (callbacks[i] instanceof JCaptchaCallback) {
406: JCaptchaCallback jc = (JCaptchaCallback) callbacks[i];
407: //we skip JCaptcha because we cannot provide
408: //CAPTCHA challenge through BASIC authentication
409: jc.setSkipJCaptchaChallenge(true);
410: }
411: }
412: }
413:
414: private void fillCertCredentials(Callback[] callbacks,
415: X509Certificate[] certificates) {
416: for (int i = 0; i < callbacks.length; i++) {
417: if (callbacks[i] instanceof CertificatesCallback) {
418: CertificatesCallback cc = (CertificatesCallback) callbacks[i];
419: cc.setCertificates(certificates);
420: break;
421: }
422: }
423: }
424:
425: public static void buildFormChallenge(FilterChain chain,
426: ServletRequest req, ServletResponse res)
427: throws IOException, ServletException {
428: chain.doFilter(req, res);
429: }
430:
431: /**
432: * send to the client the DIGEST challenge into the response, according to the RFC 2617.
433: * @param response reponse send to the Client
434: * @param token realm owned by the server => specify what kind of credential the user should provide
435: */
436: public static void buildDigestChallenge(HttpServletRequest request,
437: HttpServletResponse response, String realm) {
438: //TODO buildDigestChallenge method is not complete
439: StringBuffer responseValue = new StringBuffer();
440: //what about domain which defines the protection space?
441:
442: //realm
443: responseValue.append(HttpCallbackHandler.DIGEST_REALM);
444: responseValue.append(realm);
445: responseValue.append("\"");
446: responseValue.append(",");
447: //quality of protection qop
448: responseValue.append("qop=\"");
449: responseValue.append(getQop());
450: responseValue.append("\"");
451: responseValue.append(",");
452:
453: responseValue.append("nonce=\"");
454: responseValue.append(getNonce(request));
455: responseValue.append("\"");
456: responseValue.append(",");
457: //opaque
458: responseValue.append("opaque=");
459: responseValue.append("\"");
460: responseValue.append(getOpaque());
461: responseValue.append("\"");
462: //algorithm
463: responseValue.append("algorithm=");
464: responseValue.append("\"");
465: responseValue.append(getAlgorithm());
466: responseValue.append("\"");
467: //stale
468: responseValue.append("stale=");
469: responseValue.append("\"");
470: responseValue.append(getStale());
471: responseValue.append("\"");
472: response.setHeader(HttpCallbackHandler.WWW_AUTHENTICATE,
473: responseValue.toString());
474: response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
475: }
476:
477: /**
478: * A flag, indicating that the previous request from the client was
479: rejected because the nonce value was stale. If stale is TRUE
480: (case-insensitive), the client may wish to simply retry the request
481: with a new encrypted response, without reprompting the user for a
482: new username and password. The server should only set stale to TRUE
483: if it receives a request for which the nonce is invalid but with a
484: valid digest for that nonce (indicating that the client knows the
485: correct username/password). If stale is FALSE, or anything other
486: than TRUE, or the stale directive is not present, the username
487: and/or password are invalid, and new values must be obtained
488: * @return
489: */
490: private static String getStale() {
491: return "false";
492: }
493:
494: /**
495: * This directive is optional, but is made so only for backward
496: compatibility with RFC 2069 [6]; it SHOULD be used by all
497: implementations compliant with this version of the Digest scheme.
498: If present, it is a quoted string of one or more tokens indicating
499: the "quality of protection" values supported by the server. The
500: value "auth" indicates authentication; the value "auth-int"
501: indicates authentication with integrity protection; see the
502: descriptions below for calculating the response directive value for
503: the application of this choice. Unrecognized options MUST be
504: ignored.
505: * @return
506: */
507: private static String getQop() {
508: return "auth,auth-int";
509: }
510:
511: /**
512: * A string of data, specified by the server, which should be returned
513: by the client unchanged in the Authorization header of subsequent
514: requests with URIs in the same protection space. It is recommended
515: that this string be base64 or hexadecimal data.
516: * @return
517: */
518: private static String getOpaque() {
519: return "5ccc069c403ebaf9f0171e9517f40e41";
520: }
521:
522: /**
523: *
524: A string indicating a pair of algorithms used to produce the digest
525: and a checksum. If this is not present it is assumed to be "MD5".
526: If the algorithm is not understood, the challenge should be ignored
527: (and a different one used, if there is more than one).
528:
529: In this document the string obtained by applying the digest
530: algorithm to the data "data" with secret "secret" will be denoted
531: by KD(secret, data), and the string obtained by applying the
532: checksum algorithm to the data "data" will be denoted H(data). The
533: notation unq(X) means the value of the quoted-string X without the
534: surrounding quotes.
535:
536: For the "MD5" and "MD5-sess" algorithms
537:
538: H(data) = MD5(data)
539:
540: and
541:
542: KD(secret, data) = H(concat(secret, ":", data))
543:
544: i.e., the digest is the MD5 of the secret concatenated with a colon
545: concatenated with the data. The "MD5-sess" algorithm is intended to
546: allow efficient 3rd party authentication servers; for the
547: difference in usage, see the description in section 3.2.2.2.
548: * @return
549: */
550: private static String getAlgorithm() {
551: return "MD5";
552: }
553:
554: /**
555: * //nonce
556:
557: A server-specified data string which should be uniquely generated
558: each time a 401 response is made. It is recommended that this
559: string be base64 or hexadecimal data. Specifically, since the
560: string is passed in the header lines as a quoted string, the
561: double-quote character is not allowed.
562:
563: The contents of the nonce are implementation dependent. The quality
564: of the implementation depends on a good choice. A nonce might, for
565: example, be constructed as the base 64 encoding of
566:
567: time-stamp H(time-stamp ":" ETag ":" private-key)
568:
569: where time-stamp is a server-generated time or other non-repeating
570: value, ETag is the value of the HTTP ETag header associated with
571: the requested entity, and private-key is data known only to the
572: server. With a nonce of this form a server would recalculate the
573: hash portion after receiving the client authentication header and
574: reject the request if it did not match the nonce from that header
575: or if the time-stamp value is not recent enough. In this way the
576: server can limit the time of the nonce's validity. The inclusion of
577: the ETag prevents a replay request for an updated version of the
578: resource. (Note: including the IP address of the client in the
579: nonce would appear to offer the server the ability to limit the
580: reuse of the nonce to the same client that originally got it.
581: However, that would break proxy farms, where requests from a single
582: user often go through different proxies in the farm. Also, IP
583: address spoofing is not that hard.)
584:
585: An implementation might choose not to accept a previously used
586: nonce or a previously used digest, in order to protect against a
587: replay attack. Or, an implementation might choose to use one-time
588: nonces or digests for POST or PUT requests and a time-stamp for GET
589: requests. For more details on the issues involved see section 4.
590: of this document. The nonce is opaque to the client.
591: * @param request
592: * @return
593: */
594: private static String getNonce(HttpServletRequest request) {
595: return "dcd98b7102dd2f0e8b11d0f600bfb0c093";
596: }
597:
598: /**
599: * gets the HttpRequest password field
600: * @return password field
601: */
602: public static String getPasswordField() {
603: return passwordField;
604: }
605:
606: public static void setPasswordField(String passwordField) {
607: if (passwordField != null) {
608: HttpCallbackHandler.passwordField = passwordField;
609: }
610: }
611:
612: /**
613: * gets the HttpRequest login field
614: * @return login field
615: */
616: public static String getLoginField() {
617: return loginField;
618: }
619:
620: public static void setLoginField(String loginField) {
621: if (loginField != null) {
622: HttpCallbackHandler.loginField = loginField;
623: }
624: }
625:
626: public void setAfterRegistration(boolean afterRegistration) {
627: this .afterRegistration = afterRegistration;
628: }
629:
630: public boolean isAfterRegistration() {
631: return afterRegistration;
632: }
633: }
|