/*==========================================================================*\ | $Id: BasicAuthenticationFilter.java,v 1.5 2012/06/22 16:23:18 aallowat Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2011 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.core.http; import org.apache.http.HttpStatus; import org.eclipse.jgit.util.HttpSupport; import org.webcat.core.Application; import org.webcat.core.EOBase; import org.webcat.core.LoginSession; import org.webcat.core.Session; import org.webcat.core.User; import org.webcat.woextensions.ECActionWithResult; import static org.webcat.woextensions.ECActionWithResult.call; import com.Ostermiller.util.Base64; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOMessage; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.eocontrol.EOEditingContext; //------------------------------------------------------------------------- /** * <p> * A basic authentication filter that uses the Web-CAT user database to * authenticate users. The following authentication attempts are made: * </p> * <ol> * <li>The request's "X-Session-Id" header is examined to see if it contains a * valid Web-CAT login session identifier. If so, that session is used. (This * supports the external web API.)</li> * <li>If that header does not exist, the standard WebObjects session * restoration is attempted (lookup the session ID in the "wosid" cookie). If * found, that session is used. (This supports browser-based navigation.)</li> * <li>If the cookie does not exist, authentication is attempted using HTTP * basic authentication. If authentication succeeds, the user is logged in and * a session is created. If there are no credentials associated with the * request, a 401 status code is returned.</li> * </ol> * * @author Tony Allevato * @author Last changed by $Author: aallowat $ * @version $Revision: 1.5 $, $Date: 2012/06/22 16:23:18 $ */ public abstract class BasicAuthenticationFilter implements RequestFilter { //~ Methods ............................................................... // ---------------------------------------------------------- /** * Filters the request, only passing it further down the chain if * authentication was successful. * * @param request the request * @param response the response * @param filterChain the filter chain * @throws Exception if an error occurs */ public void filterRequest(WORequest request, WOResponse response, RequestFilterChain filterChain) throws Exception { existingSessionId = null; Session session = sessionFromContext(request.context()); try { if (session != null && isRequestValid(request) && userHasAccess(session.primeUser())) { filterChain.filterRequest(request, response); } else if (response.status() != HttpStatus.SC_UNAUTHORIZED) { generateAuthFailedResponse(response); } } finally { if (session != null) { Application.wcApplication().saveSessionForContext( request.context()); } } } // ---------------------------------------------------------- /** * Gets a value indicating whether the request is valid (usually based on * the part of the path that follows the request handler key). If the path * is invalid, the authentication filter will return a 403 Forbidden status * code. * * The default behavior is to return true for all requests; subclasses can * override this to more intelligently handle malformed paths. * * @param request the request * @return true if the request is valid, otherwise false */ protected boolean isRequestValid(WORequest request) { return true; } // ---------------------------------------------------------- /** * Gets the realm that should be displayed in the authentication dialog * used by the browser or client. This should be an informative string such * as "Web-CAT Git repository for [user]". * * @param context the request/response context * @return the realm */ protected abstract String realmForContext(WOContext context); // ---------------------------------------------------------- /** * Gets a value indicating whether the specified user has access to the * path being requested. * * @param user the user * @return if the user has access to the path, otherwise false */ protected abstract boolean userHasAccess(User user); // ---------------------------------------------------------- /** * Generates a response indicating that authentication failed. * * @param response the response to perform generation in */ private void generateAuthFailedResponse(WOResponse response) { response.appendContentString( "<html><body><h1>Authentication failed</h1></body></html>"); response.setStatus(WOMessage.HTTP_STATUS_FORBIDDEN); } // ---------------------------------------------------------- /** * Gets the session for the user making the request, creating it if it does * not yet exist (for example, if the user is logging in directly through * the Git URL). * * @param info the entity request info */ private Session sessionFromContext(final WOContext context) { final WORequest request = context.request(); final WOResponse response = context.response(); String sessionId = request.headerForKey(SESSION_ID_HEADER); if (sessionId == null) { sessionId = context._requestSessionID(); } Session session = (sessionId != null) ? // Use an existing session if we have one. (Session) Application.wcApplication() .restoreSessionWithID(sessionId, request.context()) : null; if (session == null) { String authorization = request.headerForKey( HttpSupport.HDR_AUTHORIZATION); if (authorization == null) { String realm = realmForContext(context); response.setStatus(HttpStatus.SC_UNAUTHORIZED); response.setHeader("Basic realm=\"" + realm + "\"", HttpSupport.HDR_WWW_AUTHENTICATE); } else { authorization = Base64.decode(authorization.substring(6)); String[] parts = authorization.split(":"); final String username = parts[0]; final String password = (parts.length > 1) ? parts[1] : null; final Session oldSession = session; session = call(new ECActionWithResult<Session>() { public Session action() { Session result = oldSession; User user = validateUser(username, password, ec); if (user == null) { Application.wcApplication().saveSessionForContext( context); } else { context._setRequestSessionID(existingSessionId); result = (Session) context.session(); result.setUser(user.localInstance( result.defaultEditingContext())); result._appendCookieToResponse(response); } return result; }}); } } return session; } // ---------------------------------------------------------- /** * Looks up a user with the specified username (in the format "username" or * "domain.username") and, if found, validates the user with the specified * password. * * @param username the username * @param password the password * @param session the session to associate with the user * @return true if the user was successfully authenticated, otherwise false */ private User validateUser(String username, String password, EOEditingContext ec) { // Look up the user based on the repository ID. User user = EOBase.objectWithApiId(ec, User.class, username); // Validate the user's password. if (user != null) { user = User.validate(user.userName(), password, user.authenticationDomain(), ec); } if (user != null) { LoginSession ls = LoginSession.getLoginSessionForUser(ec, user); if (ls != null) { // Remember the existing session id for restoration. existingSessionId = ls.sessionId(); } return user; } else { return null; } } //~ Static/instance variables ............................................. private static final String SESSION_ID_HEADER = "X-Session-Id"; private String existingSessionId; }