I maintain a productive Spring Boot Service (2.3.x), that connects to a remote Elastic Search cluster using the RestHighLevelClient from elasticsearch-rest-high-level-client (7.10.x) for searching.
This works like a charm, but now the remote service provider has secured its services with an OAuth2 proxy. This is ok for common REST endpoints, Spring provides everything you need: You just have to replace the standard Spring RestTemplate with the OAuth2RestTemplate from spring-security-oauth2. It also works fine with Kibana and a browser.
But the Elastic client is a hard nut to crack: I was not able to replace Elastic's RestClient with something that supports OAuth 2.0. Their code is really resistant against exchanging implementations. No interfaces, no layers of abstraction. Nevertheless I would like to keep Elastic's HighLevelClient, which is much more convenient than sending plain JSONs.
Has anybody successfully combined Elastic RestHighLevelClient with OAuth? Is there any compatible or alternative library?
Here's the Spring config I currently use:
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
#Configuration
public class ElasticSearchClientConfiguration {
// Elastic HighLevelClient currently used for searching, esp. msearch
#Bean
RestHighLevelClient restHighLevelClient() throws KeyManagementException, SSLException, NoSuchAlgorithmException {
return new RestHighLevelClient(createRestClientBuilder());
}
private RestClientBuilder createRestClientBuilder()
throws KeyManagementException, SSLException, NoSuchAlgorithmException {
// ... some more configs here
return RestClient.builder(new HttpHost(host, port, scheme)).setHttpClientConfigCallback(callback);
}
// Spring's OAuth2RestTemplate, used to call some other remote REST endpoints.
#Bean
public OAuth2RestTemplate oauth2RestTemplate() {
final ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setClientId(clientId);
resourceDetails.setClientSecret(loadSecret(clientSecretFileName));
resourceDetails.setScope(Collections.singletonList(resource));
resourceDetails.setAccessTokenUri(accessTokenUri);
final ClientCredentialsAccessTokenProvider tokenProvider = new ClientCredentialsAccessTokenProvider();
final OAuth2RestTemplate template = new OAuth2RestTemplate(resourceDetails);
template.setAccessTokenProvider(tokenProvider);
return template;
}
}
I finally found a solution that works. An Apache HttpRequestInterceptor can be used to append additional http headers.
import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
#Component
public class OAuthInterceptor implements HttpRequestInterceptor {
private static final String HEADER_AUTHORIZATION_KEY = "Authorization";
#Autowired
private OAuthTokenProvider tokenProvider; // this service fetches the token
#Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
if (!(request instanceof HttpRequestWrapper)) {
throw new HttpException("Unsupported request type: " + request.getClass());
}
final HttpRequestWrapper wrapper = (HttpRequestWrapper) request;
wrapper.addHeader(createAuthorizationHeader());
}
private Header createAuthorizationHeader() {
final String auth = tokenProvider.getAuthorization();
return new BasicHeader(HEADER_AUTHORIZATION_KEY, auth);
}
}
The interceptor can then be injected in the factory method:
#Bean
RestHighLevelClient restHighLevelClient(OAuthInterceptor oAuthInterceptor) throws KeyManagementException, SSLException, NoSuchAlgorithmException {
final CredentialsProvider credentialsProvider = createCredentialsProvider();
final RequestConfig config = RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setConnectionRequestTimeout(connectionRequestTimeout)
.setSocketTimeout(socketTimeout)
.build();
final HttpClientConfigCallback callback = new HttpClientConfigCallback() {
#Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder asyncClientBuilder) {
return asyncClientBuilder
.setDefaultCredentialsProvider(credentialsProvider)
.setDefaultRequestConfig(config)
.addInterceptorFirst(oAuthInterceptor); // That's the trick
}
};
final RestClientBuilder builder = RestClient.builder(new HttpHost(host, port, scheme)).setHttpClientConfigCallback(callback);
return new RestHighLevelClient(builder);
}
Related
I am having trouble with using the spring security saml extension with my spring boot application. We use ping federate and had a saml application set up. It was set up with a localhost:8443/saml/SSO url and another one for test inside my aws cluster. It looked something like this domain.com/saml/SSO.
My aws is an ECS Application Service fronted with a load balancer using port 443 only all the way through.
When I ran this locally the redirect was working great and no issues what so ever. I used this as a reference implementation https://github.com/vdenotaris/spring-boot-security-saml-sample/blob/master/src/main/java/com/vdenotaris/spring/boot/security/saml/web/config/WebSecurityConfig.java
Then when I deployed it to our aws instance it wouldn't work all of a sudden. It seemed to be getting an infinite amount of success redirects. When I looked at the SAML Trace it seems that the AssertionConsumerServiceURL is using the ECS Services ip address and port instead of the load blancers.
I then discovered that there is a configuration bean for handling the load balancer support. https://docs.spring.io/spring-security-saml/docs/current/reference/html/configuration-advanced.html
I tried implementing that and it seemed nothing changed. The assertionConsumerServiceUrl was still using the ip address and running an infinite redirect on success.
I am not sure if I am configuring the load balancer bean correctly or if there is something else I could do to figure this out.
Here is a sample of the WebSecurityConfig.java
package com.test.config;
import com.test.core.SAMLUserDetailsServiceImpl;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.velocity.app.VelocityEngine;
import org.camunda.bpm.webapp.impl.security.auth.ContainerBasedAuthenticationFilter;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.parse.ParserPool;
import org.opensaml.xml.parse.StaticBasicParserPool;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.saml.*;
import org.springframework.security.saml.context.SAMLContextProvider;
import org.springframework.security.saml.context.SAMLContextProviderImpl;
import org.springframework.security.saml.context.SAMLContextProviderLB;
import org.springframework.security.saml.key.JKSKeyManager;
import org.springframework.security.saml.key.KeyManager;
import org.springframework.security.saml.log.SAMLDefaultLogger;
import org.springframework.security.saml.metadata.*;
import org.springframework.security.saml.parser.ParserPoolHolder;
import org.springframework.security.saml.processor.*;
import org.springframework.security.saml.util.VelocityFactory;
import org.springframework.security.saml.websso.*;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.*;
#EnableWebSecurity
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 15)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements InitializingBean, DisposableBean {
private Timer backgroundTaskTimer;
private MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager;
public void init() {
this.backgroundTaskTimer = new Timer(true);
this.multiThreadedHttpConnectionManager = new MultiThreadedHttpConnectionManager();
}
public void shutdown() {
this.backgroundTaskTimer.purge();
this.backgroundTaskTimer.cancel();
this.multiThreadedHttpConnectionManager.shutdown();
}
#Value("${security.saml2.metadata-url}")
String metadataUrl;
#Value("${security.saml2.entity-id}")
String entityId;
#Value("${server.ssl.key-store}")
String keyStore;
#Value("${server.ssl.key-store-password}")
String keyStorePassword;
#Value("${server.ssl.key-alias}")
String keyAlias;
#Value("${server.ssl.key-password}")
String keyPassword;
#Autowired
private SAMLUserDetailsServiceImpl samlUserDetailsServiceImpl;
// Initialization of the velocity engine
#Bean
public VelocityEngine velocityEngine() {
return VelocityFactory.getEngine();
}
// XML parser pool needed for OpenSAML parsing
#Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}
#Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}
// Bindings, encoders and decoders used for creating and parsing messages
#Bean
public HttpClient httpClient() {
return new HttpClient(this.multiThreadedHttpConnectionManager);
}
// SAML Authentication Provider responsible for validating of received SAML
// messages
#Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
samlAuthenticationProvider.setUserDetails(samlUserDetailsServiceImpl);
samlAuthenticationProvider.setForcePrincipalAsString(false);
return samlAuthenticationProvider;
}
// Provider of default SAML Context
#Bean
public SAMLContextProviderImpl contextProvider() {
//return new SAMLContextProviderImpl();
SAMLContextProviderLB samlContextProviderLB = new SAMLContextProviderLB();
samlContextProviderLB.setServerName("mydomain.zone"); // SCRUBBED MY LOAD BALANCER
samlContextProviderLB.setScheme("https");
samlContextProviderLB.setServerPort(443);
samlContextProviderLB.setIncludeServerPortInRequestURL(true);
samlContextProviderLB.setContextPath("");
return samlContextProviderLB;
}
// Initialization of OpenSAML library
#Bean
public static SAMLBootstrap sAMLBootstrap() {
return new SAMLBootstrap();
}
// Logger for SAML messages and events
#Bean
public SAMLDefaultLogger samlLogger() {
return new SAMLDefaultLogger();
}
// SAML 2.0 WebSSO Assertion Consumer
#Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
return new WebSSOProfileConsumerImpl();
}
// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
#Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 Web SSO profile
#Bean
public WebSSOProfile webSSOprofile() {
return new WebSSOProfileImpl();
}
// SAML 2.0 Holder-of-Key Web SSO profile
#Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 ECP profile
#Bean
public WebSSOProfileECPImpl ecpprofile() {
return new WebSSOProfileECPImpl();
}
#Bean
public SingleLogoutProfile logoutprofile() {
return new SingleLogoutProfileImpl();
}
// Central storage of cryptographic keys
#Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader
.getResource(this.keyStore);
String storePass = this.keyStorePassword;
Map<String, String> passwords = new HashMap<String, String>();
passwords.put(this.keyAlias, this.keyPassword);
String defaultKey = this.keyAlias;
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}
#Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
return webSSOProfileOptions;
}
// Entry point to initialize authentication, default values taken from
// properties file
#Bean
public SAMLEntryPoint samlEntryPoint() {
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}
// Setup advanced info about metadata
#Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
extendedMetadata.setSigningAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
extendedMetadata.setSignMetadata(true);
extendedMetadata.setEcpEnabled(true);
return extendedMetadata;
}
// IDP Discovery Service
#Bean
public SAMLDiscovery samlIDPDiscovery() {
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
idpDiscovery.setIdpSelectionPath("/saml/discovery");
return idpDiscovery;
}
#Bean
#Qualifier("idp-ssocircle")
public ExtendedMetadataDelegate ssoCircleExtendedMetadataProvider()
throws MetadataProviderException {
String idpSSOCircleMetadataURL = this.metadataUrl;
HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(
this.backgroundTaskTimer, httpClient(), idpSSOCircleMetadataURL);
httpMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate =
new ExtendedMetadataDelegate(httpMetadataProvider, extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(true);
extendedMetadataDelegate.setMetadataRequireSignature(false);
backgroundTaskTimer.purge();
return extendedMetadataDelegate;
}
// IDP Metadata configuration - paths to metadata of IDPs in circle of trust
// is here
// Do no forget to call iniitalize method on providers
#Bean
#Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException {
List<MetadataProvider> providers = new ArrayList<MetadataProvider>();
providers.add(ssoCircleExtendedMetadataProvider());
return new CachingMetadataManager(providers);
}
// Filter automatically generates default SP metadata
#Bean
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId("WMProcessEngineUi-203871");
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager());
return metadataGenerator;
}
// The filter is waiting for connections on URL suffixed with filterSuffix
// and presents SP metadata there
#Bean
public MetadataDisplayFilter metadataDisplayFilter() {
return new MetadataDisplayFilter();
}
// Handler deciding where to redirect user after successful login
#Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl("/landing");
return successRedirectHandler;
}
// Handler deciding where to redirect user after failed login
#Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler =
new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/error");
return failureHandler;
}
#Bean
public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception {
SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOHoKProcessingFilter;
}
// Processing filter for WebSSO profile messages
#Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}
#Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}
// Handler for successful logout
#Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
successLogoutHandler.setDefaultTargetUrl("/");
return successLogoutHandler;
}
// Logout handler terminating local session
#Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler =
new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}
// Filter processing incoming logout messages
// First argument determines URL user will be redirected to after successful
// global logout
#Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(),
logoutHandler());
}
// Overrides default logout processing filter with the one processing SAML
// messages
#Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(),
new LogoutHandler[]{logoutHandler()},
new LogoutHandler[]{logoutHandler()});
}
// Bindings
private ArtifactResolutionProfile artifactResolutionProfile() {
final ArtifactResolutionProfileImpl artifactResolutionProfile =
new ArtifactResolutionProfileImpl(httpClient());
artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding()));
return artifactResolutionProfile;
}
#Bean
public HTTPArtifactBinding artifactBinding(ParserPool parserPool, VelocityEngine velocityEngine) {
return new HTTPArtifactBinding(parserPool, velocityEngine, artifactResolutionProfile());
}
#Bean
public HTTPSOAP11Binding soapBinding() {
return new HTTPSOAP11Binding(parserPool());
}
#Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), velocityEngine());
}
#Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}
#Bean
public HTTPSOAP11Binding httpSOAP11Binding() {
return new HTTPSOAP11Binding(parserPool());
}
#Bean
public HTTPPAOS11Binding httpPAOS11Binding() {
return new HTTPPAOS11Binding(parserPool());
}
// Processor
#Bean
public SAMLProcessorImpl processor() {
Collection<SAMLBinding> bindings = new ArrayList<SAMLBinding>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
bindings.add(artifactBinding(parserPool(), velocityEngine()));
bindings.add(httpSOAP11Binding());
bindings.add(httpPAOS11Binding());
return new SAMLProcessorImpl(bindings);
}
/**
* Define the security filter chain in order to support SSO Auth by using SAML 2.0
*
* #return Filter chain proxy
* #throws Exception
*/
#Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
metadataDisplayFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
samlWebSSOHoKProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
samlIDPDiscovery()));
return new FilterChainProxy(chains);
}
/**
* Returns the authentication manager currently used by Spring.
* It represents a bean definition with the aim allow wiring from
* other classes performing the Inversion of Control (IoC).
*
* #throws Exception
*/
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* Defines the web based security configuration.
*
* #param http It allows configuring web based security for specific http requests.
* #throws Exception
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
http
.httpBasic()
.authenticationEntryPoint(samlEntryPoint());
http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(samlFilter(), CsrfFilter.class);
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/saml/**").permitAll()
.antMatchers("/css/**").permitAll()
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.anyRequest().authenticated();
http
.logout()
.disable(); // The logout procedure is already handled by SAML filters.
}
/**
* Sets a custom authentication provider.
*
* #param auth SecurityBuilder used to create an AuthenticationManager.
* #throws Exception
*/
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(samlAuthenticationProvider());
}
#Override
public void afterPropertiesSet() throws Exception {
init();
}
#Override
public void destroy() throws Exception {
shutdown();
}
#Bean
public FilterRegistrationBean containerBasedAuthenticationFilter() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new ContainerBasedAuthenticationFilter());
filterRegistration.setInitParameters(Collections.singletonMap("authentication-provider", "com.example.webapp.SpringSecurityAuthenticationProvider")); // SCRUBBED
filterRegistration.setOrder(101); // make sure the filter is registered after the Spring Security Filter Chain
filterRegistration.addUrlPatterns("/app/*");
return filterRegistration;
}
}
Here is what the log looks like when it runs an infinite loop.
INFO: Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-03-22 15:21:44.590 INFO 8 --- [-nio-443-exec-7] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-03-22 15:21:44.600 INFO 8 --- [-nio-443-exec-7] o.s.web.servlet.DispatcherServlet : Completed initialization in 10 ms
2020-03-22 15:21:44.614 INFO 8 --- [-nio-443-exec-1] o.s.s.s.m.MetadataGeneratorFilter : No default metadata configured, generating with default values, please pre-configure metadata for production use
2020-03-22 15:21:44.615 WARN 8 --- [-nio-443-exec-1] o.s.s.s.m.MetadataGeneratorFilter : Generated default entity base URL https://WW.WWW.WWW.WWW:32827 based on values in the first server request. Please set property entityBaseURL on MetadataGenerator bean to fixate the value.
2020-03-22 15:21:44.767 INFO 8 --- [-nio-443-exec-1] o.s.s.s.m.MetadataGeneratorFilter : Created default metadata for system with entityID: WMProcessEngineUi-203871
2020-03-22 15:21:45.113 INFO 8 --- [-nio-443-exec-1] o.s.security.saml.log.SAMLDefaultLogger : AuthNRequest;SUCCESS;XX.XXX.XXX.XX;WMProcessEngineUi-203871;https://ssobeta.domain.com;;;
2020-03-22 15:21:45.113 INFO 8 --- [-nio-443-exec-7] o.s.security.saml.log.SAMLDefaultLogger : AuthNRequest;SUCCESS;YY.YYY.YYY.YYY;WMProcessEngineUi-203871;https://ssobeta.domain.com;;;
2020-03-22 15:21:45.113 INFO 8 --- [-nio-443-exec-9] o.s.security.saml.log.SAMLDefaultLogger : AuthNRequest;SUCCESS;ZZ.ZZZ.ZZZ.ZZZ;WMProcessEngineUi-203871;https://ssobeta.domain.com;;;
2020-03-22 15:21:59.611 INFO 8 --- [-nio-443-exec-3] o.s.security.saml.log.SAMLDefaultLogger : AuthNRequest;SUCCESS;ZZ.ZZZ.ZZZ.ZZZ;WMProcessEngineUi-203871;https://ssobeta.domain.com;;;
2020-03-22 15:21:59.661 INFO 8 --- [-nio-443-exec-5] o.s.security.saml.log.SAMLDefaultLogger : AuthNRequest;SUCCESS;YY.YYY.YYY.YYY;WMProcessEngineUi-203871;https://ssobeta.domain.com;;;
Rabbit config:
package com.rabbitMQ;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URI;
import java.net.URISyntaxException;
#EnableRabbit
#Configuration
public class RabbitMqConfig {
private static final Logger logger = LoggerFactory.getLogger(RabbitMqConfig.class);
#Value("${spring.rabbitmq.addresses}")
private String addressURL;
#Bean
public ConnectionFactory connectionFactory() throws URISyntaxException {
return new CachingConnectionFactory(new URI(addressURL));
}
/**
* Required for executing adminstration functions against an AMQP Broker
*/
#Bean
public AmqpAdmin amqpAdmin() throws URISyntaxException {
return new RabbitAdmin(connectionFactory());
}
#Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
#Bean
public AmqpTemplate rabbitTemplate() throws URISyntaxException {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setMessageConverter(jsonMessageConverter());
return rabbitTemplate;
}
}
Overview of application : Whenever an gitRepository is connected to our application, the repository name becomes the exchange name, in this case ForceCI, then each branchin that repository will create its own queue, here there are two queues develop and master . Now everytime a pull request gets created in develop branch I need to pass the information to develop queue and it should be listened by specific listener which should be registered only for develop. I saw examples for dynamic queues but I cannpt seem to find any examples on how to create dynamic listeners which will execute with different threads, how can I achieve this?
Also I am trying to send some messages to queue as test but I am not able to see them in console. (code below)
#RequestMapping(value = "/createExchange", method = RequestMethod.GET)
public void createExchange(ServletResponse response, ServletRequest
request) throws URISyntaxException {
rabbitMqConfig.amqpAdmin().declareExchange(new DirectExchange("ForceCI"));
}
#RequestMapping(value = "/createDynamicQueues", method = RequestMethod.GET)
public void createDynamicQueues(#RequestParam String branchName, ServletResponse response, ServletRequest
request) throws URISyntaxException {
Properties develop = rabbitMqConfig.amqpAdmin().getQueueProperties(branchName);
System.out.println("develop -> "+develop);
if(develop != null && develop.stringPropertyNames() != null && !develop.stringPropertyNames().isEmpty()) {
for (String stringPropertyName : develop.stringPropertyNames()) {
String property = develop.getProperty(stringPropertyName);
System.out.println("property Value -> " + property + " ---- " + "property key -> " + stringPropertyName);
}
} else {
Queue queue = new Queue(branchName, true);
String develop1 = rabbitMqConfig.amqpAdmin().declareQueue(new Queue(branchName, true));
rabbitMqConfig.amqpAdmin().declareBinding(BindingBuilder.bind(queue).to(new DirectExchange("ForceCI")).withQueueName());
System.out.println(develop1);
}
}
#RequestMapping(value = "/sendMessageToQueuesDevelop", method = RequestMethod.GET)
public void sendMessageToQueuesDevelop(ServletResponse response, ServletRequest
request) throws URISyntaxException {
Properties develop = rabbitMqConfig.amqpAdmin().getQueueProperties("develop");
String queue_name = develop.getProperty("QUEUE_NAME");
rabbitTemplate.convertAndSend("ForceCI", queue_name, "TestMessage");
}
#RequestMapping(value = "/sendMessageToQueuesMaster", method = RequestMethod.GET)
public void sendMessageToQueuesMaster(ServletResponse response, ServletRequest
request) throws URISyntaxException {
Properties develop = rabbitMqConfig.amqpAdmin().getQueueProperties("master");
String queue_name = develop.getProperty("QUEUE_NAME");
rabbitTemplate.convertAndSend("ForceCI", queue_name, "TestMessage1");
}
UPDATE
Binding was missing, when I gave binding as shown above in code, the messages started going in, but I still cant figure out how to listen these messages in different listeners and process them in different threads?
The simplest way is to use a DirectMessageListenerContainer and add queues to it as necessary. You won't get a new thread for each queue, though; with the direct container the listener is invoked on a thread from the amqp-client thread pool.
The direct container is efficient at adding queues; you can start with zero queues if needed. See Choosing a container for more information.
If you MUST have a new thread for each queue, you will have to manually create (and manage) a SimpleMessageListenerContainer for each.
I'm trying to build a JAX-RS Rest API with Jersey. I'm following the most up-voted answer from this thread: Best practice for REST token-based authentication with JAX-RS and Jersey
I got to the Identifying the current user part. I'm trying to use CDI.
Here is my main app class:
public class Main {
// Base URI the Grizzly HTTP server will listen on
public static final String BASE_URI = "http://localhost:8080/myapp/";
/**
* Starts Grizzly HTTP server exposing JAX-RS resources defined in this application.
* #return Grizzly HTTP server.
*/
public static HttpServer startServer() {
// create a resource config that scans for JAX-RS resources and providers
// in appServer package
final ResourceConfig rc = new ResourceConfig().packages("appServer");
rc.register(new CORSFilter());
rc.register(new AuthenticationFilter());
// create and start a new instance of grizzly http server
// exposing the Jersey application at BASE_URI
return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc, false);
}
/**
* Main method.
* #param args
* #throws IOException
*/
public static void main(String[] args) throws IOException {
final Weld weld = new Weld();
weld.initialize();
final HttpServer server = startServer();
server.start();
new SessionUtil().buildSession(args);
System.out.println(String.format("Jersey app started with WADL available at "
+ "%sapplication.wadl\nHit enter to stop it...", BASE_URI));
System.in.read();
server.stop();
weld.shutdown();
}
}
And the relevant filter class:
import appServer.AuthenticatedUser;
import appServer.Secured;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import javax.annotation.Priority;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Default;
import javax.inject.Inject;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
#Secured
#Provider
#Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "myRealm";
private static final String AUTHENTICATION_SCHEME = "Bearer";
public AuthenticationFilter() {
super();
}
#Inject
#AuthenticatedUser
Event<String> userAuthenticatedEvent;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
// if successful, fire event with token
userAuthenticatedEvent.fire(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
When I run the application it crashes with this error:
org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no
object available for injection at
SystemInjecteeImpl(requiredType=Event,parent=AuthenticationFilter,qualifiers={#javax.enterprise.inject.Default(),#appServer.AuthenticatedUser()},position=-1,optional=false,self=false,unqualified=null,997918120)
I have came across this question: How to use CDI Events in Java Jersey? but there was no relevant answer.
I have tried other solutions posted for similar problems but none of them worked.
So, it is obviously some sort of injection issue here:
#AuthenticatedUser
#Inject
Event<String> userAuthenticatedEvent;
Or maybe I'm not registering the Filter properly.
Any suggestions ?
Is there a way to add a query parameter to every HTTP request performed by RestTemplate in Spring?
The Atlassian API uses the query parameter os_authType to dictate the authentication method so I'd like to append ?os_authtype=basic to every request without specifying it all over my code.
Code
#Service
public class MyService {
private RestTemplate restTemplate;
#Autowired
public MyService(RestTemplateBuilder restTemplateBuilder,
#Value("${api.username}") final String username, #Value("${api.password}") final String password, #Value("${api.url}") final String url ) {
restTemplate = restTemplateBuilder
.basicAuthorization(username, password)
.rootUri(url)
.build();
}
public ResponseEntity<String> getApplicationData() {
ResponseEntity<String> response
= restTemplate.getForEntity("/demo?os_authType=basic", String.class);
return response;
}
}
You can write custom RequestInterceptor that implements ClientHttpRequestInterceptor
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
public class AtlassianAuthInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
// logic to check if request has query parameter else add it
return execution.execute(request, body);
}
}
Now we need to configure our RestTemplate to use it
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
#Configuration
public class MyAppConfig {
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());
restTemplate.setInterceptors(Collections.singletonList(new AtlassianAuthInterceptor()));
return restTemplate;
}
}
For the ones interested in logic to add a query parameter, as HttpRequest is immutable a wrapper class is needed.
class RequestWrapper {
private final HttpRequest original;
private final URI newUriWithParam;
...
public HttpMethod getMethod() { return this.original.method }
public URI getURI() { return newUriWithParam }
}
Then in your ClientHttpRequestInterceptor you can do something like
public ClientHttpResponse intercept(
request: HttpRequest,
body: ByteArray,
execution: ClientHttpRequestExecution
) {
URI uri = UriComponentsBuilder.fromUri(request.uri).queryParam("new-param", "param value").build().toUri();
return execution.execute(RequestWrapper(request, uri), body);
}
Update
Since spring 3.1 wrapper class org.springframework.http.client.support.HttpRequestWrapper is available in spring-web
I have 2 spring web apps that provide 2 separate set of services. Web App 1 has Spring Security implemented using a user-based authentication.
Now, Web App 2 needs to access the service of Web App 1. Normally, we would use the RestTemplate class to make requests to other web services.
How do we pass the authentication credentials in the request of Web App 2 to Web App 1
Here is a solution that works very well with Spring 3.1 and Apache HttpComponents 4.1 I created based various answers on this site and reading the spring RestTempalte source code. I am sharing in hopes of saving others time, I think spring should just have some code like this built in but it does not.
RestClient client = new RestClient();
client.setApplicationPath("someApp");
String url = client.login("theuser", "123456");
UserPortfolio portfolio = client.template().getForObject(client.apiUrl("portfolio"),
UserPortfolio.class);
Below is the Factory class which setups up the HttpComponents context to be the same on every request with the RestTemplate.
public class StatefullHttpComponentsClientHttpRequestFactory extends
HttpComponentsClientHttpRequestFactory
{
private final HttpContext httpContext;
public StatefullHttpComponentsClientHttpRequestFactory(HttpClient httpClient, HttpContext httpContext)
{
super(httpClient);
this.httpContext = httpContext;
}
#Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri)
{
return this.httpContext;
}
}
Below is Statefull Rest template that you can use to remember cookies, once you log in with it will remember the JSESSIONID and sent it on subsequent requests.
public class StatefullRestTemplate extends RestTemplate
{
private final HttpClient httpClient;
private final CookieStore cookieStore;
private final HttpContext httpContext;
private final StatefullHttpComponentsClientHttpRequestFactory statefullHttpComponentsClientHttpRequestFactory;
public StatefullRestTemplate()
{
super();
httpClient = new DefaultHttpClient();
cookieStore = new BasicCookieStore();
httpContext = new BasicHttpContext();
httpContext.setAttribute(ClientContext.COOKIE_STORE, getCookieStore());
statefullHttpComponentsClientHttpRequestFactory = new StatefullHttpComponentsClientHttpRequestFactory(httpClient, httpContext);
super.setRequestFactory(statefullHttpComponentsClientHttpRequestFactory);
}
public HttpClient getHttpClient()
{
return httpClient;
}
public CookieStore getCookieStore()
{
return cookieStore;
}
public HttpContext getHttpContext()
{
return httpContext;
}
public StatefullHttpComponentsClientHttpRequestFactory getStatefulHttpClientRequestFactory()
{
return statefullHttpComponentsClientHttpRequestFactory;
}
}
Here is a class to represent a rest client so that you can call into an app secured with spring
security.
public class RestClient
{
private String host = "localhost";
private String port = "8080";
private String applicationPath;
private String apiPath = "api";
private String loginPath = "j_spring_security_check";
private String logoutPath = "logout";
private final String usernameInputFieldName = "j_username";
private final String passwordInputFieldName = "j_password";
private final StatefullRestTemplate template = new StatefullRestTemplate();
/**
* This method logs into a service by doing an standard http using the configuration in this class.
*
* #param username
* the username to log into the application with
* #param password
* the password to log into the application with
*
* #return the url that the login redirects to
*/
public String login(String username, String password)
{
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add(usernameInputFieldName, username);
form.add(passwordInputFieldName, password);
URI location = this.template.postForLocation(loginUrl(), form);
return location.toString();
}
/**
* Logout by doing an http get on the logout url
*
* #return result of the get as ResponseEntity
*/
public ResponseEntity<String> logout()
{
return this.template.getForEntity(logoutUrl(), String.class);
}
public String applicationUrl(String relativePath)
{
return applicationUrl() + "/" + checkNotNull(relativePath);
}
public String apiUrl(String relativePath)
{
return applicationUrl(apiPath + "/" + checkNotNull(relativePath));
}
public StatefullRestTemplate template()
{
return template;
}
public String serverUrl()
{
return "http://" + host + ":" + port;
}
public String applicationUrl()
{
return serverUrl() + "/" + nullToEmpty(applicationPath);
}
public String loginUrl()
{
return applicationUrl(loginPath);
}
public String logoutUrl()
{
return applicationUrl(logoutPath);
}
public String apiUrl()
{
return applicationUrl(apiPath);
}
public void setLogoutPath(String logoutPath)
{
this.logoutPath = logoutPath;
}
public String getHost()
{
return host;
}
public void setHost(String host)
{
this.host = host;
}
public String getPort()
{
return port;
}
public void setPort(String port)
{
this.port = port;
}
public String getApplicationPath()
{
return applicationPath;
}
public void setApplicationPath(String contextPath)
{
this.applicationPath = contextPath;
}
public String getApiPath()
{
return apiPath;
}
public void setApiPath(String apiPath)
{
this.apiPath = apiPath;
}
public String getLoginPath()
{
return loginPath;
}
public void setLoginPath(String loginPath)
{
this.loginPath = loginPath;
}
public String getLogoutPath()
{
return logoutPath;
}
#Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("RestClient [\n serverUrl()=");
builder.append(serverUrl());
builder.append(", \n applicationUrl()=");
builder.append(applicationUrl());
builder.append(", \n loginUrl()=");
builder.append(loginUrl());
builder.append(", \n logoutUrl()=");
builder.append(logoutUrl());
builder.append(", \n apiUrl()=");
builder.append(apiUrl());
builder.append("\n]");
return builder.toString();
}
}
I was in the same situation. Here there is my solution.
Server - spring security config
<sec:http>
<sec:intercept-url pattern="/**" access="ROLE_USER" method="POST"/>
<sec:intercept-url pattern="/**" filters="none" method="GET"/>
<sec:http-basic />
</sec:http>
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider>
<sec:user-service>
<sec:user name="${rest.username}" password="${rest.password}" authorities="ROLE_USER"/>
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
Client side RestTemplate config
<bean id="httpClient" class="org.apache.commons.httpclient.HttpClient">
<constructor-arg ref="httpClientParams"/>
<property name="state" ref="httpState"/>
</bean>
<bean id="httpState" class="CustomHttpState">
<property name="credentials" ref="credentials"/>
</bean>
<bean id="credentials" class="org.apache.commons.httpclient.UsernamePasswordCredentials">
<constructor-arg value="${rest.username}"/>
<constructor-arg value="${rest.password}"/>
</bean>
<bean id="httpClientFactory" class="org.springframework.http.client.CommonsClientHttpRequestFactory">
<constructor-arg ref="httpClient"/>
</bean>
<bean class="org.springframework.web.client.RestTemplate">
<constructor-arg ref="httpClientFactory"/>
</bean>
Custom HttpState implementation
/**
* Custom implementation of {#link HttpState} with credentials property.
*
* #author banterCZ
*/
public class CustomHttpState extends HttpState {
/**
* Set credentials property.
*
* #param credentials
* #see #setCredentials(org.apache.commons.httpclient.auth.AuthScope, org.apache.commons.httpclient.Credentials)
*/
public void setCredentials(final Credentials credentials) {
super.setCredentials(AuthScope.ANY, credentials);
}
}
Maven dependency
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
The RestTemplate is very basic and limited; there doesn't seem to be an easy way to do this. The best way is probably to implement digest of basic auth in Web App 1. Then use Apache HttpClient directly to access the rest services from Web App 2.
That being said, for testing I was able to work around this with a big hack. Basically you use the RestTemplate to submit the login (j_spring_security_check), parse out the jsessionid from the request headers, then submit the rest request. Here's the code, but I doubt it's the best solution for production ready code.
public final class RESTTest {
public static void main(String[] args) {
RestTemplate rest = new RestTemplate();
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
#Override
public boolean verify(String s, SSLSession sslsession) {
return true;
}
});
// setting up a trust store with JCA is a whole other issue
// this assumes you can only log in via SSL
// you could turn that off, but not on a production site!
System.setProperty("javax.net.ssl.trustStore", "/path/to/cacerts");
System.setProperty("javax.net.ssl.trustStorePassword", "somepassword");
String jsessionid = rest.execute("https://localhost:8443/j_spring_security_check", HttpMethod.POST,
new RequestCallback() {
#Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
request.getBody().write("j_username=user&j_password=user".getBytes());
}
}, new ResponseExtractor<String>() {
#Override
public String extractData(ClientHttpResponse response) throws IOException {
List<String> cookies = response.getHeaders().get("Cookie");
// assuming only one cookie with jsessionid as the only value
if (cookies == null) {
cookies = response.getHeaders().get("Set-Cookie");
}
String cookie = cookies.get(cookies.size() - 1);
int start = cookie.indexOf('=');
int end = cookie.indexOf(';');
return cookie.substring(start + 1, end);
}
});
rest.put("http://localhost:8080/rest/program.json;jsessionid=" + jsessionid, new DAO("REST Test").asJSON());
}
}
Note for this to work, you need to create a trust store in JCA so the SSL connection can actually be made. I assume you don't want to have Spring Security's login be over plain HTTP for a production site since that would be a massive security hole.
There's a simple way to do this in case you are someone who's looking for a simple call and not a API consumer.
HttpClient client = new HttpClient();
client.getParams().setAuthenticationPreemptive(true);
Credentials defaultcreds = new UsernamePasswordCredentials("username", "password");
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new CommonsClientHttpRequestFactory(client));
client.getState().setCredentials(AuthScope.ANY, defaultcreds);
The following will authenticate and return the session cookie:
String sessionCookie= restTemplate.execute(uri, HttpMethod.POST, request -> {
request.getBody().write(("j_username=USER_NAME&j_password=PASSWORD").getBytes());
}, response -> {
AbstractClientHttpResponse r = (AbstractClientHttpResponse) response;
HttpHeaders headers = r.getHeaders();
return headers.get("Set-Cookie").get(0);
});
The currently authenticated user credentials should be available in Web App 1 on Authentication object, which is accessible through SecurityContext (for example, you can retrieve it by calling SecurityContextHolder.getContext().getAuthentication()).
After you retrieve the credentials, you can use them to access Web App 2.
You can pass "Authentiation" header with RestTemplate by either extending it with a decorator (as described here) or using RestTemplate.exchange() method, as described in this forum post.
This is very similar to ams's approach, except I've completely encapsulated the concern of maintaining the session cookie in the StatefulClientHttpRequestFactory. Also by decorating an existing ClientHttpRequestFactory with this behaviour, it can be used with any underlying ClientHttpRequestFactory and isn't bound to a specific implementation.
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.lang.String.format;
/**
* Decorates a ClientHttpRequestFactory to maintain sessions (cookies)
* to web servers.
*/
public class StatefulClientHttpRequestFactory implements ClientHttpRequestFactory {
protected final Log logger = LogFactory.getLog(this.getClass());
private final ClientHttpRequestFactory requestFactory;
private final Map<String, String> hostToCookie = new HashMap<>();
public StatefulClientHttpRequestFactory(ClientHttpRequestFactory requestFactory){
this.requestFactory = requestFactory;
}
#Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);
final String host = request.getURI().getHost();
String cookie = getCookie(host);
if(cookie != null){
logger.debug(format("Setting request Cookie header to [%s]", cookie));
request.getHeaders().set("Cookie", cookie);
}
//decorate the request with a callback to process 'Set-Cookie' when executed
return new CallbackClientHttpRequest(request, response -> {
List<String> responseCookie = response.getHeaders().get("Set-Cookie");
if(responseCookie != null){
setCookie(host, responseCookie.stream().collect(Collectors.joining("; ")));
}
return response;
});
}
private synchronized String getCookie(String host){
String cookie = hostToCookie.get(host);
return cookie;
}
private synchronized void setCookie(String host, String cookie){
hostToCookie.put(host, cookie);
}
private static class CallbackClientHttpRequest implements ClientHttpRequest{
private final ClientHttpRequest request;
private final Function<ClientHttpResponse, ClientHttpResponse> filter;
public CallbackClientHttpRequest(ClientHttpRequest request, Function<ClientHttpResponse, ClientHttpResponse> filter){
this.request = request;
this.filter = filter;
}
#Override
public ClientHttpResponse execute() throws IOException {
ClientHttpResponse response = request.execute();
return filter.apply(response);
}
#Override
public OutputStream getBody() throws IOException {
return request.getBody();
}
#Override
public HttpMethod getMethod() {
return request.getMethod();
}
#Override
public URI getURI() {
return request.getURI();
}
#Override
public HttpHeaders getHeaders() {
return request.getHeaders();
}
}
}