I'm having the weirdest problem I have ever seen before.
The application I am working on uses spring security 3.1.3 to provide authentication support. There is a custom login form for which I have implemented a custom authenticationmanager / successhandler and failurehandler.
For some reason on internet explorer I always get the error message "Please fill in all mandatory fields". This is caused by appending /login?error=1 to the end of my url which can only be accessed through the following code (the redirectAndAddError method):
public class TideUserNamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public TideUserNamePasswordAuthenticationFilter() {
super();
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
String password = request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
redirectAndAddError(response);
return null;
}
return super.attemptAuthentication(request, response);
}
private void redirectAndAddError(HttpServletResponse response) {
try {
response.sendRedirect("/tide/login?error=1");
} catch (IOException e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
}
So what I tried was using Fiddler2, a web debugging proxy to view if one of the two parameters are actually empty. The strange thing is that when this program is running the error does not occur anymore and I can log on successfully.
Had anyone had a similar problem before? I think it's not related to my code as running the tool suddenly "solves" the problem.
This problem only occurs in internet explorer which makes it even more strange.
Edit
I have used another tool to watch the requests and this is what happens in IE:
First a POST request is sent to the uri /authenticate, I have set this myself like this:
<beans:property name="filterProcessesUrl" value="/authenticate"/>
The response of that request has http status code 302, moved temporarily and returns that the new location is at /login?error=1 (my form with the mandatory fields required error).
After that a GET request occurs to /login?error=1 with status code 401: Unauthorized. The intercept-url is set up like this:
<intercept-url pattern="/login**" access="permitAll"/>
The next request is a GET request to /login?error=1 again, this time the status code is showing: ERROR_INTERNET_CONNECTION_RESET, which looks like it could be a problem.
In Google Chrome the following request is made:
POST to /authenticate, result is a 302: moved temporarily to the dashboard page (which I display after logging on)
Someone on my team finally figured out what the problem was after finding this issue in the chromium bugtracker:
https://code.google.com/p/chromium/issues/detail?id=62687
The problem has been resolved by adding this in our login controller
#RequestMapping(value = "/login", method = RequestMethod.POST)
public String doLogin() throws ServletException, IOException {
return "forward:/authenticate";
}
and changing the url that the form posts to to this one instead of the authentication url that spring security provides (we are redirecting to it manually now)
Related
I came across an Issue where public urls won't work in Spring security, when you already have an SessionID which is not valid anymore.
Example:
I have the user-register page and gave it a permitAll access like so:
http.authorizeRequests().antMatchers("/register**").permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll();
For my Session settings i have:
http.sessionManagement().invalidSessionUrl("/login?logoutcause=sessiontimeout");
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
http.sessionManagement().sessionAuthenticationErrorUrl("/login");
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
http.sessionManagement().sessionFixation().newSession();
If i have a sessionID from a previous session, which is maybe an old and invalid one, and i hit the Route "/register", spring complains about the invalid session ID and redirects me to "/login".
Just to mention it: Everything else, like login, ressource management, protected urls and logout is working as expected with the configuration.
Reproducing this: Use Redis-Session management in Spring. Got to login page, flush redis db with console. Access the register page directly in browser -> redirected to login because of invalid session id.
o.s.s.w.s.SessionManagementFilter : Requested session ID 8ad2e166-bc21-4646-8390-ad8d1043baec is invalid.
w.s.SimpleRedirectInvalidSessionStrategy : Starting new session (if required) and redirecting to '/login?logoutcause=sessiontimeout'
o.s.s.w.DefaultRedirectStrategy : Redirecting to '/login?logoutcause=sessiontimeout'
Why does Spring even check the session id for a route that have "public" access?
The next Step:
if i fully disable any security checks on the route itself, sadly the required ressources like js and css assets trigger the same behavior and either i get redirected to login, or the assets simply do not get delivered (both no option :D )
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/register/**");
super.configure(web);
}
My Solution and Workaround
I disabled the following config which solved all my problems
// DISABLED
// http.sessionManagement().invalidSessionUrl("/login?logoutcause=sessiontimeout");
My Question
This can not be the best way to do it, right?
What is the better and more secure way to do it.
Please help me to understand why this is done this way by spring, or what i configured the wrong way.
I have solved the same issue by adding the following config:
.antMatchers("/login-invalid").permitAll()
I had same issue, and solved like below.
0. pre-requisite
jdk 17
spring-boot 2.7
1. create custom InvalidSessionStrategy
#Component
public class MyInvalidSessionStrategy implements InvalidSessionStrategy {
private final InvalidSessionStrategy simpleRedirectStrategy;
private final InvalidSessionStrategy requestedUrlRedirectStrategy;
private final HandlerMappingIntrospector handlerMappingIntrospector;
public MyInvalidSessionStrategy(HandlerMappingIntrospector handlerMappingIntrospector) {
this.simpleRedirectInvalidSessionStrategy = new SimpleRedirectInvalidSessionStrategy("/login?logoutcause=sessiontimeout");
this.requestedUrlRedirectStrategy = new RequestedUrlRedirectInvalidSessionStrategy();
this.handlerMappingIntrospector = handlerMappingIntrospector;
}
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
throws IOException {
var matcher = new MvcRequestMatcher(handlerMappingIntrospector, "/register/**");
if (matcher.matches(request))) {
requestedUrlRedirectStrategy.onInvalidSessionDetected(request, response);
} else {
simpleRedirectStrategy.onInvalidSessionDetected(request, response);
}
}
}
2. configure spring-securiry and register custom InvalidSessionStrategy
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
SecurityFilterChain filterChain(
HttpSecurity http,
MyInvalidSessionStrategy invalidSessionStrategy)
throws Exception {
return http
.login(...)
.logout(...)
.sessionManagement(
configurer -> configurer
.invalidSessionStrategy(invalidSessionStrategy))
.build();
}
}
TLDR: My method requires 2 redirects/forwards to work (1 for authentication and 1 to serve the jsp page). How can I resolve both redirects/forwards (or make it a non-requirement) so as to not run into the error, java.lang.IllegalStateException: Cannot forward after response has been committed.
For more context:
I have a java servlet with a method that looks something like the following:
#GET
#Path("/test")
#Authenticate
public Viewable test(#Context HttpServletRequest request, #Context HttpServletResponse response) {
Map<String, Object> model = createModel();
return new Viewable("/somePath/jspFile", model);
}
The #Authenticate annotation intercepts the call to do some Open ID Connect type authentication which results in the user being forwarded to a different server for all authentication needs. If the user is authenticated, they are redirected back to my application.
However, when hitting the url for this method, I am getting java.lang.IllegalStateException: Cannot forward after response has been committed. I don't know too much about using this Viewable class, but based on the fact that I don't run into that error when returning String/void/whatever else, I assume returning a new Viewable needs to do some forwarding that results in the user seeing the jsp page.
I've read the main SO post about this error, but I am unsure how to apply the fixes to my current problem. For example, I don't know how I would apply something like the following fix:
protected void doPost() {
if (someCondition) {
sendRedirect();
} else {
forward();
}
}
The fix assumes that I can I can either redirect OR forward, but my current method needs a redirect for authentication AND a forward/redirect to serve the jsp page. Maybe there's an obvious fix I'm missing that doesn't require a complete rehaul of the current code?
Edit: It would be nice if I could check if the user was authenticated first, but I assume using this annotation at all automatically entails an initial redirect
Edit: It looks like the user is redirected for the initial login authentication, but does not need to be redirected again after being authenticated once due to SSO
Ok based on some preliminary testing, it seems like the following solution has worked for me:
Check if the user has already been authenticated
Return a Response rather than a Viewable.
Since the user only needs to be redirected the first time for authentication, I can return an empty/meaningless response as a placeholder. And then once the user has been authenticated and is returned to my app, I can return a Viewable wrapped in a Response object.
So the code would look something like the following:
#GET
#Path("/test")
#Authenticate
public Response test(#Context HttpServletRequest request, #Context HttpServletResponse
response) {
Map<String, Object> model = createModel();
if (userIsAuthenticated()) {
return Response.status(401).build();
} else {
return Response.ok(new Viewable("/somePath/jspFile", model)).build();
}
}
I have a Spring application (not using Spring boot) deployed to tomcat
I'm trying to return error 401 (HttpServletResponse.SC_UNAUTHORIZED) on specific URLs in given condition using OncePerRequestFilter ,
But I keep getting Not found error:
Response code: 404
My Filter(removed conditions):
#Component
public class MyFilter extends OncePerRequestFilter {
private static final Logger logger = Logger.getLogger(MyFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not autorized");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
PrintWriter out = response.getWriter();
out.write(""); // Empty
out.flush();
} catch (IOException e) {
logger.error("error filtering", e);
}
return; // stop chain
}
}
I tried using similar code to previous similar answer
I believe you can response.sendError inside do Filter method.
EDIT
If I just throw an exception instead I get a generic error code 500
Response code: 500
EDIT 2
I'm adding filter inside onStartup method overriden WebApplicationInitializer's
FilterRegistration myFilter = servletContext.addFilter("myFilter ", MyFilter.class);
myFilter.addMappingForUrlPatterns(null, false, "/myservlet/myendpoint/*");
EDIT 3
Also my filter in #Componenet and its package include in component scan
#ComponentScan(basePackages = { "my.parent.pacakge"})
I tried your code and it works. Different error codes were getting successfully returned. Although, I wanna point out a couple of things-
The method sendError() sends the response to the client and also clears the response buffer. So, anything you write after it into the response after that is of no use.
Calling setStatus() is of no use either. The HTTP response and HTTP response code has already been sent to the client in the above line when you called sendError().
Now, this is my hypothesis why your code is not working for you, but it is for me-
The reason that you might be getting HTTP 404 is that your API is not present. This can be due to a spelling error or maybe due to a simple ignorance like calling your API named /foo/bar like /foo/bar/ i.e. with extra trailing forward slash. I believe that your filter is getting executed successfully and you must be doing something useful there, not the sample code of sendError() that you have explained in the question.
Also, when you throw an Exception in your filter, the control does not reach to the API and API lookup does not happen. Hence, the default HTTP 500 response is sent to the client due to the unhandled exception instead of HTTP 404.
It return 401 as expected only if setStatus is executed after out.write
PrintWriter out = response.getWriter();
out.write("");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
We've created an application that mimics the Spring Security - API Gate Pattern tutorial (https://spring.io/guides/tutorials/spring-security-and-angular-js/#_the_api_gateway_pattern_angular_js_and_spring_security_part_iv). The only variation is that we're using a MySQL database rather than Redis.
Using localhost:8080 as the root, we have localhost:8080/login (login page), localhost:8080/ui (jQuery client), and localhost:8080/api (restful web services, business logic, etc.)
We're finding session handling and forwarding to the various entities works as expected. Meaning the session gets created as expected, forwarding is happening as expected, etc. There is one exception. If I log in, then log out, then go directly to localhost:8080/ui it'll forward me to the login page. You login, and it forwards you back to the localhost:8080/ui, but will display "ACCESS DENIED"!
After tracking the sessions in the database and client I've found that there are two sessions that exist in the database. One with permissions and one without. The client retains the one without!
Has anyone else run into this problem? Is there a way to circumvent this?
Here's a list steps I went through, the database session tracking, and client verification.
session_id principal_name Client
------------------------------------------------------------
1) go to localhost:8080
9229045c-27e0-410a-8711-45c56576d647 - X
2) login
2275db1c-fca4-4a2f-be73-e440599499d6 root X
3) logout
cc917e68-b1c0-46a4-bbe3-6705ccf7a5fa - X
4) go to localhost:8080/ui --> forwards to localhost:8080/login
cc917e68-b1c0-46a4-bbe3-6705ccf7a5fa - X
5) login -> forwards to localhost:8080/ui -> Access Denied
90d7931d-b265-42e2-a225-286bcf7d159c - X
d2fae0ac-9cf9-4287-8e38-51f64b0ab28d root
Alright, after many hours we found a solution to what seemed to be inconsistent behavior. Meaning sometimes you'd log in and it'd retain the proper session and you could go the the localhost:8080/ui page and not get the Whitelabel Error page... sometimes you'd still get it.
On the Gateway server...
1) Added RequestMethod.POST
#Controller
public class HomeController {
#RequestMapping(method = { RequestMethod.GET, RequestMethod.POST }, path = "/")
public String home() {
return "redirect:" + RequestMappings.UI;
}
}
2) Changed configure file, specifically
a) added .successForwardUrl(“/”)
b) added .loginProcessingUrl(“/login”)
c) added .logoutSuccessUrl("/login?logout")
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.frameOptions().sameOrigin()
.and().formLogin()
.loginPage(RequestMappings.LOGIN)
.failureHandler(failureHandler())
.successForwardUrl("/")
.permitAll()
.loginProcessingUrl("/login")
.and().logout()
.logoutSuccessUrl("/login?logout")
.and().authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers(RequestMappings.CHANGE_PASSWORD).permitAll()
.anyRequest().authenticated()
.and().csrf()
.csrfTokenRepository(csrfTokenRepository())
.and().addFilterAfter(csrfHeaderFilter(), SessionManagementFilter.class);
}
Now, there still is a way to get the whitepage error. If, before ever logging in, you go directly to localhost:8080/ui…. It’ll forward you to the localhost:8080/login page. You log in. You’ll be at localhost:8080/ui/ looking at everything as expected. If you remove the last forward slash then you’ll get the whitepage error. Then from there things can get mucked up in the cache. But if you go back to the root, you can login as normal and everything will work as normal.
I think what is going on is that the pre-login localhost:8080/ui call is being cached and because the index.html page was never loaded once you log back in and go back you pass the authorization check, but it tries to load… well, nothing, then throws an error. At least that’s my best guess.
Anyways, cheers! Thanks for the help, which started us off on the right track!
Most likely, when you go directly back to your UI application, it is still using the old session ID and when you log in, it is redirecting you to the UI again with the old session ID.
It could also be that it is still an old issue on Tomcat, check this thread to see how to properly clear your cookies: https://straypixels.net/clearing-cookies-in-spring-tomcat/
Bascially, extend the SimpleUrlLogoutSuccessHandler like this:
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
//This search may or may not actually be necessary, you might be able to just
// create a new cookie with the right name
for(Cookie cookie : request.getCookies()) {
if(cookie.getName() == "JSESSIONID") {
//Clear one cookie
cookie.setName("");
cookie.setMaxAge(0);
cookie.setPath(request.getContextPath());
response.addCookie(cookie);
//Clear the other cookie
Cookie cookieWithSlash = cookie.clone();
cookieWithSlash.setPath(request.getContextPath() + "/");
response.addCookie(cookieWithSlash);
}
}
//This is actually a filter; continue along the chain
super.onLogoutSuccess(request, response, authentication);
}
Here is a little background of my problem:
I'm using Spring MVC 3.1.2.RELEASE. The server is deployed on Amazon AWS. I have a simple controller.
public class MyWebController implements Controller {
#Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
try {
String delegateGuid = request.getParameter("delegateGuid");
String serviceCodesToBeUpdated = request.getParameter("serviceCodesToBeUpdated");
if (StringUtils.isBlank(delegateGuid) || StringUtils.isBlank(serviceCodesToBeUpdated)) {
throw new MyException("delegateGuid and/or serviceCodesToBeUpdted cannot be null or empty");
}
...
I have a client script that makes POST request to this endpoint. But sometimes, I hit this MyException that tells me the params are not set. But client logs show that the params are set correctly before sending the requests. The difficult part of debugging this is that it is not reproducible.
Does anyone know what could possibly have caused this issue? I know this might not be the level of detail enough to identify the problem. But if you can suggest any debug logs I could insert into my code, that would be helpful as well. I'm lost in that I don't even know where to start debugging this.
Thanks.