I'm dwelling with this problem... I have a Spring Boot application wit a S2S communication. I have a #RestController method which should accept POST request.
This is the controller
#RestController
public class PaymentRestController {
#PostMapping("/util/paymentResponse")
public void savePaymentResponse(#RequestParam boolean transaction_status, #RequestParam String usedToken,
#RequestParam String transaction_message, #RequestParam String authCode,
#RequestParam String transactionCode, #RequestParam String orderId, HttpServletRequest request) {
//business logic
}
}
If i hit this link i get a 405 error, method not allowed
At first time i found that the request was blocked by the CSFR Filter which is enabled on the web application, so I have configured my security in this way
#Configuration
#ComponentScan("it.besmart")
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Autowired
#Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
#Autowired
CustomSuccessHandler customSuccessHandler;
#Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
#Autowired
DataSource dataSource;
private final static Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
#Autowired
public void configureGlobalService(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SwitchUserFilter switchUserFilter() {
SwitchUserFilter filter = new SwitchUserFilter();
filter.setUserDetailsService(userDetailsService);
filter.setSuccessHandler(customSuccessHandler);
filter.setFailureHandler(customAuthenticationFailureHandler);
return filter;
}
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Webapp security configured");
http
.authorizeRequests()
.antMatchers("/", "/home", "/contacts", "/faq", "/privacy", "/register", "/registrationConfirm", "/util/**", "/resendRegistrationToken","/park**", "/oauth/authorize", "/error")
.permitAll()
.antMatchers("/profile**", "/edit**","/payment**", "/plate**","/notification**", "/addPaymentMethod**", "/logout/impersonate**")
.access("hasRole('USER') or hasRole('NOPAYMENT')")
.antMatchers("/book**", "/manage**")
.access("hasRole('USER')")
.antMatchers("/admin**", "/login/impersonate**").access("hasRole('ADMIN')")
.antMatchers("/updatePassword").hasAuthority("CHANGE_PASSWORD_PRIVILEGE")
.and().formLogin().loginPage("/?login=login").loginProcessingUrl("/") .successHandler(customSuccessHandler).failureHandler(customAuthenticationFailureHandler).usernameParameter("email").passwordParameter("password").and().rememberMe().rememberMeParameter("remember-me").tokenRepository(persistentTokenRepository()).tokenValiditySeconds(86400).and().exceptionHandling().accessDeniedPage("/accessDenied")
.and().csrf().ignoringAntMatchers( "/util**")
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/?logout=true").permitAll()
.and().addFilterAfter(switchUserFilter(), FilterSecurityInterceptor.class);
}
In this way i'm not getting the CSRF token exception, but still getting the 405 error.
It's not even a problem of POST because if i change to GET the request and the mapping, i still take the 405 error... And if i try to send a POST, i see in the header response that the Allowed method is POST, if i send it in GET i see allowed method POST... weird
I don't know where to see...
In my case the endpoint had ssl on i.e. it was https
In Postman I by mistake was using http
http will work fine for GETs but for POSTs it returns a 405 method not allowed. It has to be https if your endpoint expects it to be.
If you have request and response logging turned on in Spring the POST endpoint in the above situation will log as follows:
[2021-02-26T10:40:07+02:00] (my-app/fffffa343226e) 2021-02-26T08:40:07,915Z (UTC+0) [http-nio-80-exec-6] DEBUG o.s.w.f.CommonsRequestLoggingFilter - Before request [GET /api/v1/my-app, client=1.2.3.4, user=aUser]
[2021-02-26T10:40:07+02:00] (my-app/fffffa343226e) 2021-02-26T08:40:07,915Z (UTC+0) [http-nio-80-exec-6] WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported]
[2021-02-26T10:40:07+02:00] (my-app/fffffa343226e) 2021-02-26T08:40:07,916Z (UTC+0) [http-nio-80-exec-6] DEBUG o.s.w.f.CommonsRequestLoggingFilter - After request [GET /api/v1/my-app, client=1.2.3.4, user=aUser]
So the problem was that one of the parameter was null.
It has been solved adding required=null at the request parameter annotation, like that:
#RequestParam(value = "yourParamName", required = false)
this cause a 405, as defined here:
6.5.5. 405 Method Not Allowed
The 405 (Method Not Allowed) status code indicates that the method
received in the request-line is known by the origin server but not
supported by the target resource. The origin server MUST generate an
Allow header field in a 405 response containing a list of the target
resource's currently supported methods.
A 405 response is cacheable by default; i.e., unless otherwise
indicated by the method definition or explicit cache controls (see
Section 4.2.2 of [RFC7234]).
when the "target resource" are defined here:
In my case I a mapping in my controller in the following way:
#RequestMapping(name = "/fetch", method = RequestMethod.POST)
public Long createFetch() throws IOException {
return fetchService.doFetch();
}
If you notice, the above mapping is to name, but the requests work with this. Once I apply the same at the #Controller level and at method level, I started seeing this error. Setting the path to value resolved this.
Also remember to check the protocol. In my case it had to be https instead of http
The error message could certainly be more descriptive!
Related
I have made a custom exception which should give the message to client when raised. However, that does not seem to be working whenever error code is 401 i.e. UNAUTHORIZED. I tried changing the status code to something else, and message showed up.
Note - I have already set the flag server.error.include-message=always in application.properties
BadCredentialsException.java
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public class BadCredentialsException extends RuntimeException{
// Runtime exception just needs this, I guess :/
private static final long serialVersionUID = 1;
public BadCredentialsException(String message){
super(message);
}
}
Here's how I have tried raising the exception.
public ResponseEntity<Boolean> loginUser(String username, String password){
// validating username
User user = myUserRepository.findByUsername(username).
orElseThrow(() -> new ResourceNotFoundException("No username: " + username + " found. Please enter a correct username!"));
// validating password
if(!new BCryptPasswordEncoder().matches(password, user.getPassword())){
throw new BadCredentialsException("Incorrect Password. Please enter correct password to login!");
}
return ResponseEntity.ok(true);
}
Note - message is correctly being displayed in the terminal though. Just not showing up on the client.
UPDATE 1 - I have made this particular endpoint to be accessible by everyone, using permitAll(). In postman, when I select select "no auth" and call this endpoint with expected exception, exception does not give the message unless, I give correct basic auth credentials (any role). I have absolutely no clue why this is happening.
UPDATE 2 - Adding SecurityConfiguration code.
SecurityConfiguration.java
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
authorizeRequests().
antMatchers(HttpMethod.POST, "/api/v2/user/login/**").permitAll().
antMatchers(HttpMethod.POST, "/api/v2/user/", "/api/v2/user", "/api/v2/user/change-role/**").hasAuthority("ROOT").
antMatchers(HttpMethod.GET, "/api/v2/user/", "/api/v2/user").hasAuthority("ROOT").
antMatchers(HttpMethod.POST, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("ADMIN", "ROOT").
antMatchers(HttpMethod.GET, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("EMPLOYEE", "ADMIN", "ROOT").
anyRequest().
authenticated().
and().
httpBasic();
}
Actually, there are multiple ways to do that.
First, as #SergVasylchak said in the comments, you can use ControllerAdvice.
So the approach is like below:
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(BadCredentialsException.class)
#ResponseBody
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
private Message handleMessageAuth(BadCredentialsException e, HttpServletRequest request) {
Message message = new Message();
message.setTimestamp(getTimeStamp());
message.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase());
message.setStatus(HttpStatus.UNAUTHORIZED.value());
message.setMessage(e.getMessage());
message.setPath(String.valueOf(request.getRequestURI()));
return message;
}
}
Message is your custom pojo.
public class Message {
private String timestamp;
private int status;
private String error;
private String message;
private String path;
// getters & setters
}
Another solution is that implement AuthenticationEntryPoint.
What is AuthenticationEntryPoint?
It is an interface implemented by ExceptionTranslationFilter,
basically a filter which is the first point of entry for Spring
Security. It is the entry point to check if a user is authenticated
and logs the person in or throws exception (unauthorized). Usually, the
class can be used like that in simple applications but when using
Spring security in REST, JWT etc one will have to extend it to provide
better Spring Security filter chain management.
AuthenticationEntryPoint is used in Spring Web Security to configure
an application to perform certain actions whenever an unauthenticated
client tries to access private resources.
Look at this.
AuthenticationEntryPoint is used to send an HTTP response that
requests credentials from a client. Sometimes a client will
proactively include credentials such as a username/password to request
a resource. In these cases, Spring Security does not need to provide
an HTTP response that requests credentials from the client since they
are already included. In other cases, a client will make an
unauthenticated request to a resource that they are not authorized to
access. In this case, an implementation of AuthenticationEntryPoint is
used to request credentials from the client. The
AuthenticationEntryPoint implementation might perform a redirect to a
log in page, respond with a WWW-Authenticate header, etc.
For example, if you have a JWT authentication the approach is like below.
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
// if you want to return 401 status, this is enough.
// if you want to customize your response you can make it as below.
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{ \"error\": \"" + authException.getMessage() + "\" }");
}
}
Now it's time to config the AuthenticationEntryPoint in SecurityConfig.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
authorizeRequests().
antMatchers(HttpMethod.POST, "/api/v2/user/login/**").permitAll().
antMatchers(HttpMethod.POST, "/api/v2/user/", "/api/v2/user", "/api/v2/user/change-role/**").hasAuthority("ROOT").
antMatchers(HttpMethod.GET, "/api/v2/user/", "/api/v2/user").hasAuthority("ROOT").
antMatchers(HttpMethod.POST, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("ADMIN", "ROOT").
antMatchers(HttpMethod.GET, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("EMPLOYEE", "ADMIN", "ROOT").
anyRequest().
authenticated().
and().
httpBasic();
}
}
Context
I'm having some trouble with my application. We're using Spring Boot 2.4.10 and Spring Security 5.4.8. We use cookies to interact with the app.
We have a frontend application stored in src/main/resources that, among other things, connects to a websocket endpoint exposed in /api/v1/notification.
My configuration
application.properties file:
# cookie-related settings
server.servlet.session.cookie.name=mySessionCookie
server.servlet.session.cookie.path=/
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final String[] authorizedWithoutAuth = {
"/",
"/index.html",
"/static/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.antMatchers(authorizedWithoutAuth).permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.and()
.logout().logoutUrl("/api/v1/logout").invalidateHttpSession(true)
.deleteCookies("mySessionCookie");
}
}
The problem
While no user is logged in, the frontend tries to reach periodically the websocket endpoint to open a connection.
The first api/v1/notification ends redirected to the /error endpoint, which returns an HttpServletResponse with a 'Set-Cookie' header (I think this may be an anonymous cookie set in the first request?) and a 401 status. I cannot change this behaviour.
The following requests to api/v1/notification use this cookie in the header (while user is not logged in). These requests are also redirected to the /error endpoint, which returns each time an HttpServletResponse with 401 status but here, no 'Set-Cookie' header is included.
Once the user logs in with Authorization headers, a correct cookie is set by the response and used in the following requests.
The thing is, sometimes the set cookie suddenly changes again to an invalid one, and the following requests, done with this new invalid cookie, turn into a redirection to the login page.
After checking the code, it seems there is an old api/v1/notification request (previous to the login request) taking place, with an invalid cookie (the anonymous one, present before login).
This request is redirected to the /error endpoint: here, once again the HttpServletResponse has 401 status and is containing a Set-Cookie header that is modifying the browser cookie (replacing the good one).
Following is a scheme of the problem, to hopefully make it easier to understand.
Expected behaviour
I would like to prevent an unauthorized request from setting the session cookie.
It's ok if a previous request responds with a 401 code, but I don't want it to change the current set cookie.
I tried...
I tried extending the ErrorController by returning a ResponseEntity with all the headers present in the input HttpServletResponse except for the 'Set-Cookie' header. This doesn't work.
I also tried modifying my configuration to disable anonymous requests:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final String[] authorizedWithoutAuth = {
"/",
"/index.html",
"/static/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.anonymous().disable()
.authorizeRequests()
// .antMatchers(authorizedWithoutAuth).permitAll() I had to remove these from here, and include them in the method below
.anyRequest().authenticated()
.and()
.csrf().disable()
.and()
.logout().logoutUrl("/api/v1/logout").invalidateHttpSession(true)
.deleteCookies("mySessionCookie");
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(authorizedWithoutAuth);
}
}
but the session cookie is still set this way too, with 401 requests.
I also tried using #ControllerAdvice to handle the exceptions, but these are thrown by Spring Security in the AbstractSecurityInterceptor, as learnt in this response.
Thak you all for your time. Sorry for the post length :)
I started digging in Spring Security libraries, and noticed the session cookie was being set in HttpSessionRequestCache.saveRequest(...) method:
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
if (!this.requestMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not save request since it did not match [%s]", this.requestMatcher));
}
} else {
DefaultSavedRequest savedRequest = new DefaultSavedRequest(request, this.portResolver);
if (!this.createSessionAllowed && request.getSession(false) == null) {
this.logger.trace("Did not save request since there's no session and createSessionAllowed is false");
} else {
request.getSession().setAttribute(this.sessionAttrName, savedRequest);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Saved request %s to session", savedRequest.getRedirectUrl()));
}
}
}
}
The 'Set-Cookie' header appears when creating the DefaultSavedRequest object.
I changed my WebSecurityConfig to the following:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.requestCache().requestCache(getHttpSessionRequestCache()) // This is new
.and()
.authorizeRequests().antMatchers(authorizedWithoutAuth).permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.and()
.logout().logoutUrl("/api/v1/logout").invalidateHttpSession(true)
.deleteCookies("mySessionCookie");
}
public HttpSessionRequestCache getHttpSessionRequestCache()
{
HttpSessionRequestCache httpSessionRequestCache = new HttpSessionRequestCache();
httpSessionRequestCache.setCreateSessionAllowed(false); // I modified this parameter
return httpSessionRequestCache;
}
}
and now it works. When logging in with Authorization headers, the cookie is being set correctly, but all the requests with an invalid session cookie or an expired one return a 401 response without setting a new one.
Also, after reading this answer, I understood better what this createSessionAllowed was doing
When a POST request to a non-existing end-point in the application is sent, the server returns 405 instead of 404. A similar problem for Requests with an existing endpoint occurs, the status code returns 200 whenever everything goes right, but when an internal server error occurs (for example User not found), the http response becomes 405 (instead of 500). With GET requests everything works as it should.
The weird thing is, if I put on the debugger, and I follow the process of the error to be thrown, it is handling a 500 error. But apparently somewhere in the end something goes wrong and I get a 405 returned.
My web security config:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtRequestFilter jwtRequestFilter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Value("${allowedOrigin}")
private String origin = "http://localhost:4200";
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//You can enforce the use of HTTPS when your app is running on Heroku by adding
// the following configuration to your Spring Boot app.
httpSecurity.requiresChannel()
.requestMatchers(r - > r.getHeader("X-Forwarded-Proto") != null)
.requiresSecure();
httpSecurity
.cors()
.and().csrf()
.disable()
// dont authenticate this particular request
.authorizeRequests()
.antMatchers("/api/authenticate")
.permitAll()
// all other requests for /api need to be authenticated
.antMatchers("/api/**", "/admin/**")
.authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
UPDATE:
I do not have any ControllerAdivce and there is no Global Exception handler written.
The "Allow" header in the 405 response reads "GET, HEAD", even when the POST request actually entered the POST endpoint.
Based on your HttpSecurity configuration you only allowing Post requests, you will get 405 (method not allowed) for other HTTP methods like GET, PUT....
antMatchers(HttpMethod.POST).permitAll();
It turned out an ErrorController with an unimplemented method for the "/error" path was causing the problem. Whenever an exception or an error was thrown it was resolved to "/error" and picked up by the ErrorController, which for some reason resolved it to a 405. After implementing the method the HTTP statuses are returned correctly.
#RestController
public class RoutingController implements ErrorController {
private static final String PATH = "/error";
#RequestMapping(value = PATH)
public String handleError(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
return String.format("<html><body><h2>Error Page</h2><div>Status code: <b>%s</b></div>" +
"<div>Exception Message: <b>%s</b></div><body></html>",
statusCode, exception == null ? "N/A" : exception.getMessage());
}
public String getErrorPath() {
return PATH;
}
}
I have a situation I would need to know the requested payload details when the POST request got 401 Unauthorized error.
I am thinking we will NOT be able to capture the payload when the request has NOT made it to the API endpoint due to Unauthorized error. It will be filtered out before hitting this endpoint.
I am using Springboot 2.1.6
My controller method as below
#PostMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PayloadResponse> processPayload(#RequestBody String payload) {
logger.info("Received a payload: {}", payload);
}
Are there any ways we can log this payload somehow even on 401 error?
Thanks in advance
You cannot use any of SpringMVC mechanisms to catch and log this kind of error because it happens before going in MVC stack. #ControlerAdvice won't do a thing.
You can extend AuthenticationEntryPoint and config it by
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
}
}
extend it like this
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest req, HttpServletResponse res,
AuthenticationException authException)
throws IOException {
res.setContentType("application/json;charset=UTF-8");
res.setStatus(401);
res.getWriter().write(JsonUtil.getWriteMapper().writeValueAsString(
new ErrorRestResponse(authException,false,""))); //This is my custom error response
// You can put logging code around here
}
}
you can use a #ControllerAdvice to handle all kinds of requests and read their payloads if supplied, and give back the appropriate response.
I have small rest service that is protected with default Spring Boot security config. It by default requires authorization on every http method including OPTIONS, chrome however does not give a flying duck and won't include authorization header in preflight request which results in 401 response.
How can I disable http basic auth on specific method? So far I tried:
#Configuration
#EnableWebMvc
#EnableGlobalMethodSecurity(securedEnabled = true)
public class Config {
}
And in controller:
#CrossOrigin(origins = {"http://localhost:4200"}, maxAge = 4800)
#RestController
public class MainController {
#Secured("IS_AUTHENTICATED_ANONYMOUSLY")
#RequestMapping(method = RequestMethod.OPTIONS)
public ResponseEntity handle() {
return new ResponseEntity(HttpStatus.OK);
}
}
Did not work obviously.
In the method
#Override
protected void configure(HttpSecurity http) throws Exception
{
add
.antMatchers(HttpMethod.OPTIONS, "/path/to/skip/check").permitAll()