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.io.InputStream;
022: import java.security.Principal;
023: import java.util.Enumeration;
024: import java.util.Iterator;
025: import java.util.Locale;
026:
027: import javax.servlet.RequestDispatcher;
028: import javax.servlet.http.Cookie;
029: import javax.servlet.http.HttpServletResponse;
030:
031: import org.apache.catalina.Realm;
032: import org.apache.catalina.Session;
033: import org.apache.catalina.connector.Request;
034: import org.apache.catalina.connector.Response;
035: import org.apache.catalina.deploy.LoginConfig;
036: import org.apache.coyote.ActionCode;
037: import org.apache.juli.logging.Log;
038: import org.apache.juli.logging.LogFactory;
039: import org.apache.tomcat.util.buf.ByteChunk;
040: import org.apache.tomcat.util.buf.CharChunk;
041: import org.apache.tomcat.util.buf.MessageBytes;
042: import org.apache.tomcat.util.http.MimeHeaders;
043:
044: /**
045: * An <b>Authenticator</b> and <b>Valve</b> implementation of FORM BASED
046: * Authentication, as described in the Servlet API Specification, Version 2.2.
047: *
048: * @author Craig R. McClanahan
049: * @author Remy Maucherat
050: * @version $Revision: 536381 $ $Date: 2007-05-09 01:58:24 +0200 (mer., 09 mai 2007) $
051: */
052:
053: public class FormAuthenticator extends AuthenticatorBase {
054:
055: private static Log log = LogFactory.getLog(FormAuthenticator.class);
056:
057: // ----------------------------------------------------- Instance Variables
058:
059: /**
060: * Descriptive information about this implementation.
061: */
062: protected static final String info = "org.apache.catalina.authenticator.FormAuthenticator/1.0";
063:
064: /**
065: * Character encoding to use to read the username and password parameters
066: * from the request. If not set, the encoding of the request body will be
067: * used.
068: */
069: protected String characterEncoding = null;
070:
071: // ------------------------------------------------------------- Properties
072:
073: /**
074: * Return descriptive information about this Valve implementation.
075: */
076: public String getInfo() {
077:
078: return (info);
079:
080: }
081:
082: /**
083: * Return the character encoding to use to read the username and password.
084: */
085: public String getCharacterEncoding() {
086: return characterEncoding;
087: }
088:
089: /**
090: * Set the character encoding to be used to read the username and password.
091: */
092: public void setCharacterEncoding(String encoding) {
093: characterEncoding = encoding;
094: }
095:
096: // --------------------------------------------------------- Public Methods
097:
098: /**
099: * Authenticate the user making this request, based on the specified
100: * login configuration. Return <code>true</code> if any specified
101: * constraint has been satisfied, or <code>false</code> if we have
102: * created a response challenge already.
103: *
104: * @param request Request we are processing
105: * @param response Response we are creating
106: * @param config Login configuration describing how authentication
107: * should be performed
108: *
109: * @exception IOException if an input/output error occurs
110: */
111: public boolean authenticate(Request request, Response response,
112: LoginConfig config) throws IOException {
113:
114: // References to objects we will need later
115: Session session = null;
116:
117: // Have we already authenticated someone?
118: Principal principal = request.getUserPrincipal();
119: String ssoId = (String) request
120: .getNote(Constants.REQ_SSOID_NOTE);
121: if (principal != null) {
122: if (log.isDebugEnabled())
123: log.debug("Already authenticated '"
124: + principal.getName() + "'");
125: // Associate the session with any existing SSO session
126: if (ssoId != null)
127: associate(ssoId, request.getSessionInternal(true));
128: return (true);
129: }
130:
131: // Is there an SSO session against which we can try to reauthenticate?
132: if (ssoId != null) {
133: if (log.isDebugEnabled())
134: log.debug("SSO Id " + ssoId + " set; attempting "
135: + "reauthentication");
136: // Try to reauthenticate using data cached by SSO. If this fails,
137: // either the original SSO logon was of DIGEST or SSL (which
138: // we can't reauthenticate ourselves because there is no
139: // cached username and password), or the realm denied
140: // the user's reauthentication for some reason.
141: // In either case we have to prompt the user for a logon */
142: if (reauthenticateFromSSO(ssoId, request))
143: return true;
144: }
145:
146: // Have we authenticated this user before but have caching disabled?
147: if (!cache) {
148: session = request.getSessionInternal(true);
149: if (log.isDebugEnabled())
150: log.debug("Checking for reauthenticate in session "
151: + session);
152: String username = (String) session
153: .getNote(Constants.SESS_USERNAME_NOTE);
154: String password = (String) session
155: .getNote(Constants.SESS_PASSWORD_NOTE);
156: if ((username != null) && (password != null)) {
157: if (log.isDebugEnabled())
158: log.debug("Reauthenticating username '" + username
159: + "'");
160: principal = context.getRealm().authenticate(username,
161: password);
162: if (principal != null) {
163: session.setNote(Constants.FORM_PRINCIPAL_NOTE,
164: principal);
165: if (!matchRequest(request)) {
166: register(request, response, principal,
167: Constants.FORM_METHOD, username,
168: password);
169: return (true);
170: }
171: }
172: if (log.isDebugEnabled())
173: log
174: .debug("Reauthentication failed, proceed normally");
175: }
176: }
177:
178: // Is this the re-submit of the original request URI after successful
179: // authentication? If so, forward the *original* request instead.
180: if (matchRequest(request)) {
181: session = request.getSessionInternal(true);
182: if (log.isDebugEnabled())
183: log.debug("Restore request from session '"
184: + session.getIdInternal() + "'");
185: principal = (Principal) session
186: .getNote(Constants.FORM_PRINCIPAL_NOTE);
187: register(request, response, principal,
188: Constants.FORM_METHOD, (String) session
189: .getNote(Constants.SESS_USERNAME_NOTE),
190: (String) session
191: .getNote(Constants.SESS_PASSWORD_NOTE));
192: // If we're caching principals we no longer need the username
193: // and password in the session, so remove them
194: if (cache) {
195: session.removeNote(Constants.SESS_USERNAME_NOTE);
196: session.removeNote(Constants.SESS_PASSWORD_NOTE);
197: }
198: if (restoreRequest(request, session)) {
199: if (log.isDebugEnabled())
200: log.debug("Proceed to restored request");
201: return (true);
202: } else {
203: if (log.isDebugEnabled())
204: log.debug("Restore of original request failed");
205: response.sendError(HttpServletResponse.SC_BAD_REQUEST);
206: return (false);
207: }
208: }
209:
210: // Acquire references to objects we will need to evaluate
211: MessageBytes uriMB = MessageBytes.newInstance();
212: CharChunk uriCC = uriMB.getCharChunk();
213: uriCC.setLimit(-1);
214: String contextPath = request.getContextPath();
215: String requestURI = request.getDecodedRequestURI();
216: response.setContext(request.getContext());
217:
218: // Is this the action request from the login page?
219: boolean loginAction = requestURI.startsWith(contextPath)
220: && requestURI.endsWith(Constants.FORM_ACTION);
221:
222: // No -- Save this request and redirect to the form login page
223: if (!loginAction) {
224: session = request.getSessionInternal(true);
225: if (log.isDebugEnabled())
226: log.debug("Save request in session '"
227: + session.getIdInternal() + "'");
228: try {
229: saveRequest(request, session);
230: } catch (IOException ioe) {
231: log
232: .debug("Request body too big to save during authentication");
233: response.sendError(HttpServletResponse.SC_FORBIDDEN, sm
234: .getString("authenticator.requestBodyTooBig"));
235: return (false);
236: }
237: forwardToLoginPage(request, response, config);
238: return (false);
239: }
240:
241: // Yes -- Validate the specified credentials and redirect
242: // to the error page if they are not correct
243: Realm realm = context.getRealm();
244: if (characterEncoding != null) {
245: request.setCharacterEncoding(characterEncoding);
246: }
247: String username = request.getParameter(Constants.FORM_USERNAME);
248: String password = request.getParameter(Constants.FORM_PASSWORD);
249: if (log.isDebugEnabled())
250: log.debug("Authenticating username '" + username + "'");
251: principal = realm.authenticate(username, password);
252: if (principal == null) {
253: forwardToErrorPage(request, response, config);
254: return (false);
255: }
256:
257: if (log.isDebugEnabled())
258: log.debug("Authentication of '" + username
259: + "' was successful");
260:
261: if (session == null)
262: session = request.getSessionInternal(false);
263: if (session == null) {
264: if (containerLog.isDebugEnabled())
265: containerLog
266: .debug("User took so long to log on the session expired");
267: response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT,
268: sm.getString("authenticator.sessionExpired"));
269: return (false);
270: }
271:
272: // Save the authenticated Principal in our session
273: session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal);
274:
275: // Save the username and password as well
276: session.setNote(Constants.SESS_USERNAME_NOTE, username);
277: session.setNote(Constants.SESS_PASSWORD_NOTE, password);
278:
279: // Redirect the user to the original request URI (which will cause
280: // the original request to be restored)
281: requestURI = savedRequestURL(session);
282: if (log.isDebugEnabled())
283: log.debug("Redirecting to original '" + requestURI + "'");
284: if (requestURI == null)
285: response.sendError(HttpServletResponse.SC_BAD_REQUEST, sm
286: .getString("authenticator.formlogin"));
287: else
288: response.sendRedirect(response
289: .encodeRedirectURL(requestURI));
290: return (false);
291:
292: }
293:
294: // ------------------------------------------------------ Protected Methods
295:
296: /**
297: * Called to forward to the login page
298: *
299: * @param request Request we are processing
300: * @param response Response we are creating
301: * @param config Login configuration describing how authentication
302: * should be performed
303: */
304: protected void forwardToLoginPage(Request request,
305: Response response, LoginConfig config) {
306: RequestDispatcher disp = context.getServletContext()
307: .getRequestDispatcher(config.getLoginPage());
308: try {
309: disp.forward(request.getRequest(), response.getResponse());
310: response.finishResponse();
311: } catch (Throwable t) {
312: log.warn("Unexpected error forwarding to login page", t);
313: }
314: }
315:
316: /**
317: * Called to forward to the error page
318: *
319: * @param request Request we are processing
320: * @param response Response we are creating
321: * @param config Login configuration describing how authentication
322: * should be performed
323: */
324: protected void forwardToErrorPage(Request request,
325: Response response, LoginConfig config) {
326: RequestDispatcher disp = context.getServletContext()
327: .getRequestDispatcher(config.getErrorPage());
328: try {
329: disp.forward(request.getRequest(), response.getResponse());
330: } catch (Throwable t) {
331: log.warn("Unexpected error forwarding to error page", t);
332: }
333: }
334:
335: /**
336: * Does this request match the saved one (so that it must be the redirect
337: * we signalled after successful authentication?
338: *
339: * @param request The request to be verified
340: */
341: protected boolean matchRequest(Request request) {
342:
343: // Has a session been created?
344: Session session = request.getSessionInternal(false);
345: if (session == null)
346: return (false);
347:
348: // Is there a saved request?
349: SavedRequest sreq = (SavedRequest) session
350: .getNote(Constants.FORM_REQUEST_NOTE);
351: if (sreq == null)
352: return (false);
353:
354: // Is there a saved principal?
355: if (session.getNote(Constants.FORM_PRINCIPAL_NOTE) == null)
356: return (false);
357:
358: // Does the request URI match?
359: String requestURI = request.getRequestURI();
360: if (requestURI == null)
361: return (false);
362: return (requestURI.equals(sreq.getRequestURI()));
363:
364: }
365:
366: /**
367: * Restore the original request from information stored in our session.
368: * If the original request is no longer present (because the session
369: * timed out), return <code>false</code>; otherwise, return
370: * <code>true</code>.
371: *
372: * @param request The request to be restored
373: * @param session The session containing the saved information
374: */
375: protected boolean restoreRequest(Request request, Session session)
376: throws IOException {
377:
378: // Retrieve and remove the SavedRequest object from our session
379: SavedRequest saved = (SavedRequest) session
380: .getNote(Constants.FORM_REQUEST_NOTE);
381: session.removeNote(Constants.FORM_REQUEST_NOTE);
382: session.removeNote(Constants.FORM_PRINCIPAL_NOTE);
383: if (saved == null)
384: return (false);
385:
386: // Modify our current request to reflect the original one
387: request.clearCookies();
388: Iterator cookies = saved.getCookies();
389: while (cookies.hasNext()) {
390: request.addCookie((Cookie) cookies.next());
391: }
392:
393: MimeHeaders rmh = request.getCoyoteRequest().getMimeHeaders();
394: rmh.recycle();
395: Iterator names = saved.getHeaderNames();
396: while (names.hasNext()) {
397: String name = (String) names.next();
398: Iterator values = saved.getHeaderValues(name);
399: while (values.hasNext()) {
400: rmh.addValue(name).setString((String) values.next());
401: }
402: }
403:
404: request.clearLocales();
405: Iterator locales = saved.getLocales();
406: while (locales.hasNext()) {
407: request.addLocale((Locale) locales.next());
408: }
409:
410: request.getCoyoteRequest().getParameters().recycle();
411:
412: if ("POST".equalsIgnoreCase(saved.getMethod())) {
413: ByteChunk body = saved.getBody();
414:
415: if (body != null) {
416: request.getCoyoteRequest().action(
417: ActionCode.ACTION_REQ_SET_BODY_REPLAY, body);
418:
419: // Set content type
420: MessageBytes contentType = MessageBytes.newInstance();
421:
422: //If no content type specified, use default for POST
423: String savedContentType = saved.getContentType();
424: if (savedContentType == null) {
425: savedContentType = "application/x-www-form-urlencoded";
426: }
427:
428: contentType.setString(savedContentType);
429: request.getCoyoteRequest().setContentType(contentType);
430: }
431: }
432: request.getCoyoteRequest().method()
433: .setString(saved.getMethod());
434:
435: request.getCoyoteRequest().queryString().setString(
436: saved.getQueryString());
437:
438: request.getCoyoteRequest().requestURI().setString(
439: saved.getRequestURI());
440: return (true);
441:
442: }
443:
444: /**
445: * Save the original request information into our session.
446: *
447: * @param request The request to be saved
448: * @param session The session to contain the saved information
449: * @throws IOException
450: */
451: protected void saveRequest(Request request, Session session)
452: throws IOException {
453:
454: // Create and populate a SavedRequest object for this request
455: SavedRequest saved = new SavedRequest();
456: Cookie cookies[] = request.getCookies();
457: if (cookies != null) {
458: for (int i = 0; i < cookies.length; i++)
459: saved.addCookie(cookies[i]);
460: }
461: Enumeration names = request.getHeaderNames();
462: while (names.hasMoreElements()) {
463: String name = (String) names.nextElement();
464: Enumeration values = request.getHeaders(name);
465: while (values.hasMoreElements()) {
466: String value = (String) values.nextElement();
467: saved.addHeader(name, value);
468: }
469: }
470: Enumeration locales = request.getLocales();
471: while (locales.hasMoreElements()) {
472: Locale locale = (Locale) locales.nextElement();
473: saved.addLocale(locale);
474: }
475:
476: if ("POST".equalsIgnoreCase(request.getMethod())) {
477: ByteChunk body = new ByteChunk();
478: body.setLimit(request.getConnector().getMaxSavePostSize());
479:
480: byte[] buffer = new byte[4096];
481: int bytesRead;
482: InputStream is = request.getInputStream();
483:
484: while ((bytesRead = is.read(buffer)) >= 0) {
485: body.append(buffer, 0, bytesRead);
486: }
487: saved.setContentType(request.getContentType());
488: saved.setBody(body);
489: }
490:
491: saved.setMethod(request.getMethod());
492: saved.setQueryString(request.getQueryString());
493: saved.setRequestURI(request.getRequestURI());
494:
495: // Stash the SavedRequest in our session for later use
496: session.setNote(Constants.FORM_REQUEST_NOTE, saved);
497:
498: }
499:
500: /**
501: * Return the request URI (with the corresponding query string, if any)
502: * from the saved request so that we can redirect to it.
503: *
504: * @param session Our current session
505: */
506: protected String savedRequestURL(Session session) {
507:
508: SavedRequest saved = (SavedRequest) session
509: .getNote(Constants.FORM_REQUEST_NOTE);
510: if (saved == null)
511: return (null);
512: StringBuffer sb = new StringBuffer(saved.getRequestURI());
513: if (saved.getQueryString() != null) {
514: sb.append('?');
515: sb.append(saved.getQueryString());
516: }
517: return (sb.toString());
518:
519: }
520:
521: }
|