Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring security 5 "Bad credentials" exception not shown with errorDetails #4467

Closed
fred01 opened this issue Jul 25, 2017 · 19 comments
Closed

Comments

@fred01
Copy link

fred01 commented Jul 25, 2017

Summary

I'm just switch from Spring Boot 1.5.4 to 2.0.0.BUILD-SNAPSHOT. Most functionality migrate seamless, but i meet strange behavior of BadCredentialsException handling. In Spring 4 it was show as all other exceptions, like

{
    "error": "Unauthorized",
    "message": "Bad credentials",
    "path": "/v1/admin/users",
    "status": 401,
    "timestamp": "2017-07-25T10:53:13+0000"
}

But now just empty response with code 401 produced. All other spring security exceptions like "Forbidden" shown as expected in JSON.

Actual Behavior

Just

HTTP/1.1 401

on BadCredentialsException

Expected Behavior

Full JSON body

{
    "error": "Unauthorized",
    "message": "Bad credentials",
    "path": "/v1/admin/users",
    "status": 401,
    "timestamp": "2017-07-25T10:53:13+0000"
}

on BadCredentialsException

Configuration

Only default spring security properties, no additional properties set

Version

Spring Boot 2.0.0 SNAPSHOT, Spring Framework 5.0.0.M3

Sample

Sorry, part of production project

@fred01
Copy link
Author

fred01 commented Aug 16, 2017

Solved by myself. In Spring Security 5.0 necessary to permit all access to /error endpoint, for all http methods

@fred01 fred01 closed this as completed Aug 16, 2017
@mzgg
Copy link

mzgg commented Apr 15, 2018

Hello, I have same problem, Could you explain that how are solve that in detail

@fred01
Copy link
Author

fred01 commented Apr 16, 2018

Hi!
Actually it's Spring Boot 2 related issue, so i close it here.
I meet this problem twice.
First time i solved it by adding
.antMatchers("/error").permitAll()
Sometime later new Spring Boot 2 milestone broke it again. and second time it was broken completely. I make workaround then, but it was worst solution even i did. I'd intercept Spring Boot error controller and replace error in response.

@otidh
Copy link

otidh commented Sep 14, 2018

Thanks. Your workaround works for me.

@jzheaux
Copy link
Contributor

jzheaux commented Sep 14, 2018

Actually, this was a proactive decision in the 2.x release of Boot, though I think we should do a better job of explaining the rationale (for which I've just logged a ticket to the Boot team).

The ticket also includes some of the reasoning, too, but I'll briefly summarize here:

  • Spring Security secures all endpoints by default. (You can see this is the case by looking at the default implementation of configure in WebSecurityConfigurerAdapter). It was surprising (and less secure) that somehow /error wasn't included in the set of "all endpoints".
  • And the way that Spring Boot excluded /error from Spring Security was actually to bypass the filter chain altogether, meaning that secure headers, https redirect, and other important security protections were not invoked.

So, actually, yes, if you want the Spring Boot /error page to be permitted, then it is more secure for you to declaratively say so. This makes it clear in your app what security allowances you are making.

@bhartishradha
Copy link

HI

I recently did upgrade of spring boot 2.1.7 and suddently I was not getting any error message ..I found some idea with this issue
so to fix this ..i have given this
antMatchers("/error").permitAll()
but now I m getting the error message but message are coming different
so previously before upgrade when i was putting wrong username/pwd ...the response was
{
"timestamp": 1571049553776,
"status": 401,
"error": "Unauthorized",
"message": "Authentication Failed: {"errorCode":"52e","adminMail":"System Administrator","role":[]}",
"path": "/login/auth"
}
and now after upgrade,this is the response
{
"timestamp": "2019-10-14T10:40:37.651+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/login/auth"
}

Let me know if somebody can help me for this

@tjmjansen
Copy link

HI

I recently did upgrade of spring boot 2.1.7 and suddently I was not getting any error message ..I found some idea with this issue
so to fix this ..i have given this
antMatchers("/error").permitAll()
but now I m getting the error message but message are coming different
so previously before upgrade when i was putting wrong username/pwd ...the response was
{
"timestamp": 1571049553776,
"status": 401,
"error": "Unauthorized",
"message": "Authentication Failed: {"errorCode":"52e","adminMail":"System Administrator","role":[]}",
"path": "/login/auth"
}
and now after upgrade,this is the response
{
"timestamp": "2019-10-14T10:40:37.651+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/login/auth"
}

Let me know if somebody can help me for this

I have the same issue, did you find a solution?

@bhartishradha
Copy link

HI
I recently did upgrade of spring boot 2.1.7 and suddently I was not getting any error message ..I found some idea with this issue
so to fix this ..i have given this
antMatchers("/error").permitAll()
but now I m getting the error message but message are coming different
so previously before upgrade when i was putting wrong username/pwd ...the response was
{
"timestamp": 1571049553776,
"status": 401,
"error": "Unauthorized",
"message": "Authentication Failed: {"errorCode":"52e","adminMail":"System Administrator","role":[]}",
"path": "/login/auth"
}
and now after upgrade,this is the response
{
"timestamp": "2019-10-14T10:40:37.651+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/login/auth"
}
Let me know if somebody can help me for this

I have the same issue, did you find a solution?

No ..
I am still trying to find the solution

@bhartishradha
Copy link

HI
I recently did upgrade of spring boot 2.1.7 and suddently I was not getting any error message ..I found some idea with this issue
so to fix this ..i have given this
antMatchers("/error").permitAll()
but now I m getting the error message but message are coming different
so previously before upgrade when i was putting wrong username/pwd ...the response was
{
"timestamp": 1571049553776,
"status": 401,
"error": "Unauthorized",
"message": "Authentication Failed: {"errorCode":"52e","adminMail":"System Administrator","role":[]}",
"path": "/login/auth"
}
and now after upgrade,this is the response
{
"timestamp": "2019-10-14T10:40:37.651+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/login/auth"
}
Let me know if somebody can help me for this

I have the same issue, did you find a solution?

HI
I wrote a customefailure handler and with that it worked

@tjmjansen
Copy link

How did you do that? I tried but exceptions are still coming out in the same manner

@rubensa
Copy link
Contributor

rubensa commented May 4, 2021

I'm facing this problem too.
The most unexpected behavior is that if user credentials are not included at all then a 401 Unauthorized response that includes JSON response (like with any exception) is returned, but if wrong credentials are provided then a 401 Unauthorized is returned but without any JSON.
I think this is not very "consistent" behavior.

@bhartishradha
Copy link

no it wokred after applying some code.
let me forward that code

@bhartishradha
Copy link

i created an object of UsernamePasswordAuthenticationFilter and set setAuthenticationFailureHandler
like below
UsernamePasswordAuthenticationFilter authFilter = new UsernamePasswordAuthenticationFilter();

    AuthenticationProvider provider = initAuthenticationProvider();
    authFilter.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));

    // set redirect on success and submit form params
    SavedRequestAwareAuthenticationSuccessHandler authOkHandler =
        new SavedRequestAwareAuthenticationSuccessHandler();
    authOkHandler.setDefaultTargetUrl("/portal/index.html");
    authFilter.setAuthenticationSuccessHandler(authOkHandler);
    authFilter.setFilterProcessesUrl("/login/auth");
    authFilter.setUsernameParameter("username");
    authFilter.setPasswordParameter("password");
    authFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler());

the create a bean
@bean
public AuthenticationFailureHandler customAuthenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}

and here is the implementation of CustomAuthenticationFailureHandler(i wrote according to the logic of my application

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler{
private ObjectMapper objectMapper = new ObjectMapper();
private static final Logger log = LogManager.getLogger(CustomAuthenticationFailureHandler.class);
public CustomAuthenticationFailureHandler() {
log.info("CustomAuthenticationFailureHandler :started");
}
@OverRide
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {

    log.info("CustomAuthenticationFailureHandler :Method >>started");
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    Map<String, Object> data = new LinkedHashMap<String, Object>();
    data.put(
      "timestamp", 
      Calendar.getInstance().getTime());
    
    data.put(
             "status", 
             HttpStatus.UNAUTHORIZED.value());
   
    data.put(
             "error", 
             HttpStatus.UNAUTHORIZED.getReasonPhrase());
    data.put(
             "message", "Authentication Failed: "+
             exception.getMessage());
    data.put("path", request.getRequestURI());

    response.getOutputStream()
      .println(objectMapper.writeValueAsString(data));
    
}

@rubensa
Copy link
Contributor

rubensa commented May 4, 2021

@bhartishradha thanks for your code but I was talking about the inconsistency in the "default" implementation (If you override the behavior you can do "almost" anything you want).
I opened an issue here: spring-projects/spring-boot#26356

@Sanskar49
Copy link

Hey @bhartishradha I too am facing the same issue or could be a little different. I am getting a "org.springframework.security.authentication.BadCredentialsException: Bad credentials" when I hit my API through Postman(Post req) and eventhough my creds are correct, it still is giving me this error? Did you have the same problem? I saw your code.. The part where you created an object of UsernamePasswordAuthenticationFilter and then did all the authFilter.setAuthenticationSuccessHandler(authOkHandler);
authFilter.setFilterProcessesUrl("/login/auth"); I wanted to ask where you did this? In your controller method or where speceficly? I am getting this error since 2 days.. If anyone could help that'd be great.

@brytkhalifa
Copy link

@Sanskar49 Where you able to solve this problem?

@sebasira
Copy link

I'm having the same problem!

I'm getting this:
{
"timestamp": "2019-10-14T10:40:37.651+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/login/auth"
}

instead of the custom message I was having before. Could anyone help here?

@sebasira
Copy link

I resolved by setting the AuthenticationFailureHandler in my JWTAuthenticationFilter that extends UsernamePasswordAuthenticationFilter

The answer from @bhartishradha give me a clue

@m-usman-24
Copy link

m-usman-24 commented Aug 24, 2023

Writing for you all spent 5hrs on the api solution, I am using spring security 6+ and spring boot 3.1.1 solved this issue for simple form login like this

`
@AllArgsConstructor
@EqualsAndHashCode
@configuration
@EnableWebSecurity
public class SecurityConfig {

private final UserDetailsService userDetailsService;
private final CustomAuthenticationFailureHandler failureHandler;
private final PasswordEncoder passwordEncoder;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	
	http
		.authorizeHttpRequests(configurer ->
			configurer

// these requestMatchers here are for static resources as /static is allowed but the sub paths of static folder are not allowed by default
.requestMatchers("/css/")
.permitAll()
.requestMatchers("/javascript/
")
.permitAll()
// these are now controller endpoints
.requestMatchers("/login/")
.permitAll()
.requestMatchers("/signup/
")
.permitAll()
.requestMatchers("/forgot/**")
.permitAll()
.anyRequest()
.authenticated()
)

// here is the form login I am generating views using @controller annotation in my controllers here

		.formLogin(configurer ->
			configurer
				.loginPage("/login")
				.loginProcessingUrl("/login")

// this is the custom faliure handler

				.failureHandler(failureHandler)
				.defaultSuccessUrl("/notes", true)
		)
		.authenticationProvider(daoAuthenticationProvider());
		
	return http.build();
}

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
	
	DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
	provider.setUserDetailsService(userDetailsService);
	provider.setPasswordEncoder(passwordEncoder);
	
	return provider;
}

}

`

Here is the implementation of custom failure handler

`

@component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@OverRide
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {

	String exceptionName = exception.getClass().getSimpleName();
	StringBuilder failureUrl = new StringBuilder("/login?error=");
	
	for (int i = 0 ; i < exceptionName.length() ; i++) {
		char ch = exceptionName.charAt(i);
		if (Character.isUpperCase(ch)) {
			failureUrl.append(Character.toLowerCase(ch));
		}
	}
	
	getRedirectStrategy().sendRedirect(request, response, failureUrl.toString());
}

}

`

now from here user is redirected back to login page with added request param after that I am using thymeleaf

`

Bad Credentials

    <div data-th-class="${#strings.equals(param.error, 'aee')} ? alertNegative"
         data-th-if="${#strings.equals(param.error, 'aee')}"><p>Your Account is Expired</p></div>

    <div data-th-class="${#strings.equals(param.error, 'le')} ? alertNegative"
         data-th-if="${#strings.equals(param.error, 'le')}"><p>Your Account is disable, verify your email to
      enable it</p></div>

    <div data-th-class="${#strings.equals(param.error, 'de')} ? alertNegative"
         data-th-if="${#strings.equals(param.error, 'de')}"><p>Your Account is Disabled</p></div>

    <div data-th-class="${#strings.equals(param.error, 'cee')} ? alertNegative"
         data-th-if="${#strings.equals(param.error, 'cee')}"><p>Your Credentials are Expired</p></div>

    <div data-th-class="${errorMessage != null} ? alertNegative">
      <p data-th-text="${errorMessage} ?: _"></p>
    </div>

`

** What I did above was to generate views and for form login to handle exceptions for my custom implementation of login page **

like this attached image
Screenshot (109)

For http basic auth and rest apis I did this duct taping below

here is my security config

`

@requiredargsconstructor
@configuration
@EnableWebSecurity
public class SecurityConfig {

private final AuthenticationProvider authenticationProvider;
private final CustomAuthenticationEntryPoint entryPoint;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	
	return http
		.authorizeHttpRequests(customizer ->
			customizer
				.requestMatchers("/error")
				.permitAll()
				.requestMatchers("/letschat/auth/test", "/letschat/auth/update")
				.authenticated()
				.requestMatchers("/letschat/auth**")
				.permitAll()
				.anyRequest()
				.authenticated()
		)
		.csrf(AbstractHttpConfigurer::disable)
		.authenticationProvider(authenticationProvider)
		.httpBasic(customizer ->
			customizer
				.authenticationEntryPoint(entryPoint)
		)
		.build();
}

}

`

this is the important part above and also permitting the /error endpoint

** .httpBasic(customizer ->
customizer
.authenticationEntryPoint(entryPoint)
)**

now this is the implementation of CustomAuthenticationEntryPoint

`

@component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@OverRide
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {

	if (authException instanceof BadCredentialsException) {
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Bad Credentials");
		
	} else if (authException instanceof LockedException ||
			   authException instanceof DisabledException) {
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Account Locked, Unverified Email");
		
	} else {
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized, Unknown Reasons");
	}
}

}

`

I said duct taping because you cannot change these status codes as they are static final int but you can change the messages as you see above I handled disabled exception above to tell the user that you haven't verified your email

so

BEFORE

{
"timestamp": "2023-08-25T00:16:13.723+00:00",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorised",
"path": "/letschat/auth/test"
}

AFTER

{
"timestamp": "2023-08-25T00:28:45.729+00:00",
"status": 401,
"error": "Unauthorized",
"message": "Account Locked, Unverified Email",
"path": "/letschat/auth/test"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests