I have setup a LDAP Custom Authentication provider similar to the example here - https://www.baeldung.com/spring-security-authentication-provider
There is a Login Controller to handle login errors and to check if the user is in an approved list. The Controller calls the Custom authentication provider, the authenticationManager.authenticate()
method.
When wrong credentials are provided, the Custom Auth provider is called twice. Two exceptions are thrown.
First Exception:
31-12-2020 15:42:55.577 [http-nio-9090-exec-6] ERROR c.c.t.a.CustomAuthenticationProvider.hasAccess - test is not authenticated
javax.naming.AuthenticationException: [LDAP: error code 49 - 80090308: LdapErr: DSID-0C09044E, comment: AcceptSecurityContext error, data 52e, v2580 ]
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(Unknown Source) ~[na:1.8.0_261]
at com.sun.jndi.ldap.LdapCtx.processReturnCode(Unknown Source) ~[na:1.8.0_261]
at com.sun.jndi.ldap.LdapCtx.processReturnCode(Unknown Source) ~[na:1.8.0_261]
at com.sun.jndi.ldap.LdapCtx.connect(Unknown Source) ~[na:1.8.0_261]
at com.sun.jndi.ldap.LdapCtx.ensureOpen(Unknown Source) ~[na:1.8.0_261]
at com.sun.jndi.ldap.LdapCtx.ensureOpen(Unknown Source) ~[na:1.8.0_261]
at com.sun.jndi.ldap.LdapCtx.reconnect(Unknown Source) ~[na:1.8.0_261]
at javax.naming.ldap.InitialLdapContext.reconnect(Unknown Source) ~[na:1.8.0_261]
at com.tools.auth.CustomAuthenticationProvider.hasAccess(CustomAuthenticationProvider.java:65) [classes!/:1.0.0-SNAPSHOT]
at com.tools.auth.CustomAuthenticationProvider.authenticate(CustomAuthenticationProvider.java:32) [classes!/:1.0.0-SNAPSHOT]
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:174) [spring-security-core-5.0.3.RELEASE.jar!/:5.0.3.RELEASE]
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:199) [spring-security-core-5.0.3.RELEASE.jar!/:5.0.3.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:502) [spring-security-config-5.0.3.RELEASE.jar!/:5.0.3.RELEASE]
at com.tools.web.JwtAuthenticationRestController.authenticate(JwtAuthenticationRestController.java:70) [classes!/:1.0.0-SNAPSHOT]
Second Exception:
c.c.t.a.CustomAuthenticationProvider.authenticate - User does not have access
31-12-2020 15:42:55.613 [http-nio-9090-exec-6] ERROR c.c.t.w.JwtAuthenticationRestController.authenticate - Exception logging in user
org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:227) ~[spring-security-core-5.0.3.RELEASE.jar!/:5.0.3.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:502) ~[spring-security-config-5.0.3.RELEASE.jar!/:5.0.3.RELEASE]
at com.tools.web.JwtAuthenticationRestController.authenticate(JwtAuthenticationRestController.java:70) [classes!/:1.0.0-SNAPSHOT]
This is the Custom provider:
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if (hasAccess(name, password)) {
Authentication auth = new UsernamePasswordAuthenticationToken(name,
password);
return auth;
} else {
return null;
}
}
public boolean supports(Class<?> authentication) {
return true;
}
public boolean hasAccess(final String username, final String password) {
//LDAP access happens here
}
}
This is the Controller:
public class JwtAuthenticationRestController {
@Autowired
private AuthenticationManager authenticationManager;
@CrossOrigin
@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtTokenRequest authenticationRequest)
throws AuthenticationException {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
//generate token
return ResponseEntity.ok(new JwtTokenResponse(token));
}
@ExceptionHandler({AuthenticationException.class})
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) {
//handle exception. set custom response.
}
private void authenticate(String username, String password) {
try {
// Check against the approved user list
//Authenticate the user - Exception thrown here
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}catch (Exception e) {
throw new AuthenticationException("APPLICATION_ERROR", e);
}
}
}
Update Here is the Web Security configuration:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JWTWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUnAuthorizedResponseAuthenticationEntryPoint jwtUnAuthorizedResponseAuthenticationEntryPoint;
@Autowired
private JwtTokenAuthorizationOncePerRequestFilter jwtAuthenticationTokenFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new CustomAuthenticationProvider());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtUnAuthorizedResponseAuthenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.anyRequest().authenticated();
httpSecurity
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity
.headers()
.frameOptions().sameOrigin()
.cacheControl();
}
@Override
public void configure(WebSecurity webSecurity) {
webSecurity
.ignoring()
.antMatchers(
HttpMethod.POST,
"/authenticate" //authentication path
)
.antMatchers(HttpMethod.OPTIONS, "/**")
.and()
.ignoring()
.antMatchers(
HttpMethod.GET,
"\"login" //Ignore security for Login page.
)
.and()
.ignoring()
.antMatchers("/h2-console/**/**");
}
This happens only when authentication fails due to invalid password. I have checked that the Custom provider throws javax.naming.AuthenticationException
and returns null for invalis credentials.
Why does Spring throw this exception for failed authentication? The work around is to handle exception in the Controller as a login failure, but still it would be good to understand why this happens.