Get all HTTP requests with WebDriver/HtmlUnit - java

I need to verify a request for testing via WebDriver. Unfortunately there is no easy way to do this as there is no native support. It seems like I should be able to use HtmlUnit to get requests but I have only been able to get responses. Is there a way to do this with HtmlUnit or do I need to setup something else like Browsermob Proxy? I am using Java to do this.
Thanks!

I've provided an example using HtmlUnit below:
final WebClient webClient = new WebClient(BrowserVersion.CHROME);
final HtmlPage loginPage = webClient.getPage("http://www.stackoverflow.com");
WebResponse response = loginPage.getWebResponse(); // the response loaded to create this page
WebRequest request = response.getWebRequest(); // the request used to load this page

If I understand your question correctly you want to see every request and response that is made by HTMLUnit.
If you are using windows download Fiddler http://www.telerik.com/fiddler
Set the http proxy setting for HTMLUnit to use Fiddler as a proxy.
BrowserVersion bv = BrowserVersion.CHROME;
bv.setUserAgent("Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36");
webClient = new WebClient(bv, "127.0.0.1", 8888);
The above by itself will work for any site that does not use HTTPS
If you want to capture HTTPS traffic create the class below in your project
import java.security.AccessController;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivilegedAction;
import java.security.Security;
import java.security.cert.X509Certificate;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactorySpi;
import javax.net.ssl.X509TrustManager;
public final class XTrustProvider extends java.security.Provider
{
/**
*
*/
private static final long serialVersionUID = 1L;
private final static String NAME = "XTrustJSSE";
private final static String INFO = "XTrust JSSE Provider (implements trust factory with truststore validation disabled)";
private final static double VERSION = 1.0D;
#SuppressWarnings({ "unchecked", "rawtypes" })
public XTrustProvider()
{
super(NAME, VERSION, INFO);
AccessController.doPrivileged(new PrivilegedAction()
{
public Object run()
{
put("TrustManagerFactory." + TrustManagerFactoryImpl.getAlgorithm(), TrustManagerFactoryImpl.class.getName());
return null;
}
});
}
public static void install()
{
if (Security.getProvider(NAME) == null)
{
Security.insertProviderAt(new XTrustProvider(), 2);
Security.setProperty("ssl.TrustManagerFactory.algorithm", TrustManagerFactoryImpl.getAlgorithm());
}
}
public final static class TrustManagerFactoryImpl extends TrustManagerFactorySpi
{
public TrustManagerFactoryImpl()
{
}
public static String getAlgorithm()
{
return "XTrust509";
}
protected void engineInit(KeyStore keystore) throws KeyStoreException
{
}
protected void engineInit(ManagerFactoryParameters mgrparams) throws InvalidAlgorithmParameterException
{
throw new InvalidAlgorithmParameterException(XTrustProvider.NAME + " does not use ManagerFactoryParameters");
}
protected TrustManager[] engineGetTrustManagers()
{
return new TrustManager[]
{ new X509TrustManager()
{
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType)
{
}
public void checkServerTrusted(X509Certificate[] certs, String authType)
{
}
} };
}
}
}
Call the install method of the class
XTrustProvider.install();
Be sure to call the above method before HTMLUnit makes any http requests.
Now you will capture all the requests that are made by HTMLUnit including https requests.
If you run into any issues comment and I will help.

How about just something like this:
HtmlUnitDriver driver = new HtmlUnitDriver() {
public Set<WebRequest> requests = new HashSet<>();
#Override
protected WebClient modifyWebClient(WebClient originalClient) {
return new WebClient() {
#Override
public WebResponse getPage(WebWindow window, WebRequest request) {
requests.add(request);
return super.getPage(window, request)
}
#Override
public WebResponse loadWebResponse(WebRequest request) {
requests.add(request);
return super.loadWebResponse(request);
}
// If it's really necessary for your use case, you can also override the "download" method in a similar way, but note that this is an internal API
}
}
};
driver.open("http://www.example.com/");
Set<WebRequest> requests = (Set<WebRequest>) driver.getClass().getField("requests").get(driver);
for (WebRequest request : requests) {
System.out.println(request.getUrl().toString()); // or whatever you want
}
Of course, if the order is important, you could use a List instead of a Set, but then you'd have to check if the request was already there to avoid duplicates.

Related

how to force hostname when behind a chain of 2 revese proxy

An spring boot application is hosted behind 2 reverse proxy (chained).
reverse-proxy 1 --> reverse-proxy 2 --> spring boot app
And the host and forward headers are not chain correctly. there is a way to force the host to a fixed value? like the hostname of the "reverse proxy 1"?
i have fixed my issue by changing the serverName in incoming request.
i have add a valve to tomcat:
public class HostForceValve extends ValveBase {
private final String proxyName;
public HostForceValve(String proxyName) {
this.proxyName = proxyName;
}
#Override public void invoke(Request request, Response response) throws IOException, ServletException {
org.apache.coyote.Request coyoteRequest = request.getCoyoteRequest();
MimeHeaders mimeHeaders = coyoteRequest.getMimeHeaders();
mimeHeaders.removeHeader("host");
final MessageBytes host = mimeHeaders.addValue("host");
host.setString(proxyName);
request.setRemoteHost(proxyName);
request.getCoyoteRequest().serverName().setString(proxyName);
try {
Valve next = getNext();
if (null == next) {
return;
}
next.invoke(request, response);
} finally {
request.setRemoteHost(proxyName);
}
}
}
And add this value to the tomcat embedded server:
#Component
public class MyTomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Value("${proxyName:}")
private String proxyName;
#Override
public void customize(TomcatServletWebServerFactory factory) {
final Collection<Valve> currents = factory.getEngineValves();
final ArrayList<Valve> addValves = new ArrayList<>(currents);
if (StringUtils.hasLength(proxyName)) {
addValves.add(0, new HostForceValve(proxyName));
}
factory.setEngineValves(addValves);
}
}

Enable CORS for OPTIONS request using Spring Framework

Every time I make a PUT Ajax call to my service, it return the following error:
XMLHttpRequest cannot load http://localhost:8080/users/edit. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access. The response had HTTP status code 403.
After 2 days of investigation, I've reached to try the next solution on my code.
This is the main class where I load the necessary classes and run the application:
#SpringBootApplication
#EnableAutoConfiguration
public class Application extends SpringBootServletInitializer{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DispatcherServletInitializer.class, OptionsController.class,Application.class);
}
}
The DispatcherServilet initializer, where I enable the dispatchOptionsRequest:
public abstract class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("dispatchOptionsRequest", "true");
super.customizeRegistration(registration);
}
}
A controller for handle all OPTIONS request:
#Controller
public class OptionsController {
#RequestMapping(method = RequestMethod.OPTIONS)
public HttpServletResponse handle(HttpServletResponse theHttpServletResponse) throws IOException {
theHttpServletResponse.addHeader("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with");
theHttpServletResponse.addHeader("Access-Control-Max-Age", "60");
theHttpServletResponse.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
theHttpServletResponse.addHeader("Access-Control-Allow-Origin", "*");
return theHttpServletResponse;
}
}
What I'm doing wrong with the configuration?
Finally, the DispatcheServlet customize initializer was the class that really solved my problem. The OPTIONS request was failing because of the optionsController I had implemented, it was wrong.
So I removed that optionsController, and just by adding the handle method in my Rest Controller for the OPTIONS request, the problem was solved:
#CrossOrigin(origins = "*", maxAge = 3600)
#RestController
#RequestMapping("/users")
public class Users {
#RequestMapping(
value = "/edit",
method = RequestMethod.PUT)
public ResponseEntity<?> create(#RequestBody User user){
....
....
}
#RequestMapping(
value = "/**",
method = RequestMethod.OPTIONS
)
public ResponseEntity handle() {
return new ResponseEntity(HttpStatus.OK);
}
}
If you use a modern version of Spring (4.2) you can benefit of the #CrossOrigin.
Indeed if you use Spring < 4.2v you can create a Servlet Filter and put hear the header for CORS support like below:
package it.valeriovaudi.web.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
Copyright 2015 Valerio Vaudi
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.
*/
public class CORSFilter implements Filter {
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_NAME = "Access-Control-Allow-Origin";
public static final String DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE = "*";
public static final String ACCESS_CONTROL_ALLOW_METHDOS_NAME = "Access-Control-Allow-Methods";
public static final String DEFAULT_ACCESS_CONTROL_ALLOW_METHDOS_VALUE = "POST, GET, OPTIONS, DELETE";
public static final String ACCESS_CONTROL_MAX_AGE_NAME = "Access-Control-Max-Age";
public static final String DEFAULT_ACCESS_CONTROL_MAX_AGE_VALUE = "3600";
public static final String ACCESS_CONTROL_ALLOW_HEADERS_NAME = "Access-Control-Allow-Headers";
public static final String DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS_VALUE = "x-requested-with";
private String accessControlAllowOrigin = DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE;
private String accessControlAllowMethods = DEFAULT_ACCESS_CONTROL_ALLOW_METHDOS_VALUE;
private String accessControlAllowMaxAge = DEFAULT_ACCESS_CONTROL_MAX_AGE_VALUE;
private String accessControlAllowHeaders = DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS_VALUE;
/**
* #return the method return a map that associated the name of paramiters in the web.xml to the class variable name for the header binding*/
private Map<String,String> initConfig(){
Map<String, String> result = new HashMap<>();
result.put(ACCESS_CONTROL_ALLOW_ORIGIN_NAME,"accessControlAllowOrigin");
result.put(ACCESS_CONTROL_ALLOW_METHDOS_NAME,"accessControlAllowMethods");
result.put(ACCESS_CONTROL_MAX_AGE_NAME,"accessControlAllowMaxAge");
result.put(ACCESS_CONTROL_ALLOW_HEADERS_NAME,"accessControlAllowHeaders");
return result;
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
String initParameterValue;
Map<String, String> stringStringMap = initConfig();
for (Map.Entry<String, String> stringStringEntry : stringStringMap.entrySet()) {
initParameterValue = filterConfig.getInitParameter(stringStringEntry.getKey());
// if the init paramiter value isn't null then set the value in the correct http header
if(initParameterValue!=null){
try {
getClass().getDeclaredField(stringStringEntry.getValue()).set(this, initParameterValue);
} catch (IllegalAccessException | NoSuchFieldException ignored) { }
}
}
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_NAME, accessControlAllowOrigin);
response.setHeader(ACCESS_CONTROL_ALLOW_METHDOS_NAME, accessControlAllowMethods);
response.setHeader(ACCESS_CONTROL_MAX_AGE_NAME, accessControlAllowMaxAge);
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_NAME, accessControlAllowHeaders);
filterChain.doFilter(servletRequest, servletResponse);
}
#Override
public void destroy() {
}
}
in Spring boot you can register this filter as spring bean and Spring will register the filter for you.
I hope that this can help you.

How to access Jersey resource secured by #RolesAllowed

We were testing a REST webservice developed in jersey through postman rest client. It is a POST method and is annotated with #RolesAllowed. The full annotation the method is as follows:
#POST
#Path("/configuration")
#RolesAllowed("admin")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
When I requested this http://baseurl/configuration with the expected HTTP body content, I got 403 response(it is expected since it is allowed only for admin as it seems).
My doubt is how to access this service with the specified role via rest client.
So it seems like you set up the RolesAllowedDynamicFeature, but you have no authentication happening to set up the user and roles. What the RolesAllowedDynamicFeature does is lookup the SecurityContext, and calls the SecurityContext.isUserInRole(<"admin">) to see if the user in the SecurityContext has the role.
I imagine you don't know how the SecurityContext is set. There are a couple of ways. The first is through the servlet authentication mechanism. You can see more at Securing Web Applications from the Java EE tutorial.
Basically you need to set up a security realm or security domain on the server. Every server has it's own specific way of setting it up. You can see an example here or how it would be done with Tomcat.
Basically the realm/domain contains the users allowed to access the web app. Those users have associated roles. When the servlet container does the authentication, whether it be Basic authentication or Form authentication, it looks up the user from the credentials, and if the user is authenticated, the user and its roles are associated with the request. Jersey gathers this information and puts it into the SecurityContext for the request.
If this seems a bit complicated, an easier way to just forget the servlet container authentication and just create a Jersey filter, where you set the SecurityContext yourself. You can see an example here. You can use whatever authentication scheme you want. The important part is setting the SecurityContext with the user information, wherever you get it from, maybe a service that accesses a data store.
See Also:
securing rest services in Jersey
UPDATE
Here is a complete example of the second option using the filter. The test is run by Jersey Test Framework. You can run the test as is
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
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.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.DatatypeConverter;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.JerseyTest;
import static junit.framework.Assert.*;
import org.junit.Test;
public class BasicAuthenticationTest extends JerseyTest {
#Provider
#Priority(Priorities.AUTHENTICATION)
public static class BasicAuthFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(BasicAuthFilter.class.getName());
#Inject
private UserStore userStore;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String authentication = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authentication == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!authentication.startsWith("Basic ")) {
return;
}
authentication = authentication.substring("Basic ".length());
String[] values = new String(DatatypeConverter.parseBase64Binary(authentication),
Charset.forName("ASCII")).split(":");
if (values.length < 2) {
throw new WebApplicationException(400);
}
String username = values[0];
String password = values[1];
LOGGER.log(Level.INFO, "{0} - {1}", new Object[]{username, password});
User user = userStore.getUser(username);
if (user == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!user.password.equals(password)) {
throw new AuthenticationException("Authentication credentials are required");
}
requestContext.setSecurityContext(new MySecurityContext(user));
}
}
static class MySecurityContext implements SecurityContext {
private final User user;
public MySecurityContext(User user) {
this.user = user;
}
#Override
public Principal getUserPrincipal() {
return new Principal() {
#Override
public String getName() {
return user.username;
}
};
}
#Override
public boolean isUserInRole(String role) {
return role.equals(user.role);
}
#Override
public boolean isSecure() { return true; }
#Override
public String getAuthenticationScheme() {
return "Basic";
}
}
static class AuthenticationException extends WebApplicationException {
public AuthenticationException(String message) {
super(Response
.status(Status.UNAUTHORIZED)
.header("WWW-Authenticate", "Basic realm=\"" + "Dummy Realm" + "\"")
.type("text/plain")
.entity(message)
.build());
}
}
class User {
public final String username;
public final String role;
public final String password;
public User(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
}
class UserStore {
public final Map<String, User> users = new ConcurrentHashMap<>();
public UserStore() {
users.put("peeskillet", new User("peeskillet", "secret", "USER"));
users.put("stackoverflow", new User("stackoverflow", "superSecret", "ADMIN"));
}
public User getUser(String username) {
return users.get(username);
}
}
private static final String USER_RESPONSE = "Secured User Stuff";
private static final String ADMIN_RESPONSE = "Secured Admin Stuff";
private static final String USER_ADMIN_STUFF = "Secured User Admin Stuff";
#Path("secured")
public static class SecuredResource {
#GET
#Path("userSecured")
#RolesAllowed("USER")
public String getUser() {
return USER_RESPONSE;
}
#GET
#Path("adminSecured")
#RolesAllowed("ADMIN")
public String getAdmin() {
return ADMIN_RESPONSE;
}
#GET
#Path("userAdminSecured")
#RolesAllowed({"USER", "ADMIN"})
public String getUserAdmin() {
return USER_ADMIN_STUFF;
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(SecuredResource.class)
.register(BasicAuthFilter.class)
.register(RolesAllowedDynamicFeature.class)
.register(new AbstractBinder(){
#Override
protected void configure() {
bind(new UserStore()).to(UserStore.class);
}
});
}
static String getBasicAuthHeader(String username, String password) {
return "Basic " + Base64.encodeAsString(username + ":" + password);
}
#Test
public void should_return_403_with_unauthorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(USER_RESPONSE, response.readEntity(String.class));
}
#Test
public void should_return_403_with_unauthorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(ADMIN_RESPONSE, response.readEntity(String.class));
}
}
Here is the only dependency needed to run the test
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>

Spring RESTTemplate with HTTPS Authentication [duplicate]

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();
}
}
}

Fine-grained Authentication with RESTlet

I want to expose a resource using RESTlet with a fine-grained authentication. My ServerResource should be accessable via GET only for authenticated members (using BASIC Authentication). However, requests using POST should be available also for callers without any authentication.
In order to clearify:
http://path/myapp/user should allow anyone to register using POST, but only registered members should be able to GET a list of all users.
I'm unfortunately not much into RESTlet and I only find examples using coarser authentication for whole Restlets or Routers.
So how do I enable optional authentication for resources and check them on a per-method level?
Thanks in advance!
To do basic authentication in RESTlet 2.0 (I assume you're using 2.0 since you mention ServerResource), you need to use a ChallengeAuthenticator. If this is configured with optional = true then authentication will only be requested if you invoke ChallengeAuthenticator.challenge().
You can create your application with an authenticate() method, and call this whenever you need access to a resource to be secured:
Application:
package example;
import org.restlet.*;
import org.restlet.data.ChallengeScheme;
import org.restlet.routing.Router;
import org.restlet.security.*;
public class ExampleApp extends Application {
private ChallengeAuthenticator authenticatior;
private ChallengeAuthenticator createAuthenticator() {
Context context = getContext();
boolean optional = true;
ChallengeScheme challengeScheme = ChallengeScheme.HTTP_BASIC;
String realm = "Example site";
// MapVerifier isn't very secure; see docs for alternatives
MapVerifier verifier = new MapVerifier();
verifier.getLocalSecrets().put("user", "password".toCharArray());
ChallengeAuthenticator auth = new ChallengeAuthenticator(context, optional, challengeScheme, realm, verifier) {
#Override
protected boolean authenticate(Request request, Response response) {
if (request.getChallengeResponse() == null) {
return false;
} else {
return super.authenticate(request, response);
}
}
};
return auth;
}
#Override
public Restlet createInboundRoot() {
this.authenticatior = createAuthenticator();
Router router = new Router();
router.attach("/user", UserResource.class);
authenticatior.setNext(router);
return authenticatior;
}
public boolean authenticate(Request request, Response response) {
if (!request.getClientInfo().isAuthenticated()) {
authenticatior.challenge(response, false);
return false;
}
return true;
}
}
Resource:
package example;
import org.restlet.data.MediaType;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.resource.ServerResource;
public class UserResource extends ServerResource {
#Override
public Representation get() {
ExampleApp app = (ExampleApp) getApplication();
if (!app.authenticate(getRequest(), getResponse())) {
// Not authenticated
return new EmptyRepresentation();
}
// Generate list of users
// ...
}
#Override
public Representation post(Representation entity) {
// Handle post
// ...
}
}
I'm presently using Restlet v2.0.10.
The problem with ChallengeAuthenticator.isOptional() is that it's all or nothing. An alternative to the answer provided by #sea36 above is to override ChallengeAuthenticator.beforeHandle() to either perform authentication or skip it based on request method. For example, the resource below will only require authentication when the GET method is used.
Application:
package example;
import org.restlet.*;
import org.restlet.data.ChallengeScheme;
import org.restlet.routing.Router;
import org.restlet.security.ChallengeAuthenticator;
import org.restlet.security.MapVerifier;
public class ExampleApp extends Application {
private ChallengeAuthenticator createAuthenticator() {
Context context = getContext();
ChallengeScheme challengeScheme = ChallengeScheme.HTTP_BASIC;
String realm = "Example site";
// MapVerifier isn't very secure; see docs for alternatives
MapVerifier verifier = new MapVerifier();
verifier.getLocalSecrets().put("user", "password".toCharArray());
ChallengeAuthenticator authOnGet = new ChallengeAuthenticator(context, challengeScheme, realm) {
#Override
protected int beforeHandle(Request request, Response response) {
if (request.getMethod() == Method.GET)
return super.beforeHandle(request, response);
response.setStatus(Status.SUCCESS_OK);
return CONTINUE;
}
};
return authOnGet;
}
#Override
public Restlet createInboundRoot() {
ChallengeAuthenticator userResourceWithAuth = createAuthenticator();
userResourceWithAuth.setNext(UserResource.class);
Router router = new Router();
router.attach("/user", userResourceWithAuth);
return router;
}
}
Resource:
package example;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.representation.Representation;
import org.restlet.resource.ServerResource;
public class UserResource extends ServerResource {
#Get
public Representation listUsers() {
// retrieve list of users and generate response
// ...
}
#Post
public void register(Representation entity) {
// handle post
// ...
}
}
Note that this example applies the policy of authenticating on GET only to the UserResource and not other resources handled by the router.

Categories

Resources