Enable CORS for OPTIONS request using Spring Framework - java

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.

Related

How to set SameSite and Secure attribute to JSESSIONID cookie

I have a Spring Boot Web Application (Spring boot version 2.0.3.RELEASE) and running in an Apache Tomcat 8.5.5 server.
With the recent security policy which has imposed by Google Chrome (Rolled out since 80.0), it is requested to apply the new SameSite attribute to make the Cross-site cookie access in a more secure way instead of the CSRF. As I have done nothing related that and Chrome has set default value SameSite=Lax for the first-party cookies, one of my third-party service integration is failing due to the reason that chrome is restricting access of cross-site cookies when SameSite=Lax and if the third party response is coming from a POST request (Once the procedure completes third-party service redirect to our site with a POST request). in there Tomcat unable to find the session so it appends a new JSESSIONID (with a new session and the previous session was killed) at the end of the URL. So Spring rejects the URL as it contains a semicolon which was introduced by the new JSESSIONID append.
So I need to change the JSESSIONID cookie attributes(SameSite=None; Secure) and tried it in several ways including WebFilters.I have seen the same question and answers in Stackoverflow and tried most of them but ended up in nowhere.
can someone come up with a solution to change those headers in Spring Boot, please?
UPDATE on 06/07/2021 - Added correct Path attribute with new sameSite attributes to avoid session cookie duplication with GenericFilterBean approach.
I was able to come up with my own solution for this.
I have two kinds of applications which run on Spring boot which has different Spring security configurations and they needed different solutions to fix this.
CASE 1: No user authentication
Solution 1
In here you might have created an endpoint for the 3rd party response, in your application. You are safe until you access httpSession in a controller method. If you are accessing session in different controller method then send a temporary redirect request to there like follows.
#Controller
public class ThirdPartyResponseController{
#RequestMapping(value=3rd_party_response_URL, method=RequestMethod.POST)
public void thirdPartyresponse(HttpServletRequest request, HttpServletResponse httpServletResponse){
// your logic
// and you can set any data as an session attribute which you want to access over the 2nd controller
request.getSession().setAttribute(<data>)
try {
httpServletResponse.sendRedirect(<redirect_URL>);
} catch (IOException e) {
// handle error
}
}
#RequestMapping(value=redirect_URL, method=RequestMethod.GET)
public String thirdPartyresponse(HttpServletRequest request, HttpServletResponse httpServletResponse, Model model, RedirectAttributes redirectAttributes, HttpSession session){
// your logic
return <to_view>;
}
}
Still, you need to allow the 3rd_party_response_url in your security configuration.
Solution 2
You can try the same GenericFilterBean approach described below.
Case 2: Users need to be authenticated/sign in
In a Spring Web application where you have configured most of your security rules either through HttpSecurity or WebSecurity, check this solution.
Sample security config which I have tested the solution:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
......
..antMatchers(<3rd_party_response_URL>).permitAll();
.....
..csrf().ignoringAntMatchers(<3rd_party_response_URL>);
}
}
The Important points which I want to highlight in this configuration are you should allow the 3rd party response URL from Spring Security and CSRF protection(if it's enabled).
Then we need to create a HttpServletRequest Filter by extending GenericFilterBean class (Filter class did not work for me) and setting the SameSite Attributes to the JSESSIONID cookie by intercepting each HttpServletRequest and setting the response headers.
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class SessionCookieFilter extends GenericFilterBean {
private final List<String> PATHS_TO_IGNORE_SETTING_SAMESITE = Arrays.asList("resources", <add other paths you want to exclude>);
private final String SESSION_COOKIE_NAME = "JSESSIONID";
private final String SESSION_PATH_ATTRIBUTE = ";Path=";
private final String ROOT_CONTEXT = "/";
private final String SAME_SITE_ATTRIBUTE_VALUES = ";HttpOnly;Secure;SameSite=None";
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String requestUrl = req.getRequestURL().toString();
boolean isResourceRequest = requestUrl != null ? StringUtils.isNoneBlank(PATHS_TO_IGNORE_SETTING_SAMESITE.stream().filter(s -> requestUrl.contains(s)).findFirst().orElse(null)) : null;
if (!isResourceRequest) {
Cookie[] cookies = ((HttpServletRequest) request).getCookies();
if (cookies != null && cookies.length > 0) {
List<Cookie> cookieList = Arrays.asList(cookies);
Cookie sessionCookie = cookieList.stream().filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())).findFirst().orElse(null);
if (sessionCookie != null) {
String contextPath = request.getServletContext() != null && StringUtils.isNotBlank(request.getServletContext().getContextPath()) ? request.getServletContext().getContextPath() : ROOT_CONTEXT;
resp.setHeader(HttpHeaders.SET_COOKIE, sessionCookie.getName() + "=" + sessionCookie.getValue() + SESSION_PATH_ATTRIBUTE + contextPath + SAME_SITE_ATTRIBUTE_VALUES);
}
}
}
chain.doFilter(request, response);
}
}
Then add this filter to the Spring Security filter chain by
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
....
.addFilterAfter(new SessionCookieFilter(), BasicAuthenticationFilter.class);
}
in order to determine where you need to place the new filter in Spring’s security filter chain, you can debug the Spring security filter chain easily and identify a proper location in the filter chain. Apart from the BasicAuthenticationFilter, after the SecurityContextPersistanceFilter would be an another ideal place.
This SameSite cookie attribute will not support some old browser versions and in that case, check the browser and avoid setting SameSite in incompatible clients.
private static final String _I_PHONE_IOS_12 = "iPhone OS 12_";
private static final String _I_PAD_IOS_12 = "iPad; CPU OS 12_";
private static final String _MAC_OS_10_14 = " OS X 10_14_";
private static final String _VERSION = "Version/";
private static final String _SAFARI = "Safari";
private static final String _EMBED_SAFARI = "(KHTML, like Gecko)";
private static final String _CHROME = "Chrome/";
private static final String _CHROMIUM = "Chromium/";
private static final String _UC_BROWSER = "UCBrowser/";
private static final String _ANDROID = "Android";
/*
* checks SameSite=None;Secure incompatible Browsers
* https://www.chromium.org/updates/same-site/incompatible-clients
*/
public static boolean isSameSiteInCompatibleClient(HttpServletRequest request) {
String userAgent = request.getHeader("user-agent");
if (StringUtils.isNotBlank(userAgent)) {
boolean isIos12 = isIos12(userAgent), isMacOs1014 = isMacOs1014(userAgent), isChromeChromium51To66 = isChromeChromium51To66(userAgent), isUcBrowser = isUcBrowser(userAgent);
//TODO : Added for testing purpose. remove before Prod release.
LOG.info("*********************************************************************************");
LOG.info("is iOS 12 = {}, is MacOs 10.14 = {}, is Chrome 51-66 = {}, is Android UC Browser = {}", isIos12, isMacOs1014, isChromeChromium51To66, isUcBrowser);
LOG.info("*********************************************************************************");
return isIos12 || isMacOs1014 || isChromeChromium51To66 || isUcBrowser;
}
return false;
}
private static boolean isIos12(String userAgent) {
return StringUtils.contains(userAgent, _I_PHONE_IOS_12) || StringUtils.contains(userAgent, _I_PAD_IOS_12);
}
private static boolean isMacOs1014(String userAgent) {
return StringUtils.contains(userAgent, _MAC_OS_10_14)
&& ((StringUtils.contains(userAgent, _VERSION) && StringUtils.contains(userAgent, _SAFARI)) //Safari on MacOS 10.14
|| StringUtils.contains(userAgent, _EMBED_SAFARI)); // Embedded browser on MacOS 10.14
}
private static boolean isChromeChromium51To66(String userAgent) {
boolean isChrome = StringUtils.contains(userAgent, _CHROME), isChromium = StringUtils.contains(userAgent, _CHROMIUM);
if (isChrome || isChromium) {
int version = isChrome ? Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROME).substring(0, 2))
: Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROMIUM).substring(0, 2));
return ((version >= 51) && (version <= 66)); //Chrome or Chromium V51-66
}
return false;
}
private static boolean isUcBrowser(String userAgent) {
if (StringUtils.contains(userAgent, _UC_BROWSER) && StringUtils.contains(userAgent, _ANDROID)) {
String[] version = StringUtils.splitByWholeSeparator(StringUtils.substringAfter(userAgent, _UC_BROWSER).substring(0, 7), ".");
int major = Integer.valueOf(version[0]), minor = Integer.valueOf(version[1]), build = Integer.valueOf(version[2]);
return ((major != 0) && ((major < 12) || (major == 12 && (minor < 13)) || (major == 12 && minor == 13 && (build < 2)))); //UC browser below v12.13.2 in android
}
return false;
}
Add above check in SessionCookieFilter like follows,
if (!isResourceRequest && !UserAgentUtils.isSameSiteInCompatibleClient(req)) {
This filter won't work in localhost environments as it requires a Secured(HTTPS) connection to set Secure cookie attribute.
For a detailed explanation read this blog post.
I was in same situation earlier. Since there is nothing like SameSite in javax.servlet.http.Cookie class so it's not possible to add that.
Part 1: So what I did is wrote a filter which intercepts the required third party request only.
public class CustomFilter implements Filter {
private static final String THIRD_PARTY_URI = "/third/party/uri";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if(THIRD_PARTY_URI.equals(request.getRequestURI())) {
chain.doFilter(request, new CustomHttpServletResponseWrapper(response));
} else {
chain.doFilter(request, response);
}
}
enter code here
// ... init destroy methods here
}
Part 2: Cookies are sent as Set-Cookie response header. So this CustomHttpServletResponseWrapper overrides the addCookie method and check, if it is the required cookie (JSESSIONID), instead of adding it to cookie, it adds directly to response header Set-Cookie with SameSite=None attribute.
public class CustomHttpServletResponseWrapper extends HttpServletResponseWrapper {
public CustomHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
}
#Override
public void addCookie(Cookie cookie) {
if ("JSESSIONID".equals(cookie.getName())) {
super.addHeader("Set-Cookie", getCookieValue(cookie));
} else {
super.addCookie(cookie);
}
}
private String getCookieValue(Cookie cookie) {
StringBuilder builder = new StringBuilder();
builder.append(cookie.getName()).append('=').append(cookie.getValue());
builder.append(";Path=").append(cookie.getPath());
if (cookie.isHttpOnly()) {
builder.append(";HttpOnly");
}
if (cookie.getSecure()) {
builder.append(";Secure");
}
// here you can append other attributes like domain / max-age etc.
builder.append(";SameSite=None");
return builder.toString();
}
}
As mentioned in this answer:
Same-Site flag for session cookie in Spring Security
#Configuration
public static class WebConfig implements WebMvcConfigurer {
#Bean
public TomcatContextCustomizer sameSiteCookiesConfig() {
return context -> {
final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
cookieProcessor.setSameSiteCookies(SameSiteCookies.NONE.getValue());
context.setCookieProcessor(cookieProcessor);
};
}
}
but this seems even simpler
#Configuration
public static class WebConfig implements WebMvcConfigurer {
#Bean
public CookieSameSiteSupplier cookieSameSiteSupplier(){
return CookieSameSiteSupplier.ofNone();
}
}
Or ... even simpler, spring boot since 2.6.0 supports setting it in application.properties.
Spring documentation about SameSite Cookies
server.servlet.session.cookie.same-site = none

Spring boot AngularJs $http.get is empty

I am creating a resource monitor that retrieves typical info about a machine on a different domain but same port. When accessing the url directly the data is returned successfully. However if we try it using angularJs the $http.get request would return a "blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource". We decided to use the chrome CORS extension to allow the connection. Only problem is now the $http.get request is always empty despite the data existing. Not sure why this is happening as no error is produced.
Angular Controller
app.controller("ServerResourcesController", [ "$scope", "$http", function($scope, $http) {
$http.get("http://000.000.0.0:8080/testing")
.then(function(data){
console.log(data);
})
}]);
Controller
#RestController
public class ServerRestController {
Logger logger = LoggerFactory.getLogger(ServerRestController.class);
ServerQueryController sqc = new ServerQueryController();
#RequestMapping("/server-service-info")
public String ServiceInfo() {//Welcome page, non-rest
return "Server Resource Monitor Service";
}
//rest end point
#GetMapping("/server-resources-info")
public ServerInformation ServerInformation() {
ServerInformation serverInformation = sqc.CurrentServerResourceInformation();
return serverInformation;
}
}
Object Class
#Getter #Setter
#JsonIgnoreProperties(ignoreUnknown = true)
public class ServerInformation {
private String name;
private String ipAddress;
private double systemCpuLoad;
private double freePhysicalMemory;
private double totalPhysicalMemory;
private String operatingSystem;
private double freeDiskSpace;
private double diskUsage;
public ServerInformation() {
}
#Override
public String toString() {
return "Values{ systemCpuLoad: "+systemCpuLoad+
", freePhysicalMemory: "+freePhysicalMemory+
", totalPhysicalMemory: "+totalPhysicalMemory+
", operatingSystem: "+operatingSystem+
", freeDiskSpace: "+freeDiskSpace+
", diskUsage: "+diskUsage+
" }";
}
}
It seems your ServerRestController needs to have cross-origin, add this
#RestController
#CrossOrigin(origins = "*")
public class ServerRestController {
...
}
Also, If you want to allow a specific origin you could do it like this:
#CrossOrigin(origins = "http://stackoverflow.com", maxAge = 3600)
You can set #CrossOrigin with the origins to allow and max age either on each method or on the RestController.
Moreover, if you have multiple RestController it's not a best practice to write #CrossOrigin on each and every controller you may just create a Filter like this:
#Component
public class SimpleCORSFilter implements Filter {
private final Logger log = LoggerFactory.getLogger(SimpleCORSFilter.class);
public SimpleCORSFilter() {
log.info("SimpleCORSFilter init");
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
chain.doFilter(req, res);
}
#Override
public void init(FilterConfig filterConfig) {
}
#Override
public void destroy() {
}
}
See the example here: spring cors

How to make jetty ErrorHandler display error in json format in java

I have the jetty server started this way by the sample code below where I have written my own errorHandler class. Through some few research, I got some information here. But it seems not enough to get me what I want. The name of the class that I set to be called by the server is HandleClient.
So if error 404 occurs, it display {"message":"HTTP error 404"} . the program works fine anyway But the response is in text/plain format.
My Problem is: How can I configure the class to format and display the error in MIME MediaType: application/json.
I have had sleepless nights on this. Will be very grateful to all helps.
public class MVCPattern{
public MVCPattern(){
}
public static void main(String args[]) throws JSONException, IOException{
MVCPattern mvcPattern = new MVCPattern();
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
Server jettyServer = new Server(9000);
jettyServer.setHandler(context);
context.setErrorHandler(new ErrorHandler());
// default error handler for resources out of "context" scope
jettyServer.addBean(new ErrorHandler());
ServletHolder jerseyServlet = context.addServlet(org.glassfish.jersey.servlet.ServletContainer.class, "/*");
jerseyServlet.setInitOrder(0);
// Tells the Jersey Servlet which REST service/class to load.
jerseyServlet.setInitParameter("jersey.config.server.provider.classnames",
HandleClient.class.getCanonicalName() );
try {
jettyServer.start();
jettyServer.join();
} catch (Exception ex) {
Logger.getLogger(HandleClient.class.getName()).log(Level.SEVERE, null, ex);
} finally {
jettyServer.destroy();
}
}
/**
* Dummy error handler that disables any error pages or jetty related messages and returns our
* ERROR status JSON with plain HTTP status instead. All original error messages (from our code) are preserved
* as they are not handled by this code.
*/
static class ErrorHandler extends ErrorPageErrorHandler {
#Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
response.getWriter()
.append("{\"message\":\"HTTP error ")
.append(String.valueOf(response.getStatus()))
.append("\"}");
}
}
}
You should check the Accept header in the HttpServletRequest.getHeader("Accept") to first see if the client can accept that type.
Then use HttpServletResponse.setContentType("text/json") to set the content type for the response.
Or, if you are using Jetty 9.3.11.v20160721 (or newer) you can override the ErrorHandler.generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String mimeType) method and handle it accordingly.
See: https://github.com/eclipse/jetty.project/blob/jetty-9.3.11.v20160721/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java#L196-L226 for example on its use.
This is not a big deal.
By default Jetty uses ErrorPageErrorHandler, you can check Jetty's WebAppContext () constructors, the default one looks like this (jetty 9.4.19.v20190610):
public WebAppContext()
{
this(null,null,null,null,null,new ErrorPageErrorHandler(),SESSIONS|SECURITY);
}
https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
You can extend ErrorPageErrorHandler and write to response errors in JSON.
Example:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.igorkhromov.dto.Errors;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
public class ErrorHandler extends ErrorPageErrorHandler {
/*
Messages to error made based on:
Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
https://tools.ietf.org/html/rfc7231
*/
private static final String ERROR_404_MESSAGE = "Target resource not found";
private static final String ERROR_501_MESSAGE = "Server functionality to process request is not implemented";
private static final String ERROR_502_MESSAGE = "Server cannot proxy request";
private static final String ERROR_503_MESSAGE = "Server is currently unable to handle the request";
private static final String ERROR_504_MESSAGE = "Server did not receive a timely response from an upstream server";
private static final String ERROR_UNEXPECTED_MESSAGE = "Unexpected error occurs";
private static final ObjectMapper MAPPER = new ObjectMapper();
#Override
protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String mimeType)
throws IOException
{
baseRequest.setHandled(true);
Writer writer = getAcceptableWriter(baseRequest, request, response);
if (null != writer) {
response.setContentType(MimeTypes.Type.APPLICATION_JSON.asString());
response.setStatus(code);
handleErrorPage(request, writer, code, message);
}
}
#Override
protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException
{
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
return response.getWriter();
}
#Override
protected void writeErrorPage(HttpServletRequest request, Writer writer, int code, String message, boolean showStacks)
throws IOException
{
try {
writer.write(MAPPER.writeValueAsString(new Errors(getMessage(code))));
}
catch (Exception e) {
// Log if needed
}
}
private boolean isRestRequest(HttpServletRequest request) {
return request.getServletPath().startsWith("/api/");
}
private String getMessage(int code) {
switch (code) {
case 404 : return ERROR_404_MESSAGE;
case 501 : return ERROR_501_MESSAGE;
case 502 : return ERROR_502_MESSAGE;
case 503 : return ERROR_503_MESSAGE;
case 504 : return ERROR_504_MESSAGE;
default : return ERROR_UNEXPECTED_MESSAGE;
}
}
}
Code in my GitHub repo:https://github.com/xrom888/blog_jetty-display-errors-in-json
Custom ErrorHandler: https://github.com/xrom888/blog_jetty-display-errors-in-json/blob/master/src/main/java/com/igorkhromov/ErrorHandler.java
All code under the MIT license, enjoy )).

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