/** * Copyright 2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.roboconf.dm.rest.services.internal.filters; import static net.roboconf.dm.rest.services.cors.ResponseCorsFilter.CORS_REQ_HEADERS; import static net.roboconf.dm.rest.services.cors.ResponseCorsFilter.ORIGIN; import static net.roboconf.dm.rest.services.cors.ResponseCorsFilter.buildHeaders; import java.io.IOException; import java.util.Map; import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.roboconf.core.utils.Utils; import net.roboconf.dm.management.api.IPreferencesMngr; import net.roboconf.dm.rest.commons.UrlConstants; import net.roboconf.dm.rest.commons.security.AuthenticationManager; import net.roboconf.dm.rest.services.internal.ServletRegistrationComponent; import net.roboconf.dm.rest.services.internal.annotations.RestIndexer; import net.roboconf.dm.rest.services.internal.annotations.RestIndexer.RestOperationBean; import net.roboconf.dm.rest.services.internal.audit.AuditLogRecord; import net.roboconf.dm.rest.services.internal.resources.IAuthenticationResource; import net.roboconf.dm.rest.services.internal.resources.IPreferencesResource; /** * A filter to determine and request (if necessary) authentication. * <p> * This filter is registered as an OSGi service. PAX's web extender automatically * binds it to the web server (Karaf's Jetty). This filter is only applied to the * resources in this bundle, which means the REST API and the web socket. Other web * applications are not impacted. As an example, Karaf and Roboconf web administrations * are served by other bundles, this filter cannot be applied to them. * </p> * @author Vincent Zurczak - Linagora */ public class AuthenticationFilter implements Filter { static final String USER_AGENT = "User-Agent"; private final Logger logger = Logger.getLogger( getClass().getName()); private final RestIndexer restIndexer; private AuthenticationManager authenticationMngr; private boolean authenticationEnabled, enableCors; private long sessionPeriod; /** * Constructor. */ public AuthenticationFilter() { this.restIndexer = new RestIndexer(); } @Override public void doFilter( ServletRequest req, ServletResponse resp, FilterChain chain ) throws IOException, ServletException { if( ! this.authenticationEnabled ) { chain.doFilter( req, resp ); } else { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String requestedPath = request.getRequestURI(); String restVerb = request.getMethod(); // Find the session ID in the cookies String sessionId = null; Cookie[] cookies = request.getCookies(); if( cookies != null ) { for( Cookie cookie : cookies ) { if( UrlConstants.SESSION_ID.equals( cookie.getName())) { sessionId = cookie.getValue(); break; } } } // Audit audit( request, sessionId ); // Is there a valid session? boolean loggedIn = false; if( ! Utils.isEmptyOrWhitespaces( sessionId )) { loggedIn = this.authenticationMngr.isSessionValid( sessionId, this.sessionPeriod ); this.logger.finest( "Session " + sessionId + (loggedIn ? " was successfully " : " failed to be ") + "validated." ); } else { this.logger.finest( "No session ID was found in the cookie. Authentication cannot be performed." ); } // Valid session, go on. Send an error otherwise. // No redirection, we mainly deal with our web socket and REST API. // Exceptions: // * We want to reach the login API. // * We want to get the user language preference. // * We received an OPTIONS request. // POST requests with CORS are always preceded by an OPTIONS request. // OPTIONS requests never come with a cookie. So, we do not filter them. boolean loginRequest = requestedPath.endsWith( IAuthenticationResource.PATH + IAuthenticationResource.LOGIN_PATH ); boolean optionsRequest = "options".equalsIgnoreCase( restVerb ); boolean languagePreference = requestedPath.endsWith( IPreferencesResource.PATH ) && "get".equalsIgnoreCase( restVerb ) && ("key=" + IPreferencesMngr.USER_LANGUAGE).equals( request.getQueryString()); if( loggedIn || loginRequest || languagePreference || optionsRequest ) { chain.doFilter( request, response ); } else { // CORS? if( this.enableCors ) { Map<String,String> headers = buildHeaders( request.getHeader( CORS_REQ_HEADERS ), request.getHeader( ORIGIN )); for( Map.Entry<String,String> h : headers.entrySet()) response.setHeader( h.getKey(), h.getValue()); } // Send an error response.sendError( 403, "Authentication is required." ); } } } /** * @param request * @param sessionId */ private void audit( HttpServletRequest request, String sessionId ) { // Find the right method RestOperationBean rightBean = null; String restVerb = request.getMethod(); String uri = request.getRequestURI(); String queryString = request.getQueryString(); if( queryString != null ) uri += "?" + queryString; String path = cleanPath( uri ); for( RestOperationBean rmb : this.restIndexer.restMethods ) { if( path != null && path.matches( rmb.getUrlPattern()) && rmb.getRestVerb().equalsIgnoreCase( restVerb )) { rightBean = rmb; break; } } // TODO; check the permissions? // Audit String ipAddress = request.getRemoteAddr(); String userAgent = request.getHeader( USER_AGENT ); String user = this.authenticationMngr.findUsername( sessionId ); boolean authorized = user != null; if( rightBean != null ) this.logger.log( new AuditLogRecord( user, rightBean.getJerseyPath(), uri, restVerb, ipAddress, userAgent, authorized )); else this.logger.log( new AuditLogRecord( user, null, uri, restVerb, ipAddress, userAgent, authorized )); } @Override public void destroy() { // nothing } @Override public void init( FilterConfig filterConfig ) throws ServletException { // nothing } /** * @param authenticationEnabled the authenticationEnabled to set */ public void setAuthenticationEnabled( boolean authenticationEnabled ) { this.authenticationEnabled = authenticationEnabled; } /** * @param authenticationMngr the authenticationMngr to set */ public void setAuthenticationManager( AuthenticationManager authenticationMngr ) { this.authenticationMngr = authenticationMngr; } /** * @param sessionPeriod the sessionPeriod to set */ public void setSessionPeriod( long sessionPeriod ) { this.sessionPeriod = sessionPeriod; } /** * @param enableCors the enableCors to set */ public void setEnableCors( boolean enableCors ) { this.enableCors = enableCors; } /** * Cleans the path by removing the servlet paths and URL parameters. * @param path a non-null path * @return a non-null path */ static String cleanPath( String path ) { return path .replaceFirst( "^" + ServletRegistrationComponent.REST_CONTEXT + "/", "/" ) .replaceFirst( "^" + ServletRegistrationComponent.WEBSOCKET_CONTEXT + "/", "/" ) .replaceFirst( "\\?.*", "" ); } }