/** * Copyright (C) 2015 Valkyrie RCP * * 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 org.valkyriercp.security.remoting; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpRequestInterceptor; import org.apache.http.auth.*; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.protocol.ClientContext; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor; import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean; import org.springframework.security.core.Authentication; import org.valkyriercp.security.AuthenticationAware; import java.io.IOException; /** * Extension of <code>HttpInvokerProxyFactoryBean</code> that supports the use of BASIC * authentication on each HTTP request while using commons-httpclient. * Commons-httpclient can be easily configured to use SSL (so the BASIC authentication isn't sniffable): * <code> * ProtocolSocketFactory authSSLProtocolSocketFactory = new AuthSSLProtocolSocketFactory(null, null, * truststoreUrl, TRUSTSTORE_PASSWORD); * Protocol.registerProtocol("https", new Protocol("https", authSSLProtocolSocketFactory, 443)); * </code> * <p> * This factory takes care of instantiating the proper invocation executor and keeping * it up to date with the latest user credentials. Once a more complete AOP implementation * is available, then this "token forwarding" can be removed as the default executor is * already wired to receive notifications when it is constructed by the application * context. * <p> * This configuration assumes that the user's credentials are "global" to the application * and every invocation should use the same credentials. If you need per-thread * authentication then you should look at using a combination of * {@link org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean} and * AuthenticationSimpleHttpInvokerRequestExecutor. * <p> * {@link AuthenticationAware} is implemented in order to get notifications of changes in * the user's credentials. Please see the class documentation for * <code>AuthenticationAware</code> to see how to configure the application context so * that authentication changes are broadcast properly. * <p> * @author Geoffrey De Smet * @author Larry Streepy */ public class BasicAuthCommonsHttpInvokerProxyFactoryBean extends HttpInvokerProxyFactoryBean implements AuthenticationAware { /** * Constructor. Install the default executor. */ public BasicAuthCommonsHttpInvokerProxyFactoryBean() { setHttpInvokerRequestExecutor(new HttpComponentsHttpInvokerRequestExecutor()); } /** * Handle a change in the current authentication token. * This method will fail fast if the executor isn't a CommonsHttpInvokerRequestExecutor. * @see org.valkyriercp.security.AuthenticationAware#setAuthenticationToken(org.springframework.security.core.Authentication) */ public void setAuthenticationToken(Authentication authentication) { if( logger.isDebugEnabled() ) { logger.debug("New authentication token: " + authentication); } HttpComponentsHttpInvokerRequestExecutor executor = (HttpComponentsHttpInvokerRequestExecutor) getHttpInvokerRequestExecutor(); DefaultHttpClient httpClient = (DefaultHttpClient) executor.getHttpClient(); BasicCredentialsProvider provider = new BasicCredentialsProvider(); httpClient.setCredentialsProvider(provider); httpClient.addRequestInterceptor(new PreemptiveAuthInterceptor()); UsernamePasswordCredentials usernamePasswordCredentials; if (authentication != null) { usernamePasswordCredentials = new UsernamePasswordCredentials( authentication.getName(), authentication.getCredentials().toString()); } else { usernamePasswordCredentials = null; } provider.setCredentials(AuthScope.ANY, usernamePasswordCredentials); } static class PreemptiveAuthInterceptor implements HttpRequestInterceptor { public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException { AuthState authState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE); if (authState.getAuthScheme() == null) { AuthScheme authScheme = (AuthScheme) context.getAttribute("preemptive-auth"); CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER); HttpHost targetHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); if (authScheme != null) { Credentials creds = credsProvider.getCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort())); if (creds == null) { throw new HttpException("No credentials for preemptive authentication"); } authState.setAuthScheme(authScheme); authState.setCredentials(creds); } } } } }