We have taken the sec-server-win-auth sample application from the Spring Security Kerberos documentation and extended it by a RestController
. In this RestController
we have defined some GET
- and POST
-mappings to handle the corresponding requests.
Additionally, we are using swagger to try out the requests.
After setting up an Active Directory server we can start up the application and upon opening the swagger-endpoint https://myserver.test.local/swagger-ui/index.html
, the user is prompted with the Windows-login window. After authenticating, the swagger UI opens and one can try out the requests.
The GET
requests are working fine, but after executing a POST
request, the repsonse body contains the HTML-code from the login page, together with the message "Invalid username and password.":
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Kerberos Example</title>
<link rel="icon" href="data:,">
</head>
<body>
<div>
Invalid username and password.
</div>
<form action="/login" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
After digging and debugging we found, that during the spnego-authentication protocol, a request to /login
is performed. This is not a problem if one is performing a GET
request on e.g., /config
, but if a POST
request on /config
is executed, the following happens
2024-05-08 11:30:13,508 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Securing POST /config
2024-05-08 11:30:13,509 [DEBUG|org.springframework.security.web.authentication.AnonymousAuthenticationFilter|AnonymousAuthenticationFilter] Set SecurityContextHolder to anonymous SecurityContext
2024-05-08 11:30:13,509 [DEBUG|org.springframework.security.web.savedrequest.HttpSessionRequestCache|HttpSessionRequestCache] Saved request https://myserver.test.local/config?continue to session
2024-05-08 11:30:13,510 [DEBUG|org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint|SpnegoEntryPoint] Add header WWW-Authenticate:Negotiate to https://myserver.test.local/config, forward: /login
2024-05-08 11:30:13,515 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Securing POST /login
2024-05-08 11:30:13,517 [DEBUG|org.springframework.security.web.DefaultRedirectStrategy|DefaultRedirectStrategy] Redirecting to /login?error
2024-05-08 11:30:13,525 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Securing GET /login?error
2024-05-08 11:30:13,527 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Secured GET /login?error
2024-05-08 11:30:13,528 [DEBUG|org.springframework.web.servlet.DispatcherServlet|LogFormatUtils] GET "/login?error", parameters={masked}
2024-05-08 11:30:13,528 [DEBUG|org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping|AbstractHandlerMapping] Mapped to org.example.MainController#login()
2024-05-08 11:30:13,532 [DEBUG|org.springframework.web.servlet.DispatcherServlet|FrameworkServlet] Completed 200 OK
2024-05-08 11:30:13,532 [DEBUG|org.springframework.security.web.authentication.AnonymousAuthenticationFilter|AnonymousAuthenticationFilter] Set SecurityContextHolder to anonymous SecurityContext
For some reason, the AbstractLdapAuthenticationProvider::authenticate
method gets called and throws a BadCredentialsException
. Using the debugger we found out that the variables username
and password
in line 68 and 69 are empty strings. So the web-app thinks the user has entered bad credentials and responds with the login page, together with the message "Invalid username and password".
We suspect that the reason why the AbstractLdapAuthenticationProvider
is called is because a POST
request on /login
was performed, which also happens if one clicks the login-button after entering username and password on the /login
page.
It seems, that during the spnego-protocol, a request on the /login
page is performed using the same HTTP-method as the initial request (we have also tried it with a DELETE
).
Our question(s):
AbstractLdapAuthenticationProvider
called in the first place? Can we disable it and only use the KerberosServiceAuthenticationProvider
?forward
during spnego authentication? (Add header WWW-Authenticate:Negotiate to https://myserver.test.local/config, forward: /login
)This is our WebSecurityConfig
(modified from the sample):
/* imports omitted */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Autowired
private SpringConfig config;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider();
ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider = activeDirectoryLdapAuthenticationProvider();
ProviderManager providerManager = new ProviderManager(kerberosServiceAuthenticationProvider, activeDirectoryLdapAuthenticationProvider);
http
.authorizeHttpRequests(authz -> authz
.anyRequest()
.authenticated()
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(spnegoEntryPoint())
)
.formLogin(formLogin -> formLogin
.loginPage(config.getActiveDirectoryLoginSerlvet())
.permitAll()
)
.logout(logout -> logout
.permitAll()
)
.authenticationProvider(activeDirectoryLdapAuthenticationProvider)
.authenticationProvider(kerberosServiceAuthenticationProvider)
.addFilterBefore(spnegoAuthenticationProcessingFilter(providerManager), BasicAuthenticationFilter.class)
.csrf(csrf -> csrf
.disable()
);
return http.build();
}
@Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
return new ActiveDirectoryLdapAuthenticationProvider(config.getActiveDirectoryDomain(), config.getActiveDirectoryServer());
}
@Bean
public SpnegoEntryPoint spnegoEntryPoint() {
return new SpnegoEntryPoint(config.getActiveDirectoryLoginSerlvet());
}
// @Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
filter.setAuthenticationManager(authenticationManager);
return filter;
}
public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() throws Exception {
KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
provider.setTicketValidator(sunJaasKerberosTicketValidator());
provider.setUserDetailsService(ldapUserDetailsService());
return provider;
}
@Bean
public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
ticketValidator.setServicePrincipal(config.getActiveDirectoryServicePrincipal());
ticketValidator.setKeyTabLocation(new FileSystemResource(config.getActiveDirectoryKeytabLocation()));
ticketValidator.setDebug(true);
return ticketValidator;
}
@Bean
public KerberosLdapContextSource kerberosLdapContextSource() throws Exception {
KerberosLdapContextSource contextSource = new KerberosLdapContextSource(config.getActiveDirectoryServer());
contextSource.setLoginConfig(loginConfig());
return contextSource;
}
public SunJaasKrb5LoginConfig loginConfig() throws Exception {
SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
loginConfig.setKeyTabLocation(new FileSystemResource(config.getActiveDirectoryKeytabLocation()));
loginConfig.setServicePrincipal(config.getActiveDirectoryServicePrincipal());
loginConfig.setDebug(true);
loginConfig.setIsInitiator(true);
loginConfig.afterPropertiesSet();
return loginConfig;
}
@Bean
public LdapUserDetailsService ldapUserDetailsService() throws Exception {
FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(config.getActiveDirectoryLdapSearchBase(), config.getActiveDirectoryLdapSearchFilter(), kerberosLdapContextSource());
LdapUserDetailsService service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator());
service.setUserDetailsMapper(new LdapUserDetailsMapper());
return service;
}
}
Thank you!
Edit: Added links to spring sample and to github repo of AuthenticatinoProvider
TLDR: Use the SpnegoEntryPoint()
constructor instead of the SpnegoEntryPoint(String forwardUrl)
.
After some more digging I have figured it out:
In the sample the SpnegoEntryPoint
is constructed with the SpnegoEntryPoint(String forwardUrl)
constructur where forwardUrl
= "/login"
. Since the initial request is a POST
request and the SpnegoEntryPoint
forwards to "/login"
(see line 105 in SpnegoEntryPoint.java), the AbstractLdapAuthenticationProvider::authenticate
gets called and throws the exception.
If one uses the SpnegoEntryPoint()
constructor without any arguments, the problem is solved completely and POST
requests work as well.
The only downside I have experienced so far is that, if one does not want to use Kerberos to authenticate against the app but standard LDAP password querying, he/she has to type in the "/login"
endpoint explicitly, the redirection to this endpoint if one cancels the Windows-login window is turned off.