Unable to access Spring Boot login error page with custom login failure handler - spring-boot

Whenever we try to input wrong credentials in Spring Boot login page, we got Bad Credentials Error with link /login?error I'm trying to limit a login for which I've created a custom login failure handler and whenever I try to provide wrong credentials I'm not able to get any kind of error by Spring Security at this /login?error page in place of this, I'm getting Status 404 Error.
AppConfig
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.failureHandler(loginFailureHandler)
.permitAll()
.and()
.logout();
}
LoginFailureHandler
#Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Autowired
private UserService service;
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String username = request.getParameter("username");
Employee emp = service.getByUsername(username);
if(emp!=null) {
if (emp.isAccountNonLocked()) {
if (emp.getFailedAttempt() < UserService.MAX_FAILED_ATTEMPTS - 1) {
service.increaseFailedAttempt(emp);
} else {
service.lock(emp);
exception = new LockedException("Your account has been locked due to three failed attempts"
+"Try again after 24 Hours....");
}
} else {
if(service.unlockWhenTimeExpired(emp)){
exception = new LockedException("Your Account is unLocked now...." +
"try to login again");
}
}
}
super.setDefaultFailureUrl("/login?error"); // I'm not getting this page while a fail login
super.onAuthenticationFailure(request, response, exception);
}
}
Since I'm unable to get this page /login?error I'm not able to display any message regarding failure login.

I assume you are using spring-boot-mvc. You can overwrite the default login page by creating a login.html in the src/main/resources/templates directory.
And in it you can display your error message by utilizing th:if="${param.error}" like so:
<div th:if="${param.error}">Wrong credentials!</div>

Related

Spring boot security, always redirects to login page, if navigate through address bar

I have a react project, and the security works fine untill I navigate within page - i.e. clicking buttons etc. But, if I refresh page, or input url directly into adress field, it always navigates to login form.
This is my security config:
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
.and()
.csrf().disable();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**");
web.ignoring().antMatchers("/rest/system/getVersion");
}
}
This is restAuthenticationEntryPoint -
#Slf4j
#Component
#RequiredArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ResponseWrapMessage responseWrapMessage;
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.error(e.getLocalizedMessage(), e);
BaseResponse baseResponse = new BaseResponse(UNKNOWN_ERROR, e.getLocalizedMessage());
insufficientAuthenticationWrapper(baseResponse, e);
responseWrapMessage.wrap(httpServletResponse, baseResponse);
}
private void insufficientAuthenticationWrapper(BaseResponse baseResponse, AuthenticationException e) {
if (e instanceof InsufficientAuthenticationException) {
baseResponse.setContent(CREDENTIAL_NO_VALID);
}
}
}
This is accessDeniedHandler:
#Slf4j
#Component
#RequiredArgsConstructor
public class RestAccessDeniedHandler implements AccessDeniedHandler {
private final ResponseWrapMessage responseWrapMessage;
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error(accessDeniedException.getLocalizedMessage(), accessDeniedException);
BaseResponse baseResponse = new BaseResponse(UNKNOWN_ERROR, accessDeniedException.getLocalizedMessage());
responseWrapMessage.wrap(response, baseResponse);
}
}
How can it be changed? I want to persist session, untill I do the logout.
UPDATE:
I see that cookie JSESSIONID is set on the logine page like -
set-cookie: JSESSIONID=9E3BD2B1CF7C69A49902DAA7E71E393E; Path=/mctm-bh; HttpOnly
And then it is sent out when I navigate pressing buttons within the page -
Cookie: JSESSIONID=9E3BD2B1CF7C69A49902DAA7E71E393E
But if I press enter in the address URL then it is NOT sent out, and hence I am redirected to login page
How can it be changed? Maybe I there is some problem with the cookie - like there is now expires attribute. But.. specification does not say that it should be additionally customized and also it is not clear how to do it.
!UPDATE2! I heard, that it is a common problem with basic authorization. If we authorise with 'Authorization: basic ...' then browser saves auth in some cash. And that cash is invalidated if we type something in browser address field. And the way out is not to use basic auth, and to migrate as an option to spring tokens solution.
And some more details could also be found here:
https://habr.com/ru/post/488388/
I suggest you check that your session tracking mechanics is working, e.g. check that you have cookies allowed in your browser, and define the tracking-mode parameter explicitly in the application.properties:
server.servlet.session.tracking-modes=COOKIE
This chooses where to store the JSESSIONID — in the cookie
Typing in the Address bar is equivalent to creating a new session/ or opening the link in a new tab. This can be one reason why it is asking for a new log-in every time you navigate through the address bar.
I heard, that it is a common problem with basic authorization. If we authorise with 'Authorization: basic ...' then browser saves auth in some cash. And that cash is invalidated if we type something in browser address field. And the way out is not to use basic auth, and to migrate as an option to spring tokens solution. And some more details could also be found here: https://habr.com/ru/post/488388/

Send a http 401 error code instead of default login page, spring security

I am using a basic authorization with the Spring Security. I configure the latter via Java config.
I would like to send to a client the HTTP 401 error code with the message "Invalid login and password" if they are invalid. However, currently Spring Security simply displays me a default pop-up window.
Here is my Spring security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().anyRequest().authenticated().and()
.httpBasic()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/pages/index.html");
http.exceptionHandling().authenticationEntryPoint(new AjaxAuthorizationPoint());
}
As far as I understood, I have to add the custom authentificationEntryPoint to handle the case of the invalid credentials i.e. I have to send there a 401 error code with the error message
Here is the code for it. For the sake of simplicity, the body of the method is rather simple.
public class AjaxAuthorizationPoint extends BasicAuthenticationEntryPoint{
#Override
public void commence(final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException {
System.out.println("blah");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
However, the method public void commence() doesn't fire up, when I enter an invalid login and password and Spring simply sends me the default login pop-up window.
How can I redefine the default strategy? How can I configure the Spring security to send a HTTP 401 error code instead of displaying a default login page?
I think, I have found a solution for my problem. Here the code that does exactly what I need.
Spring security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().authenticationEntryPoint(new AjaxAuthorizationPoint("/ajax_login"));
http.authorizeRequests().anyRequest().authenticated().and().httpBasic()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/pages/index.html");
}
And custom ajax authorization point:
public class AjaxAuthorizationPoint extends LoginUrlAuthenticationEntryPoint {
public AjaxAuthorizationPoint(String loginFormUrl) {
super(loginFormUrl);
}
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(403);
response.getWriter().print("Invalid login/password");
response.getWriter().flush();
}
}
Would appreciate any code review.

Spring Security Disable Login Page / Redirect

Is there a way to disable the redirect for Spring Security and the login page. My requirements specify the login should be part of the navigation menu.
Example:
Therefore there is no dedicated login page. The login information needs to be submitted via Ajax. If an error occurs it should return JSON specifying the error and use the proper HTTP Status code. If authentication checks out it should return a 200 and then javascript can handle it from there.
I hope that makes sense unless there is any easier way to accomplish this with Spring Security. I don't have much experience with Spring Security. I assume this has to be a common practice, but I didn't find much.
Current spring security configuration
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/public/**").permitAll()
.antMatchers("/about").permitAll()
.anyRequest().fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.usernameParameter("email")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.deleteCookies("remember-me")
.logoutSuccessUrl("/")
.permitAll()
.and()
.rememberMe();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
Update:
I tried using HttpBasic() but then it asks for login creds not matter what and its the ugly browser popup which is not acceptable to the end user. It looks like I may have to extend AuthenticationEntryPoint.
At the end of the day I need Spring security to send back JSON saying the authentication succeeded or failed.
The redirect behavior comes from SavedRequestAwareAuthenticationSuccessHandler which is the default success handler. Thus an easy solution to remove the redirect is to write your own success handler. E.g.
http.formLogin().successHandler(new AuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//do nothing
}
});
You need to disable redirection in a couple of different places. Here's a sample based on https://github.com/Apress/beg-spring-boot-2/blob/master/chapter-13/springboot-rest-api-security-demo/src/main/java/com/apress/demo/config/WebSecurityConfig.java
In my case, I don't return json body but only HTTP status to indicate success/failure. But you can further customize the handlers to build the body. I also kept CSRF protection on.
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
public void initialize(AuthenticationManagerBuilder auth, DataSource dataSource) throws Exception {
// here you can customize queries when you already have credentials stored somewhere
var usersQuery = "select username, password, 'true' from users where username = ?";
var rolesQuery = "select username, role from users where username = ?";
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(usersQuery)
.authoritiesByUsernameQuery(rolesQuery)
;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// all URLs are protected, except 'POST /login' so anonymous user can authenticate
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
// 401-UNAUTHORIZED when anonymous user tries to access protected URLs
.and()
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
// standard login form that sends 204-NO_CONTENT when login is OK and 401-UNAUTHORIZED when login fails
.and()
.formLogin()
.successHandler((req, res, auth) -> res.setStatus(HttpStatus.NO_CONTENT.value()))
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
// standard logout that sends 204-NO_CONTENT when logout is OK
.and()
.logout()
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT))
// add CSRF protection to all URLs
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
;
}
}
Here's a deep explanation of the whole process, including CSRF and why you need a session: https://spring.io/guides/tutorials/spring-security-and-angular-js/
Scenarios that I tested:
happy path
GET /users/current (or any of your protected URLs)
request --> no cookie
<- response 401 + cookie XSRF-TOKEN
POST /login
-> header X-XSRF-TOKEN + cookie XSRF-TOKEN + body form with valid username/password
<- 204 + cookie JSESSIONID
GET /users/current
-> cookie JSESSIONID
<- 200 + body with user details
POST /logout
-> header X-XSRF-TOKEN + cookie XSRF-TOKEN + cookie JSESSIONID
<- 204
=== exceptional #1: bad credentials
POST /login
-> header X-XSRF-TOKEN + cookie XSRF-TOKEN + body form with bad username/password
<- 401
=== exceptional #2: no CSRF at /login (like a malicious request)
POST /login
-> cookie XSRF-TOKEN + body form with valid username/password
<- 401 (I would expect 403, but this should be fine)
=== exceptional #3: no CSRF at /logout (like a malicious request)
(user is authenticated)
POST /logout
-> cookie XSRF-TOKEN + cookie JSESSIONID + empty body
<- 403
(user is still authenticated)
On my project I implemented it for the requirements:
1) For rest-request 401 status if user is not authorized
2) For simple page 302 redirect to login page if user is not authorized
public class AccessDeniedFilter extends GenericFilterBean {
#Override
public void doFilter(
ServletRequest request,
ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
if (e instanceof NestedServletException &&
((NestedServletException) e).getRootCause() instanceof AccessDeniedException) {
HttpServletRequest rq = (HttpServletRequest) request;
HttpServletResponse rs = (HttpServletResponse) response;
if (isAjax(rq)) {
rs.sendError(HttpStatus.FORBIDDEN.value());
} else {
rs.sendRedirect("/#sign-in");
}
}
}
}
private Boolean isAjax(HttpServletRequest request) {
return request.getContentType() != null &&
request.getContentType().contains("application/json") &&
request.getRequestURI() != null &&
(request.getRequestURI().contains("api") || request.getRequestURI().contains("rest"));
}
}
And enable the filter:
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http
.addFilterBefore(new AccessDeniedFilter(),
FilterSecurityInterceptor.class);
...
}
You can change handle AccessDeniedException for you requirements in the condition:
if (isAjax(rq)) {
rs.sendError(HttpStatus.FORBIDDEN.value());
} else {
rs.sendRedirect("/#sign-in");
}
When a browser gets a 401 with "WWW-Authetication: Basic ... ", it pops up a Dialog. Spring Security sends that header unless it sees "X-Requested-With" in the request.
You should send "X-Requested-With: XMLHttpRequest" header for all requests, this is an old fashioned way of saying - I am an AJAX request.

Spring security redirect to login and restore form data previously entered

Overview
I have Spring Web-Application secured with Spring Security
On the site there is a form to input some data, this form is public, but the data will only be processed for authenticated users
If the user press the submit button and is not yet logged in, he will be delegated to the login page. Was the login successfull the user will be redirected to a site where the result of the data processing is visible
Problem
In standard configuration all the data which has been setup by the user are lost after the login process. As I understand it its because a new HttpRequest is created for the redirect after the login.
Solution
I have to write a custom LoginUrlAuthenticationEntryPoint which stores the form data in the session
I have to write a custom SavedRequestAwareAuthenticationSuccessHandler which reads the date from the session an add them as parameters to the url
WebApp Configuration
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private SecurityProperties security;
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("admin")
.roles("ADMIN", "USER")
.and()
.withUser("user")
.password("user")
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/inputForm")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(new SavedRequestAwareAuthenticationSuccessHandlerCustom())
.and()
.csrf()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.and()
.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPointCustom("/login"));
}
}
Custom SuccessHandler
public class SavedRequestAwareAuthenticationSuccessHandlerCustom extends SavedRequestAwareAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String text = (String) request.getSession().getAttribute("text");
if (text != null) {
request.getSession().removeAttribute("text");
setDefaultTargetUrl("/user/dashboard/?text=" + text);
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
Custom EntryPoint
public class LoginUrlAuthenticationEntryPointCustom extends LoginUrlAuthenticationEntryPoint {
public LoginUrlAuthenticationEntryPointCustom(String loginFormUrl) {
super(loginFormUrl);
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException,
ServletException {
String text = request.getParameter("text");
request.getSession().setAttribute("text", text);
super.commence(request, response, authException);
}
}
What would you say, is this a valid way to restore the form data, are the better/other solutions, maybe a standard way in spring?
Update
It seem's that something is still wrong with my configuration, cause as seen in the debug message, the request ist not saved by the "HttpSessionRequestCache". If I get this working I don't have to work around with custom implementations.
o.s.s.w.util.matcher.AndRequestMatcher : Trying to match using Ant [pattern='/**', GET]
o.s.s.w.u.matcher.AntPathRequestMatcher : Request 'POST /user/dashboard' doesn't match 'GET /**
o.s.s.w.util.matcher.AndRequestMatcher : Did not match
o.s.s.w.s.HttpSessionRequestCache : Request not saved as configured RequestMatcher did not match
kindly make sure that the form method is post
like this
<form th:action="#{/login}" method="post">
<!-- form input -- >
</form>

Spring MVC how to return to same view on login failed

I have Spring Security configured as:
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.formLogin()
.successHandler( successHandler() )
.loginPage("/login")
.loginProcessingUrl("/loginProcess")
.failureUrl( LOGIN_ERROR )}
How would be my controller for LOGIN_ERROR path in order to keep the user into same page but showing the expected error on login form?
And could be posible redirect user to Registration page when user failed to login 3 times?
Your first question's answer:
You can create a "CustomAuthenticationFailureHandler" by extending the AuthenticationFailureHandler class.
In CustomAuthenticationFailureHandler's onAuthenticationFailure method, write this code:
#Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException
{
saveException(request, exception);
redirectStrategy.sendRedirect(request, response, request.getHeader("referer"));
}
In your configuration file add this code instead of failureUrl:
#Bean
public CustomAuthenticationFailureHandler customAuthenticationFailureHandler()
{
return new CustomAuthenticationFailureHandler();
}
http.failureHandler(customAuthenticationFailureHandler())
Your second question's answer:
You can use a session or database to store failure attempts. Then after 3 failure attempts, you just need to change the url to redirect.
The common way is to return to the login page(failureUrl( "/login?error" )) and in the login form view:
<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>
That way when you navigate to login the first time error parameter will be empty the the error message will not be shown.

Resources