Переглянути джерело

Add Spring Security Kerberos

Move the Spring Security Kerberos Extension into Spring Security

Closes gh-17879
Rob Winch 1 тиждень тому
батько
коміт
f5fb127c8c
69 змінених файлів з 6173 додано та 0 видалено
  1. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png
  2. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png
  3. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png
  4. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png
  5. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png
  6. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png
  7. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png
  8. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png
  9. BIN
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie2.png
  10. 118 0
      docs/modules/ROOT/examples/kerberos/AuthProviderConfig.java
  11. 27 0
      docs/modules/ROOT/examples/kerberos/AuthProviderConfigTest.java
  12. 35 0
      docs/modules/ROOT/examples/kerberos/DummyUserDetailsService.java
  13. 67 0
      docs/modules/ROOT/examples/kerberos/KerberosLdapContextSourceConfig.java
  14. 38 0
      docs/modules/ROOT/examples/kerberos/KerberosRestTemplateConfig.java
  15. 151 0
      docs/modules/ROOT/examples/kerberos/SpnegoConfig.java
  16. 5 0
      docs/modules/ROOT/nav.adoc
  17. 473 0
      docs/modules/ROOT/pages/servlet/authentication/kerberos/appendix.adoc
  18. 3 0
      docs/modules/ROOT/pages/servlet/authentication/kerberos/index.adoc
  19. 5 0
      docs/modules/ROOT/pages/servlet/authentication/kerberos/introduction.adoc
  20. 225 0
      docs/modules/ROOT/pages/servlet/authentication/kerberos/samples.adoc
  21. 85 0
      docs/modules/ROOT/pages/servlet/authentication/kerberos/ssk.adoc
  22. 4 0
      docs/modules/ROOT/pages/whats-new.adoc
  23. 23 0
      kerberos/kerberos-client/spring-security-kerberos-client.gradle
  24. 355 0
      kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/KerberosRestTemplate.java
  25. 122 0
      kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/config/SunJaasKrb5LoginConfig.java
  26. 156 0
      kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/ldap/KerberosLdapContextSource.java
  27. 135 0
      kerberos/kerberos-client/src/test/java/org/springframework/security/kerberos/client/KerberosRestTemplateTests.java
  28. 10 0
      kerberos/kerberos-client/src/test/resources/log4j.properties
  29. 26 0
      kerberos/kerberos-client/src/test/resources/minikdc-krb5.conf
  30. 86 0
      kerberos/kerberos-client/src/test/resources/minikdc.ldiff
  31. 15 0
      kerberos/kerberos-core/spring-security-kerberos-core.gradle
  32. 72 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/JaasSubjectHolder.java
  33. 23 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthentication.java
  34. 72 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProvider.java
  35. 29 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosClient.java
  36. 132 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosMultiTier.java
  37. 122 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProvider.java
  38. 233 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceRequestToken.java
  39. 92 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidation.java
  40. 40 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidator.java
  41. 69 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosUsernamePasswordAuthenticationToken.java
  42. 78 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/GlobalSunJaasKerberosConfig.java
  43. 47 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/JaasUtil.java
  44. 153 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosClient.java
  45. 332 0
      kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidator.java
  46. 92 0
      kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProviderTests.java
  47. 173 0
      kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProviderTests.java
  48. 68 0
      kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosTicketValidationTests.java
  49. 91 0
      kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidatorTests.java
  50. 16 0
      kerberos/kerberos-test/spring-security-kerberos-test.gradle
  51. 88 0
      kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/KerberosSecurityTestcase.java
  52. 429 0
      kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/MiniKdc.java
  53. 192 0
      kerberos/kerberos-test/src/test/java/org/springframework/security/kerberos/test/TestMiniKdc.java
  54. 10 0
      kerberos/kerberos-test/src/test/resources/log4j.properties
  55. 25 0
      kerberos/kerberos-test/src/test/resources/minikdc-krb5.conf
  56. 47 0
      kerberos/kerberos-test/src/test/resources/minikdc.ldiff
  57. 19 0
      kerberos/kerberos-web/spring-security-kerberos-web.gradle
  58. 71 0
      kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java
  59. 320 0
      kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoAuthenticationProcessingFilter.java
  60. 142 0
      kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoEntryPoint.java
  61. 44 0
      kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfig.java
  62. 33 0
      kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfigTests.java
  63. 34 0
      kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/DummyUserDetailsService.java
  64. 80 0
      kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/SpnegoConfig.java
  65. 298 0
      kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoAuthenticationProcessingFilterTests.java
  66. 121 0
      kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoEntryPointTests.java
  67. 47 0
      kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/AuthProviderConfig.xml
  68. 63 0
      kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/SpnegoConfig.xml
  69. 12 0
      kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/appproperties.xml

BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png


BIN
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie2.png


+ 118 - 0
docs/modules/ROOT/examples/kerberos/AuthProviderConfig.java

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kerberos.docs;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
+import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
+import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+
+//tag::snippetA[]
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig {
+
+	@Value("${app.service-principal}")
+	private String servicePrincipal;
+
+	@Value("${app.keytab-location}")
+	private String keytabLocation;
+
+	@Bean
+	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+		KerberosAuthenticationProvider kerberosAuthenticationProvider = kerberosAuthenticationProvider();
+		KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider();
+		ProviderManager providerManager = new ProviderManager(kerberosAuthenticationProvider,
+				kerberosServiceAuthenticationProvider);
+
+		http
+			.authorizeHttpRequests((authz) -> authz
+				.requestMatchers("/", "/home").permitAll()
+				.anyRequest().authenticated()
+			)
+			.exceptionHandling()
+				.authenticationEntryPoint(spnegoEntryPoint())
+				.and()
+			.formLogin()
+				.loginPage("/login").permitAll()
+				.and()
+			.logout()
+				.permitAll()
+				.and()
+			.authenticationProvider(kerberosAuthenticationProvider())
+			.authenticationProvider(kerberosServiceAuthenticationProvider())
+			.addFilterBefore(spnegoAuthenticationProcessingFilter(providerManager),
+					BasicAuthenticationFilter.class);
+			return http.build();
+	}
+
+	@Bean
+	public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
+		KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
+		SunJaasKerberosClient client = new SunJaasKerberosClient();
+		client.setDebug(true);
+		provider.setKerberosClient(client);
+		provider.setUserDetailsService(dummyUserDetailsService());
+		return provider;
+	}
+
+	@Bean
+	public SpnegoEntryPoint spnegoEntryPoint() {
+		return new SpnegoEntryPoint("/login");
+	}
+
+	public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
+			AuthenticationManager authenticationManager) {
+		SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
+		filter.setAuthenticationManager(authenticationManager);
+		return filter;
+	}
+
+	@Bean
+	public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
+		KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
+		provider.setTicketValidator(sunJaasKerberosTicketValidator());
+		provider.setUserDetailsService(dummyUserDetailsService());
+		return provider;
+	}
+
+	@Bean
+	public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
+		SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
+		ticketValidator.setServicePrincipal(servicePrincipal);
+		ticketValidator.setKeyTabLocation(new FileSystemResource(keytabLocation));
+		ticketValidator.setDebug(true);
+		return ticketValidator;
+	}
+
+	@Bean
+	public DummyUserDetailsService dummyUserDetailsService() {
+		return new DummyUserDetailsService();
+	}
+}
+//end::snippetA[]

+ 27 - 0
docs/modules/ROOT/examples/kerberos/AuthProviderConfigTest.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ * file except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package org.springframework.security.kerberos.docs;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(locations= {"AuthProviderConfig.xml"})
+public class AuthProviderConfigTest {
+
+	@Test
+	public void configLoads() {}
+}

+ 35 - 0
docs/modules/ROOT/examples/kerberos/DummyUserDetailsService.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kerberos.docs;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+//tag::snippetA[]
+public class DummyUserDetailsService implements UserDetailsService {
+
+    @Override
+    public UserDetails loadUserByUsername(String username)
+            throws UsernameNotFoundException {
+        return new User(username, "notUsed", true, true, true, true,
+                AuthorityUtils.createAuthorityList("ROLE_USER"));
+    }
+
+}
+//end::snippetA[]

+ 67 - 0
docs/modules/ROOT/examples/kerberos/KerberosLdapContextSourceConfig.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kerberos.client.docs;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
+import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource;
+import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
+
+public class KerberosLdapContextSourceConfig {
+
+//tag::snippetA[]
+	@Value("${app.ad-server}")
+	private String adServer;
+
+	@Value("${app.service-principal}")
+	private String servicePrincipal;
+
+	@Value("${app.keytab-location}")
+	private String keytabLocation;
+
+	@Value("${app.ldap-search-base}")
+	private String ldapSearchBase;
+
+	@Value("${app.ldap-search-filter}")
+	private String ldapSearchFilter;
+
+	@Bean
+	public KerberosLdapContextSource kerberosLdapContextSource() {
+		KerberosLdapContextSource contextSource = new KerberosLdapContextSource(adServer);
+		SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
+		loginConfig.setKeyTabLocation(new FileSystemResource(keytabLocation));
+		loginConfig.setServicePrincipal(servicePrincipal);
+		loginConfig.setDebug(true);
+		loginConfig.setIsInitiator(true);
+		contextSource.setLoginConfig(loginConfig);
+		return contextSource;
+	}
+
+	@Bean
+	public LdapUserDetailsService ldapUserDetailsService() {
+		FilterBasedLdapUserSearch userSearch =
+				new FilterBasedLdapUserSearch(ldapSearchBase, ldapSearchFilter, kerberosLdapContextSource());
+		LdapUserDetailsService service = new LdapUserDetailsService(userSearch);
+		service.setUserDetailsMapper(new LdapUserDetailsMapper());
+		return service;
+	}
+//end::snippetA[]
+
+}

+ 38 - 0
docs/modules/ROOT/examples/kerberos/KerberosRestTemplateConfig.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kerberos.client.docs;
+
+import org.springframework.security.kerberos.client.KerberosRestTemplate;
+
+public class KerberosRestTemplateConfig {
+
+//tag::snippetA[]
+    public void doWithTicketCache() {
+        KerberosRestTemplate restTemplate =
+                new KerberosRestTemplate();
+        restTemplate.getForObject("http://neo.example.org:8080/hello", String.class);
+    }
+//end::snippetA[]
+
+//tag::snippetB[]
+    public void doWithKeytabFile() {
+        KerberosRestTemplate restTemplate =
+                new KerberosRestTemplate("/tmp/user2.keytab", "user2@EXAMPLE.ORG");
+        restTemplate.getForObject("http://neo.example.org:8080/hello", String.class);
+    }
+//end::snippetB[]
+
+}

+ 151 - 0
docs/modules/ROOT/examples/kerberos/SpnegoConfig.java

@@ -0,0 +1,151 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kerberos.docs;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
+import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
+import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource;
+import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
+import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
+import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
+import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+
+//tag::snippetA[]
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig {
+
+	@Value("${app.ad-domain}")
+	private String adDomain;
+
+	@Value("${app.ad-server}")
+	private String adServer;
+
+	@Value("${app.service-principal}")
+	private String servicePrincipal;
+
+	@Value("${app.keytab-location}")
+	private String keytabLocation;
+
+	@Value("${app.ldap-search-base}")
+	private String ldapSearchBase;
+
+	@Value("${app.ldap-search-filter}")
+	private String ldapSearchFilter;
+
+	@Bean
+	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+		KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider();
+		ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider = activeDirectoryLdapAuthenticationProvider();
+		ProviderManager providerManager = new ProviderManager(kerberosServiceAuthenticationProvider,
+				activeDirectoryLdapAuthenticationProvider);
+
+		http
+			.authorizeHttpRequests((authz) -> authz
+				.requestMatchers("/", "/home").permitAll()
+				.anyRequest().authenticated()
+			)
+			.exceptionHandling()
+				.authenticationEntryPoint(spnegoEntryPoint())
+				.and()
+			.formLogin()
+				.loginPage("/login").permitAll()
+				.and()
+			.logout()
+				.permitAll()
+				.and()
+			.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
+			.authenticationProvider(kerberosServiceAuthenticationProvider())
+			.addFilterBefore(spnegoAuthenticationProcessingFilter(providerManager),
+				BasicAuthenticationFilter.class);
+
+		return http.build();
+	}
+
+	@Bean
+	public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
+		return new ActiveDirectoryLdapAuthenticationProvider(adDomain, adServer);
+	}
+
+	@Bean
+	public SpnegoEntryPoint spnegoEntryPoint() {
+		return new SpnegoEntryPoint("/login");
+	}
+
+	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(servicePrincipal);
+		ticketValidator.setKeyTabLocation(new FileSystemResource(keytabLocation));
+		ticketValidator.setDebug(true);
+		return ticketValidator;
+	}
+
+	@Bean
+	public KerberosLdapContextSource kerberosLdapContextSource() throws Exception {
+		KerberosLdapContextSource contextSource = new KerberosLdapContextSource(adServer);
+		contextSource.setLoginConfig(loginConfig());
+		return contextSource;
+	}
+
+	public SunJaasKrb5LoginConfig loginConfig() throws Exception {
+		SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
+		loginConfig.setKeyTabLocation(new FileSystemResource(keytabLocation));
+		loginConfig.setServicePrincipal(servicePrincipal);
+		loginConfig.setDebug(true);
+		loginConfig.setIsInitiator(true);
+		loginConfig.afterPropertiesSet();
+		return loginConfig;
+	}
+
+	@Bean
+	public LdapUserDetailsService ldapUserDetailsService() throws Exception {
+		FilterBasedLdapUserSearch userSearch =
+				new FilterBasedLdapUserSearch(ldapSearchBase, ldapSearchFilter, kerberosLdapContextSource());
+		LdapUserDetailsService service =
+				new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator());
+		service.setUserDetailsMapper(new LdapUserDetailsMapper());
+		return service;
+	}
+}
+//end::snippetA[]

+ 5 - 0
docs/modules/ROOT/nav.adoc

@@ -62,6 +62,11 @@
 *** xref:servlet/authentication/runas.adoc[Run-As]
 *** xref:servlet/authentication/logout.adoc[Logout]
 *** xref:servlet/authentication/events.adoc[Authentication Events]
+** xref:servlet/authentication/kerberos/index.adoc[Kerberos]
+*** xref:servlet/authentication/kerberos/introduction.adoc[Introduction]
+*** xref:servlet/authentication/kerberos/ssk.adoc[Reference]
+*** xref:servlet/authentication/kerberos/samples.adoc[Samples]
+*** xref:servlet/authentication/kerberos/appendix.adoc[Appendices]
 ** xref:servlet/authorization/index.adoc[Authorization]
 *** xref:servlet/authorization/architecture.adoc[Authorization Architecture]
 *** xref:servlet/authorization/authorize-http-requests.adoc[Authorize HTTP Requests]

+ 473 - 0
docs/modules/ROOT/pages/servlet/authentication/kerberos/appendix.adoc

@@ -0,0 +1,473 @@
+[[appendices]]
+= Appendices
+:figures: servlet/authentication/kerberos
+:numbered!:
+
+[appendix]
+== Material Used in this Document
+Dummy UserDetailsService used in samples because we don't have a real
+user source.
+
+[source,java,indent=0]
+----
+include::example$kerberos/DummyUserDetailsService.java[tags=snippetA]
+----
+
+[appendix]
+== Crash Course to Kerberos
+In any authentication process there are usually a three parties
+involved.
+
+image::{figures}/drawio-kerb-cc1.png[]
+
+First is a `client` which sometimes is a client computer but in most
+of the scenarios it is the actual user sitting on a computer and
+trying to access resources. Then there is the `resource` user is trying
+to access. In this example it is a web server.
+
+Then there is a `Key Distribution Center` or `KDC`. In a case of
+Windows environment this would be a `Domain Controller`. `KDC` is the
+one which really brings everything together and thus is the most
+critical component in your environment. Because of this it is also
+considered as a single point of failure.
+
+Initially when `Kerberos` environment is setup and domain user
+principals created into a database, encryption keys are also
+created. These encryption keys are based on shared secrets(i.e. user
+password) and actual passwords are never kept in a clear text.
+Effectively `KDC` has its own key and other keys for domain users.
+
+Interestingly there is no communication between a `resource` and a
+`KDC` during the authentication process.
+
+image::{figures}/drawio-kerb-cc2.png[]
+
+When client wants to authenticate itself with a `resource` it first
+needs to communicate with a `KDC`. `Client` will craft a special package
+which contains encrypted and unencrypted parts. Unencrypted part
+contains i.e. information about a user and encrypted part other
+information which is part of a protocol. `Client` will encrypt package
+data with its own key.
+
+When `KDC` receives this authentication package from a client it
+checks who this `client` claims to be from an unencrypted part and based
+on that information it uses `client` decryption key it already have in
+its database. If this decryption is succesfull `KDC` knows that this
+`client` is the one it claims to be.
+
+What KDC returns to a client is a ticket called `Ticket Granting
+Ticket` which is signed by a KDC's own private key. Later when
+`client` sends back this ticket it can try to decrypt it and if that
+operation is succesfull it knows that it was a ticket it itself
+originally signed and gave to a `client`.
+
+image::{figures}/drawio-kerb-cc3.png[]
+
+When client wants to get a ticket which it can use to authenticate
+with a service, `TGT` is sent to `KDC` which then signs a service ticket
+with service's own key. This a moment when a trust between
+`client` and `service` is created. This service ticket contains data
+which only `service` itself is able to decrypt.
+
+image::{figures}/drawio-kerb-cc4.png[]
+
+When `client` is authenticating with a service it sends previously
+received service ticket to a service which then thinks that I don't
+know anything about this guy but he gave me an authentication ticket.
+What `service` can do next is try to decrypt that ticket and if that
+operation is succesfull it knows that only other party who knows my
+credentials is the `KDC` and because I trust him I can also trust that
+this client is a one he claims to be.
+
+[appendix]
+== Setup Kerberos Environments
+Doing a production setup of Kerberos environment is out of scope of
+this document but this appendix provides some help to get you
+started for setting up needed components for development.
+
+[[setupmitkerberos]]
+=== Setup MIT Kerberos
+First action is to setup a new realm and a database.
+
+[source,text,indent=0]
+----
+# kdb5_util create -s -r EXAMPLE.ORG
+Loading random data
+Initializing database '/var/lib/krb5kdc/principal' for realm 'EXAMPLE.ORG',
+master key name 'K/M@EXAMPLE.ORG'
+You will be prompted for the database Master Password.
+It is important that you NOT FORGET this password.
+Enter KDC database master key:
+Re-enter KDC database master key to verify:
+----
+
+`kadmin` command can be used to administer Kerberos environment but
+you can't yet use it because there are no admin users in a database.
+
+[source,text,indent=0]
+----
+root@neo:/etc/krb5kdc# kadmin
+Authenticating as principal root/admin@EXAMPLE.ORG with password.
+kadmin: Client not found in Kerberos database while initializing
+kadmin interface
+----
+
+Lets use `kadmin.local` command to create one.
+
+[source,text,indent=0]
+----
+root@neo:/etc/krb5kdc# kadmin.local
+Authenticating as principal root/admin@EXAMPLE.ORG with password.
+
+kadmin.local:  listprincs
+K/M@EXAMPLE.ORG
+kadmin/admin@EXAMPLE.ORG
+kadmin/changepw@EXAMPLE.ORG
+kadmin/cypher@EXAMPLE.ORG
+krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
+
+kadmin.local:  addprinc root/admin@EXAMPLE.ORG
+WARNING: no policy specified for root/admin@EXAMPLE.ORG; defaulting to
+no policy
+Enter password for principal "root/admin@EXAMPLE.ORG":
+Re-enter password for principal "root/admin@EXAMPLE.ORG":
+Principal "root/admin@EXAMPLE.ORG" created.
+----
+
+Then enable admins by modifying `kadm5.acl` file and restart Kerberos
+services.
+
+[source,text,indent=0]
+----
+# cat /etc/krb5kdc/kadm5.acl
+# This file Is the access control list for krb5 administration.
+*/admin *
+----
+
+Now you can use `kadmin` with previously created `root/admin`
+principal. Lets create our first user `user1`.
+
+[source,text,indent=0]
+----
+kadmin:  addprinc user1
+WARNING: no policy specified for user1@EXAMPLE.ORG; defaulting to no
+policy
+Enter password for principal "user1@EXAMPLE.ORG":
+Re-enter password for principal "user1@EXAMPLE.ORG":
+Principal "user1@EXAMPLE.ORG" created.
+----
+
+Lets create our second user `user2` and export a keytab file.
+
+[source,text,indent=0]
+----
+kadmin:  addprinc user2
+WARNING: no policy specified for user2@EXAMPLE.ORG; defaulting to no
+policy
+Enter password for principal "user2@EXAMPLE.ORG":
+Re-enter password for principal "user2@EXAMPLE.ORG":
+Principal "user2@EXAMPLE.ORG" created.
+
+kadmin:  ktadd -k /tmp/user2.keytab user2@EXAMPLE.ORG
+Entry for principal user2@EXAMPLE.ORG with kvno 2, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:/tmp/user2.keytab.
+Entry for principal user2@EXAMPLE.ORG with kvno 2, encryption type arcfour-hmac added to keytab WRFILE:/tmp/user2.keytab.
+Entry for principal user2@EXAMPLE.ORG with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/tmp/user2.keytab.
+Entry for principal user2@EXAMPLE.ORG with kvno 2, encryption type des-cbc-crc added to keytab WRFILE:/tmp/user2.keytab.
+----
+
+Lets create a service ticket for tomcat and export credentials to a
+keytab file named `tomcat.keytab`.
+
+[source,text,indent=0]
+----
+kadmin:  addprinc -randkey HTTP/neo.example.org@EXAMPLE.ORG
+WARNING: no policy specified for HTTP/neo.example.org@EXAMPLE.ORG;
+defaulting to no policy
+Principal "HTTP/neo.example.org@EXAMPLE.ORG" created.
+
+kadmin:  ktadd -k /tmp/tomcat.keytab HTTP/neo.example.org@EXAMPLE.ORG
+Entry for principal HTTP/neo.example.org@EXAMPLE.ORG with kvno 2, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:/tmp/tomcat2.keytab.
+Entry for principal HTTP/neo.example.org@EXAMPLE.ORG with kvno 2, encryption type arcfour-hmac added to keytab WRFILE:/tmp/tomcat2.keytab.
+Entry for principal HTTP/neo.example.org@EXAMPLE.ORG with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/tmp/tomcat2.keytab.
+Entry for principal HTTP/neo.example.org@EXAMPLE.ORG with kvno 2, encryption type des-cbc-crc added to keytab WRFILE:/tmp/tomcat2.keytab.
+----
+
+[[setupwinkerberos]]
+=== Setup Windows Domain Controller
+
+This was tested using `Windows Server 2012 R2`
+
+[TIP]
+====
+Internet is full of good articles and videos how to setup Windows AD
+but these two are quite usefull
+http://www.rackspace.com/knowledge_center/article/installing-active-directory-on-windows-server-2012[Rackspace] and
+http://social.technet.microsoft.com/wiki/contents/articles/12370.windows-server-2012-set-up-your-first-domain-controller-step-by-step.aspx[Microsoft
+Technet].
+====
+
+- Normal domain controller and active directory setup was done.
+- Used dns domain `example.org` and windows domain `EXAMPLE`.
+- I created various domain users like `user1`, `user2`, `user3`,
+  `tomcat` and set passwords to `Password#`.
+
+I eventually also added all ip's of my vm's to AD's dns server for
+that not to cause any trouble.
+
+[source,text]
+----
+Name: WIN-EKBO0EQ7TS7.example.org
+Address: 172.16.101.135
+
+Name: win8vm.example.org
+Address: 172.16.101.136
+
+Name: neo.example.org
+Address: 172.16.101.1
+----
+
+Service Principal Name(SPN) needs to be setup with `HTTP` and a
+server name `neo.example.org` where tomcat servlet container is run. This
+is used with `tomcat` domain user and its `keytab` is then used as a
+service credential.
+
+[source,text]
+----
+PS C:\> setspn -A HTTP/neo.example.org tomcat
+----
+
+I exported keytab file which is copied to linux server running tomcat.
+
+[source,text]
+----
+PS C:\> ktpass /out c:\tomcat.keytab /mapuser tomcat@EXAMPLE.ORG /princ HTTP/neo.example.org@EXAMPLE.ORG /pass Password# /ptype KRB5_NT_PRINCIPAL /crypto All
+ Targeting domain controller: WIN-EKBO0EQ7TS7.example.org
+ Using legacy password setting method
+ Successfully mapped HTTP/neo.example.org to tomcat.
+----
+
+[appendix]
+== Troubleshooting
+This appendix provides generic information about troubleshooting
+errors and problems.
+
+[IMPORTANT]
+====
+If you think environment and configuration is correctly setup, do
+double check and ask other person to check possible obvious mistakes
+or typos. Kerberos setup is generally very brittle and it is not
+always very easy to debug where the problem lies.
+====
+
+.Cannot find key of appropriate type to decrypt
+
+[source,text]
+----
+GSSException: Failure unspecified at GSS-API level (Mechanism level:
+Invalid argument (400) - Cannot find key of appropriate type to
+decrypt AP REP - RC4 with HMAC)
+----
+
+If you see abore error indicating missing key type, this will happen
+with two different use cases. Firstly your JVM may not support
+appropriate encryption type or it is disabled in your `krb5.conf`
+file.
+
+[source,text]
+----
+default_tkt_enctypes = rc4-hmac
+default_tgs_enctypes = rc4-hmac
+----
+
+Second case is less obvious and hard to track because it will lead
+into same error. This specific `GSSException` is throws also if you
+simply don't have a required encryption key which then may be caused
+by a misconfiguration in your kerberos server or a simply typo in your
+principal.
+
+.Using wrong kerberos configuration
+
+{zwsp} +
+
+In most system all commands and libraries will search kerberos
+configuration either from a default locations or special locations
+like JDKs. It's easy to get mixed up especially if working from unix
+systems, which already may have default settings to work with MIT
+kerberos, towards Windows domains.
+
+This is a specific example what happens with `ldapsearch` trying to
+query Windows AD using kerberos authentication.
+
+[source,text]
+----
+$ ldapsearch -H ldap://WIN-EKBO0EQ7TS7.example.org -b "dc=example,dc=org"
+SASL/GSSAPI authentication started
+ldap_sasl_interactive_bind_s: Local error (-2)
+  additional info: SASL(-1): generic failure: GSSAPI Error:
+  Unspecified GSS failure.  Minor code may provide more information
+  (No Kerberos credentials available)
+----
+
+Well that doesn't look good and is a simple indication that I don't
+have a valid kerberos tickets as shown below.
+
+[source,text]
+----
+$ klist
+klist: Credentials cache file '/tmp/krb5cc_1000' not found
+----
+
+We already have a keytab file we exported from Windows AD to be used
+with tomcat running on Linux. Lets try to use that to authenticate
+with Windows AD.
+
+You can have a dedicated config file which usually can be used with
+native Linux commands and JVMs via system propertys.
+
+[source,text]
+----
+$ cat krb5.ini
+[libdefaults]
+default_realm = EXAMPLE.ORG
+default_keytab_name = /tmp/tomcat.keytab
+forwardable=true
+
+[realms]
+EXAMPLE.ORG = {
+  kdc = WIN-EKBO0EQ7TS7.example.org:88
+}
+
+[domain_realm]
+example.org=EXAMPLE.ORG
+.example.org=EXAMPLE.ORG
+----
+
+Lets use that config and a keytab to get initial credentials.
+
+[source,text]
+----
+$ env KRB5_CONFIG=/path/to/krb5.ini kinit -kt tomcat.keytab HTTP/neo.example.org@EXAMPLE.ORG
+
+$ klist
+Ticket cache: FILE:/tmp/krb5cc_1000
+Default principal: HTTP/neo.example.org@EXAMPLE.ORG
+
+Valid starting     Expires            Service principal
+26/03/15 09:04:37  26/03/15 19:04:37  krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
+  renew until 27/03/15 09:04:37
+----
+
+Lets see what happens if we now try to do a simple query against
+Windows AD.
+
+[source,text]
+----
+$ ldapsearch -H ldap://WIN-EKBO0EQ7TS7.example.org -b "dc=example,dc=org"
+SASL/GSSAPI authentication started
+ldap_sasl_interactive_bind_s: Local error (-2)
+  additional info: SASL(-1): generic failure: GSSAPI Error:
+  Unspecified GSS failure.  Minor code may provide more information
+  (KDC returned error string: PROCESS_TGS)
+----
+
+This may be simply because `ldapsearch` is getting confused and simply
+using wrong configuration. You can tell `ldapsearch` to use a
+different configuration via `KRB5_CONFIG` env variable just like we
+did with `kinit`. You can also use `KRB5_TRACE=/dev/stderr` to get
+more verbose output of what native libraries are doing.
+
+[source,text]
+----
+$ env KRB5_CONFIG=/path/to/krb5.ini ldapsearch -H ldap://WIN-EKBO0EQ7TS7.example.org -b "dc=example,dc=org"
+
+$ klist
+Ticket cache: FILE:/tmp/krb5cc_1000
+Default principal: HTTP/neo.example.org@EXAMPLE.ORG
+
+Valid starting     Expires            Service principal
+26/03/15 09:11:03  26/03/15 19:11:03  krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
+  renew until 27/03/15 09:11:03
+  26/03/15 09:11:44  26/03/15 19:11:03
+  ldap/win-ekbo0eq7ts7.example.org@EXAMPLE.ORG
+    renew until 27/03/15 09:11:03
+----
+
+Above you can see what happened if query was successful by looking
+kerberos tickets. Now you can experiment with further query commands
+i.e. if you working with `KerberosLdapContextSource`.
+
+[source,text]
+----
+$ ldapsearch -H ldap://WIN-EKBO0EQ7TS7.example.org \
+-b "dc=example,dc=org" \
+"(| (userPrincipalName=user2@EXAMPLE.ORG)
+(sAMAccountName=user2@EXAMPLE.ORG))" \
+dn
+
+...
+# test user, example.org
+dn: CN=test user,DC=example,DC=org
+----
+
+[appendix]
+[[browserspnegoconfig]]
+== Configure Browsers for Spnego Negotiation
+
+=== Firefox
+Complete following steps to ensure that your Firefox browser is
+enabled to perform Spnego authentication.
+
+- Open Firefox.
+- At address field, type *about:config*.
+- In filter/search, type *negotiate*.
+- Parameter *network.negotiate-auth.trusted-uris* may be set to
+  default *https://* which doesn't work for you. Generally speaking
+  this parameter has to replaced with the server address if Kerberos
+  delegation is required.
+- It is recommended to use `https` for all communication.
+
+=== Chrome
+
+With Google Chrome you generally need to set command-line parameters
+order to white list servers with Chrome will negotiate.
+
+- on Windows machines (clients): Chrome shares the configuration with
+  Internet Explorer so if all changes were applied to IE (as described
+  in E.3), nothing has to be passed via command-line parameters.
+- on Linux/Mac OS machines (clients): the command-line parameter
+  `--auth-negotiate-delegate-whitelist` should only used if Kerberos
+  delegation is required (otherwise do not set this parameter).
+- It is recommended to use `https` for all communication.
+
+[source,text]
+----
+--auth-server-whitelist="*.example.com"
+--auth-negotiate-delegate-whitelist="*.example.com"
+----
+
+You can see which policies are enable by typing *chrome://policy/*
+into Chrome's address bar.
+
+With Linux Chrome will also read policy files from
+`/etc/opt/chrome/policies/managed` directory.
+
+.mypolicy.json
+[source,json]
+----
+{
+  "AuthServerWhitelist" : "*.example.org",
+  "AuthNegotiateDelegateWhitelist" : "*.example.org",
+  "DisableAuthNegotiateCnameLookup" : true,
+  "EnableAuthNegotiatePort" : true
+}
+----
+
+=== Internet Explorer
+Complete following steps to ensure that your Internet Explorer browser
+is enabled to perform Spnego authentication.
+
+- Open Internet Explorer.
+- Click *Tools > Intenet Options > Security* tab.
+- In *Local intranet* section make sure your server is trusted by i.e.
+  adding it into a list.
+

+ 3 - 0
docs/modules/ROOT/pages/servlet/authentication/kerberos/index.adoc

@@ -0,0 +1,3 @@
+= Spring Security Kerberos
+
+Spring Security Kerberos adds the ability to work with Kerberos and Spring applications.

+ 5 - 0
docs/modules/ROOT/pages/servlet/authentication/kerberos/introduction.adoc

@@ -0,0 +1,5 @@
+[[introduction]]
+= Introduction
+
+Spring Security Kerberos {spring-security-version} is built and tested with JDK 17,
+Spring Security {spring-security-version} and Spring Framework {spring-core-version}.

+ 225 - 0
docs/modules/ROOT/pages/servlet/authentication/kerberos/samples.adoc

@@ -0,0 +1,225 @@
+[[springsecuritykerberossamples]]
+= Spring Security Kerberos Samples
+:figures: servlet/authentication/kerberos
+
+This part of the reference documentation is introducing samples
+projects. Samples can be compiled manually by building main
+distribution from
+https://github.com/spring-projects/spring-security-kerberos.
+
+[IMPORTANT]
+====
+If you run sample as is it will not work until a correct configuration
+is applied. See notes below for specific samples.
+====
+
+<<samples-sec-server-win-auth>> sample for Windows environment
+
+<<samples-sec-server-client-auth>> sample using server side authenticator
+
+<<samples-sec-server-spnego-form-auth>> sample using ticket validation
+with spnego and form
+
+<<samples-sec-client-rest-template>> sample for KerberosRestTemplate
+
+[[samples-sec-server-win-auth]]
+== Security Server Windows Auth Sample
+Goals of this sample:
+
+- In windows environment, User will be able to logon to application
+  with Windows Active directory Credential which has been entered
+  during log on to windows. There should not be any ask for
+  userid/password credentials.
+- In non-windows environment, User will be presented with a screen
+  to provide Active directory credentials.
+
+[source,yaml,indent=0]
+----
+server:
+    port: 8080
+    app:
+        ad-domain: EXAMPLE.ORG
+        ad-server: ldap://WIN-EKBO0EQ7TS7.example.org/
+        service-principal: HTTP/neo.example.org@EXAMPLE.ORG
+        keytab-location: /tmp/tomcat.keytab
+        ldap-search-base: dc=example,dc=org
+        ldap-search-filter: "(| (userPrincipalName={0}) (sAMAccountName={0}))"
+----
+In above you can see the default configuration for this sample. You
+can override these settings using a normal Spring Boot tricks like
+using command-line options or custom `application.yml` file.
+
+Run a server.
+[source,text,subs="attributes"]
+----
+$ java -jar sec-server-win-auth-{spring-security-version}.jar
+----
+
+[IMPORTANT]
+====
+You may need to use custom kerberos config with Linux either by using
+`-Djava.security.krb5.conf=/path/to/krb5.ini` or
+`GlobalSunJaasKerberosConfig` bean.
+====
+
+[NOTE]
+====
+See xref:servlet/authentication/kerberos/appendix.adoc#setupwinkerberos[Setup Windows Domain Controller]
+for more instructions how to work with windows kerberos environment.
+====
+
+Login to `Windows 8.1` using domain credentials and access sample
+
+image::{figures}/ie1.png[]
+image::{figures}/ie2.png[]
+
+Access sample application from a non windows vm and use domain
+credentials manually.
+
+image::{figures}/ff1.png[]
+image::{figures}/ff2.png[]
+image::{figures}/ff3.png[]
+
+
+[[samples-sec-server-client-auth]]
+== Security Server Side Auth Sample
+This sample demonstrates how server is able to authenticate user
+against kerberos environment using his credentials passed in via a
+form login.
+
+Run a server.
+[source,text,subs="attributes"]
+----
+$ java -jar sec-server-client-auth-{spring-security-version}.jar
+----
+
+[source,yaml,indent=0]
+----
+server:
+    port: 8080
+----
+
+[[samples-sec-server-spnego-form-auth]]
+== Security Server Spnego and Form Auth Sample
+This sample demonstrates how a server can be configured to accept a
+Spnego based negotiation from a browser while still being able to fall
+back to a form based authentication.
+
+Using a `user1` principal xref:servlet/authentication/kerberos/appendix.adoc#setupmitkerberos[Setup MIT Kerberos],
+do a kerberos login manually using credentials.
+[source,text]
+----
+$ kinit user1
+Password for user1@EXAMPLE.ORG:
+
+$ klist
+Ticket cache: FILE:/tmp/krb5cc_1000
+Default principal: user1@EXAMPLE.ORG
+
+Valid starting     Expires            Service principal
+10/03/15 17:18:45  11/03/15 03:18:45  krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
+  renew until 11/03/15 17:18:40
+----
+
+or using a keytab file.
+
+[source,text]
+----
+$ kinit -kt user2.keytab user1
+
+$ klist
+Ticket cache: FILE:/tmp/krb5cc_1000
+Default principal: user2@EXAMPLE.ORG
+
+Valid starting     Expires            Service principal
+10/03/15 17:25:03  11/03/15 03:25:03  krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
+  renew until 11/03/15 17:25:03
+----
+
+Run a server.
+[source,text,subs="attributes"]
+----
+$ java -jar sec-server-spnego-form-auth-{spring-security-version}.jar
+----
+
+Now you should be able to open your browser and let it do Spnego
+authentication with existing ticket.
+
+[NOTE]
+====
+See xref:servlet/authentication/kerberos/appendix.adoc#browserspnegoconfig[Configure Browsers for Spnego Negotiation]
+for more instructions for configuring browsers to use Spnego.
+====
+
+[source,yaml,indent=0]
+----
+server:
+    port: 8080
+app:
+    service-principal: HTTP/neo.example.org@EXAMPLE.ORG
+    keytab-location: /tmp/tomcat.keytab
+----
+
+[[samples-sec-client-rest-template]]
+== Security Client KerberosRestTemplate Sample
+This is a sample using a Spring RestTemplate to access Kerberos
+protected resource. You can use this together with
+<<samples-sec-server-spnego-form-auth>>.
+
+Default application is configured as shown below.
+[source,yaml,indent=0]
+----
+app:
+    user-principal: user2@EXAMPLE.ORG
+    keytab-location: /tmp/user2.keytab
+    access-url: http://neo.example.org:8080/hello
+----
+
+
+Using a `user1` principal xref:servlet/authentication/kerberos/appendix.adoc#setupmitkerberos[Setup MIT Kerberos],
+do a kerberos login manually using credentials.
+[source,text,subs="attributes"]
+----
+$ java -jar sec-client-rest-template-{spring-security-version}.jar --app.user-principal --app.keytab-location
+----
+
+[NOTE]
+====
+In above we simply set `app.user-principal` and `app.keytab-location`
+to empty values which disables a use of keytab file.
+====
+
+If operation is succesfull you should see below output with `user1@EXAMPLE.ORG`.
+[source,text]
+----
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
+  <head>
+    <title>Spring Security Kerberos Example</title>
+  </head>
+  <body>
+    <h1>Hello user1@EXAMPLE.ORG!</h1>
+  </body>
+</html>
+----
+
+Or use a `user2` with a keytab file.
+[source,text,subs="attributes"]
+----
+$ java -jar sec-client-rest-template-{spring-security-version}.jar
+----
+
+If operation is succesfull you should see below output with `user2@EXAMPLE.ORG`.
+[source,text]
+----
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
+  <head>
+    <title>Spring Security Kerberos Example</title>
+  </head>
+  <body>
+    <h1>Hello user2@EXAMPLE.ORG!</h1>
+  </body>
+</html>
+----
+

+ 85 - 0
docs/modules/ROOT/pages/servlet/authentication/kerberos/ssk.adoc

@@ -0,0 +1,85 @@
+[[springsecuritykerberos]]
+= Spring and Spring Security Kerberos
+:figures: servlet/authentication/kerberos
+
+This part of the reference documentation explains the core functionality
+that Spring Security Kerberos provides to any Spring based application.
+
+<<ssk-authprovider>> describes the authentication provider support.
+
+<<ssk-spnego>> describes the spnego negotiate support.
+
+<<ssk-resttemplate>> describes the RestTemplate support.
+
+
+[[ssk-authprovider]]
+== Authentication Provider
+
+Provider configuration using JavaConfig.
+
+[source,java,indent=0]
+----
+include::example$kerberos/AuthProviderConfig.java[tags=snippetA]
+----
+
+[[ssk-spnego]]
+== Spnego Negotiate
+
+Spnego configuration using JavaConfig.
+
+[source,java,indent=0]
+----
+include::example$kerberos/SpnegoConfig.java[tags=snippetA]
+----
+
+[[ssk-resttemplate]]
+== Using KerberosRestTemplate
+
+If there is a need to access Kerberos protected web resources
+programmatically we have `KerberosRestTemplate` which extends
+`RestTemplate` and does necessary login actions prior to delegating to
+actual RestTemplate methods. You basically have few options to
+configure this template.
+
+- Leave keyTabLocation and userPrincipal empty if you want to
+  use cached ticket.
+- Use keyTabLocation and userPrincipal if you want to use
+  keytab file.
+- Use loginOptions if you want to customise Krb5LoginModule options.
+- Use a customised httpClient.
+
+With ticket cache.
+[source,java,indent=0]
+----
+include::example$kerberos/KerberosRestTemplateConfig.java[tags=snippetA]
+----
+
+With keytab file.
+[source,java,indent=0]
+----
+include::example$kerberos/KerberosRestTemplateConfig.java[tags=snippetB]
+----
+
+[[ssk-kerberosldap]]
+== Authentication with LDAP Services
+
+With most of your samples we're using `DummyUserDetailsService`
+because there is not necessarily need to query a real user details
+once kerberos authentication is successful and we can use kerberos
+principal info to create that dummy user. However there is a way to
+access kerberized LDAP services in a say way and query user details
+from there.
+
+`KerberosLdapContextSource` can be used to bind into LDAP via kerberos
+which is at least proven to work well with Windows AD services.
+
+[source,java,indent=0]
+----
+include::example$kerberos/KerberosLdapContextSourceConfig.java[tags=snippetA]
+----
+
+[TIP]
+====
+Sample xref:servlet/authentication/kerberos/samples.adoc#samples-sec-server-win-auth[Security Server Windows Auth Sample]
+is currently configured to query user details from AD if authentication happen via kerberos.
+====

+ 4 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -9,6 +9,10 @@ Below are the highlights of the release, or you can view https://github.com/spri
 Being a major release, there are a number of deprecated APIs that are removed in Spring Security 7.
 Each section that follows will indicate the more notable removals as well as the new features in that module
 
+== Modules
+
+* The https://github.com/spring-projects/spring-security-kerberos[Spring Security Kerberos Extension] is now part of Spring Security. See the xref:servlet/authentication/kerberos/index.adoc[Kerberos] section of the reference for details.
+
 == Core
 
 * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`

+ 23 - 0
kerberos/kerberos-client/spring-security-kerberos-client.gradle

@@ -0,0 +1,23 @@
+plugins {
+	id 'io.spring.convention.spring-module'
+}
+
+description = 'Spring Security Kerberos Client'
+
+dependencies {
+	management platform(project(":spring-security-dependencies"))
+	implementation project(':spring-security-kerberos-core')
+	implementation project(':spring-security-kerberos-web')
+	api('org.springframework:spring-web')
+	api libs.org.apache.httpcomponents.httpclient
+	optional project(':spring-security-ldap')
+	testImplementation project(':spring-security-kerberos-test')
+	testImplementation 'org.springframework:spring-test'
+	testImplementation project(':spring-security-config')
+	testImplementation 'org.junit.jupiter:junit-jupiter'
+	testImplementation 'org.mockito:mockito-junit-jupiter'
+	testImplementation libs.org.assertj.assertj.core
+	testImplementation 'com.squareup.okhttp3:mockwebserver'
+	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+

+ 355 - 0
kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/KerberosRestTemplate.java

@@ -0,0 +1,355 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.client;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.Credentials;
+import org.apache.hc.client5.http.auth.KerberosConfig;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+import org.apache.hc.client5.http.impl.auth.SPNegoSchemeFactory;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RequestCallback;
+import org.springframework.web.client.ResponseExtractor;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * {@code RestTemplate} that is able to make kerberos SPNEGO authenticated REST requests.
+ * Under a hood this {@code KerberosRestTemplate} is using {@link HttpClient} to support
+ * Kerberos.
+ *
+ * <p>
+ * Generally this template can be configured in few different ways.
+ * <ul>
+ * <li>Leave keyTabLocation and userPrincipal empty if you want to use cached ticket</li>
+ * <li>Use keyTabLocation and userPrincipal if you want to use keytab file</li>
+ * <li>Use userPrincipal and password if you want to use user/password</li>
+ * <li>Use loginOptions if you want to customise Krb5LoginModule options</li>
+ * <li>Use a customised httpClient</li>
+ * </ul>
+ *
+ * @author Janne Valkealahti
+ *
+ */
+public class KerberosRestTemplate extends RestTemplate {
+
+	private static final Credentials credentials = new NullCredentials();
+
+	private final String keyTabLocation;
+
+	private final String userPrincipal;
+
+	private final String password;
+
+	private final Map<String, Object> loginOptions;
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 */
+	public KerberosRestTemplate() {
+		this(null, null, null, null, buildHttpClient());
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param httpClient the http client
+	 */
+	public KerberosRestTemplate(HttpClient httpClient) {
+		this(null, null, null, null, httpClient);
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param keyTabLocation the key tab location
+	 * @param userPrincipal the user principal
+	 */
+	public KerberosRestTemplate(String keyTabLocation, String userPrincipal) {
+		this(keyTabLocation, userPrincipal, buildHttpClient());
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param keyTabLocation the key tab location
+	 * @param userPrincipal the user principal
+	 * @param httpClient the http client
+	 */
+	public KerberosRestTemplate(String keyTabLocation, String userPrincipal, HttpClient httpClient) {
+		this(keyTabLocation, userPrincipal, null, null, httpClient);
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param loginOptions the login options
+	 */
+	public KerberosRestTemplate(Map<String, Object> loginOptions) {
+		this(null, null, null, loginOptions, buildHttpClient());
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param loginOptions the login options
+	 * @param httpClient the http client
+	 */
+	public KerberosRestTemplate(Map<String, Object> loginOptions, HttpClient httpClient) {
+		this(null, null, null, loginOptions, httpClient);
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param keyTabLocation the key tab location
+	 * @param userPrincipal the user principal
+	 * @param loginOptions the login options
+	 */
+	public KerberosRestTemplate(String keyTabLocation, String userPrincipal, Map<String, Object> loginOptions) {
+		this(keyTabLocation, userPrincipal, null, loginOptions, buildHttpClient());
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param keyTabLocation the key tab location
+	 * @param userPrincipal the user principal
+	 * @param password the password
+	 * @param loginOptions the login options
+	 */
+	public KerberosRestTemplate(String keyTabLocation, String userPrincipal, String password,
+			Map<String, Object> loginOptions) {
+		this(keyTabLocation, userPrincipal, password, loginOptions, buildHttpClient());
+	}
+
+	/**
+	 * Instantiates a new kerberos rest template.
+	 * @param keyTabLocation the key tab location
+	 * @param userPrincipal the user principal
+	 * @param password the password
+	 * @param loginOptions the login options
+	 * @param httpClient the http client
+	 */
+	private KerberosRestTemplate(String keyTabLocation, String userPrincipal, String password,
+			Map<String, Object> loginOptions, HttpClient httpClient) {
+		super(new HttpComponentsClientHttpRequestFactory(httpClient));
+		this.keyTabLocation = keyTabLocation;
+		this.userPrincipal = userPrincipal;
+		this.password = password;
+		this.loginOptions = loginOptions;
+	}
+
+	/**
+	 * Builds the default instance of {@link HttpClient} having kerberos support.
+	 * @return the http client with spneno auth scheme
+	 */
+	private static HttpClient buildHttpClient() {
+		HttpClientBuilder builder = HttpClientBuilder.create();
+
+		Lookup<AuthSchemeFactory> authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
+			.register(StandardAuthScheme.SPNEGO,
+					new SPNegoSchemeFactory(KerberosConfig.custom()
+						.setStripPort(KerberosConfig.Option.ENABLE)
+						.setUseCanonicalHostname(KerberosConfig.Option.DISABLE)
+						.build(), SystemDefaultDnsResolver.INSTANCE))
+			.build();
+
+		builder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+		RequestConfig negotiate = RequestConfig.copy(RequestConfig.DEFAULT)
+			.setTargetPreferredAuthSchemes(Set.of(StandardAuthScheme.SPNEGO, StandardAuthScheme.KERBEROS))
+			.build();
+		builder.setDefaultRequestConfig(negotiate);
+		BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+		credentialsProvider.setCredentials(new AuthScope(null, -1), credentials);
+		builder.setDefaultCredentialsProvider(credentialsProvider);
+		CloseableHttpClient httpClient = builder.build();
+		return httpClient;
+	}
+
+	/**
+	 * Setup the {@link LoginContext} with credentials and options for authentication
+	 * against kerberos.
+	 * @return the login context
+	 */
+	private LoginContext buildLoginContext() throws LoginException {
+		ClientLoginConfig loginConfig = new ClientLoginConfig(this.keyTabLocation, this.userPrincipal, this.password,
+				this.loginOptions);
+		Set<Principal> princ = new HashSet<Principal>(1);
+		if (this.userPrincipal != null) {
+			princ.add(new KerberosPrincipal(this.userPrincipal));
+		}
+		Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>());
+		CallbackHandler callbackHandler = new CallbackHandlerImpl(this.userPrincipal, this.password);
+		LoginContext lc = new LoginContext("", sub, callbackHandler, loginConfig);
+		return lc;
+	}
+
+	@Override
+	protected final <T> T doExecute(final URI url, final String uriTemplate, final HttpMethod method,
+			final RequestCallback requestCallback, final ResponseExtractor<T> responseExtractor)
+			throws RestClientException {
+
+		try {
+			LoginContext lc = buildLoginContext();
+			lc.login();
+			Subject serviceSubject = lc.getSubject();
+			return Subject.doAs(serviceSubject, new PrivilegedAction<T>() {
+
+				@Override
+				public T run() {
+					return KerberosRestTemplate.this.doExecuteSubject(url, uriTemplate, method, requestCallback,
+							responseExtractor);
+				}
+			});
+
+		}
+		catch (Exception ex) {
+			throw new RestClientException("Error running rest call", ex);
+		}
+	}
+
+	private <T> T doExecuteSubject(URI url, String uriTemplate, HttpMethod method, RequestCallback requestCallback,
+			ResponseExtractor<T> responseExtractor) throws RestClientException {
+		return super.doExecute(url, uriTemplate, method, requestCallback, responseExtractor);
+	}
+
+	private static final class ClientLoginConfig extends Configuration {
+
+		private final String keyTabLocation;
+
+		private final String userPrincipal;
+
+		private final String password;
+
+		private final Map<String, Object> loginOptions;
+
+		private ClientLoginConfig(String keyTabLocation, String userPrincipal, String password,
+				Map<String, Object> loginOptions) {
+			super();
+			this.keyTabLocation = keyTabLocation;
+			this.userPrincipal = userPrincipal;
+			this.password = password;
+			this.loginOptions = loginOptions;
+		}
+
+		@Override
+		public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+
+			Map<String, Object> options = new HashMap<String, Object>();
+
+			// if we don't have keytab or principal only option is to rely on
+			// credentials cache.
+			if (!StringUtils.hasText(this.keyTabLocation) || !StringUtils.hasText(this.userPrincipal)) {
+				// cache
+				options.put("useTicketCache", "true");
+			}
+			else {
+				// keytab
+				options.put("useKeyTab", "true");
+				options.put("keyTab", this.keyTabLocation);
+				options.put("principal", this.userPrincipal);
+				options.put("storeKey", "true");
+			}
+
+			options.put("doNotPrompt", Boolean.toString(this.password == null));
+			options.put("isInitiator", "true");
+
+			if (this.loginOptions != null) {
+				options.putAll(this.loginOptions);
+			}
+
+			return new AppConfigurationEntry[] {
+					new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
+							AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) };
+		}
+
+	}
+
+	private static class NullCredentials implements Credentials {
+
+		@Override
+		public Principal getUserPrincipal() {
+			return null;
+		}
+
+		@Override
+		public char[] getPassword() {
+			return null;
+		}
+
+	}
+
+	private static final class CallbackHandlerImpl implements CallbackHandler {
+
+		private final String userPrincipal;
+
+		private final String password;
+
+		private CallbackHandlerImpl(String userPrincipal, String password) {
+			super();
+			this.userPrincipal = userPrincipal;
+			this.password = password;
+		}
+
+		@Override
+		public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+
+			for (Callback callback : callbacks) {
+				if (callback instanceof NameCallback) {
+					NameCallback nc = (NameCallback) callback;
+					nc.setName(this.userPrincipal);
+				}
+				else if (callback instanceof PasswordCallback) {
+					PasswordCallback pc = (PasswordCallback) callback;
+					pc.setPassword(this.password.toCharArray());
+				}
+				else {
+					throw new UnsupportedCallbackException(callback, "Unknown Callback");
+				}
+			}
+		}
+
+	}
+
+}

+ 122 - 0
kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/config/SunJaasKrb5LoginConfig.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.client.config;
+
+import java.util.HashMap;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.util.Assert;
+
+/**
+ * Implementation of {@link Configuration} which uses Sun's JAAS Krb5LoginModule.
+ *
+ * @author Nelson Rodrigues
+ * @author Janne Valkealahti
+ *
+ */
+public class SunJaasKrb5LoginConfig extends Configuration implements InitializingBean {
+
+	private static final Log LOG = LogFactory.getLog(SunJaasKrb5LoginConfig.class);
+
+	private String servicePrincipal;
+
+	private Resource keyTabLocation;
+
+	private Boolean useTicketCache = false;
+
+	private Boolean isInitiator = false;
+
+	private Boolean debug = false;
+
+	private String keyTabLocationAsString;
+
+	public void setServicePrincipal(String servicePrincipal) {
+		this.servicePrincipal = servicePrincipal;
+	}
+
+	public void setKeyTabLocation(Resource keyTabLocation) {
+		this.keyTabLocation = keyTabLocation;
+	}
+
+	public void setUseTicketCache(Boolean useTicketCache) {
+		this.useTicketCache = useTicketCache;
+	}
+
+	public void setIsInitiator(Boolean isInitiator) {
+		this.isInitiator = isInitiator;
+	}
+
+	public void setDebug(Boolean debug) {
+		this.debug = debug;
+	}
+
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		Assert.hasText(this.servicePrincipal, "servicePrincipal must be specified");
+
+		if (this.keyTabLocation != null && this.keyTabLocation instanceof ClassPathResource) {
+			LOG.warn(
+					"Your keytab is in the classpath. This file needs special protection and shouldn't be in the classpath. JAAS may also not be able to load this file from classpath.");
+		}
+
+		if (!this.useTicketCache) {
+			Assert.notNull(this.keyTabLocation, "keyTabLocation must be specified when useTicketCache is false");
+		}
+
+		if (this.keyTabLocation != null) {
+			this.keyTabLocationAsString = this.keyTabLocation.getURL().toExternalForm();
+			if (this.keyTabLocationAsString.startsWith("file:")) {
+				this.keyTabLocationAsString = this.keyTabLocationAsString.substring(5);
+			}
+		}
+	}
+
+	@Override
+	public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+		HashMap<String, String> options = new HashMap<>();
+
+		options.put("principal", this.servicePrincipal);
+
+		if (this.keyTabLocation != null) {
+			options.put("useKeyTab", "true");
+			options.put("keyTab", this.keyTabLocationAsString);
+			options.put("storeKey", "true");
+		}
+
+		options.put("doNotPrompt", "true");
+
+		if (this.useTicketCache) {
+			options.put("useTicketCache", "true");
+			options.put("renewTGT", "true");
+		}
+
+		options.put("isInitiator", this.isInitiator.toString());
+		options.put("debug", this.debug.toString());
+
+		return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
+				AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
+	}
+
+}

+ 156 - 0
kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/ldap/KerberosLdapContextSource.java

@@ -0,0 +1,156 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.client.ldap;
+
+import java.security.PrivilegedAction;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.naming.AuthenticationException;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.security.auth.Subject;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
+import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
+import org.springframework.util.Assert;
+
+/**
+ * Implementation of an {@link LdapContextSource} that authenticates with the ldap server
+ * using Kerberos.
+ *
+ * Example usage:
+ *
+ * <pre>
+ *  &lt;bean id=&quot;authorizationContextSource&quot; class=&quot;org.springframework.security.kerberos.ldap.KerberosLdapContextSource&quot;&gt;
+ *      &lt;constructor-arg value=&quot;${authentication.ldap.ldapUrl}&quot; /&gt;
+ *      &lt;property name=&quot;referral&quot; value=&quot;ignore&quot; /&gt;
+ *
+ *       &lt;property name=&quot;loginConfig&quot;&gt;
+ *           &lt;bean class=&quot;org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig&quot;&gt;
+ *               &lt;property name=&quot;servicePrincipal&quot; value=&quot;${authentication.ldap.servicePrincipal}&quot; /&gt;
+ *               &lt;property name=&quot;useTicketCache&quot; value=&quot;true&quot; /&gt;
+ *               &lt;property name=&quot;isInitiator&quot; value=&quot;true&quot; /&gt;
+ *               &lt;property name=&quot;debug&quot; value=&quot;false&quot; /&gt;
+ *           &lt;/bean&gt;
+ *       &lt;/property&gt;
+ *   &lt;/bean&gt;
+ *
+ *   &lt;sec:ldap-user-service id=&quot;ldapUserService&quot; server-ref=&quot;authorizationContextSource&quot; user-search-filter=&quot;(| (userPrincipalName={0}) (sAMAccountName={0}))&quot;
+ *       group-search-filter=&quot;(member={0})&quot; group-role-attribute=&quot;cn&quot; role-prefix=&quot;none&quot; /&gt;
+ * </pre>
+ *
+ * @author Nelson Rodrigues
+ * @see SunJaasKrb5LoginConfig
+ */
+public class KerberosLdapContextSource extends DefaultSpringSecurityContextSource implements InitializingBean {
+
+	private Configuration loginConfig;
+
+	/**
+	 * Instantiates a new kerberos ldap context source.
+	 * @param url the url
+	 */
+	public KerberosLdapContextSource(String url) {
+		super(url);
+	}
+
+	/**
+	 * Instantiates a new kerberos ldap context source.
+	 * @param urls the urls
+	 * @param baseDn the base dn
+	 */
+	public KerberosLdapContextSource(List<String> urls, String baseDn) {
+		super(urls, baseDn);
+	}
+
+	@Override
+	public void afterPropertiesSet() /* throws Exception */ {
+		// org.springframework.ldap.core.support.AbstractContextSource in 4.x
+		// doesn't throw Exception for its InitializingBean method, so
+		// we had to remove it from here also. Addition to that
+		// we need to catch super call and re-throw.
+		try {
+			super.afterPropertiesSet();
+		}
+		catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+		Assert.notNull(this.loginConfig, "loginConfig must be specified");
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	protected DirContext getDirContextInstance(final @SuppressWarnings("rawtypes") Hashtable environment)
+			throws NamingException {
+		environment.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
+
+		Subject serviceSubject = login();
+
+		final NamingException[] suppressedException = new NamingException[] { null };
+		DirContext dirContext = Subject.doAs(serviceSubject, new PrivilegedAction<>() {
+
+			@Override
+			public DirContext run() {
+				try {
+					return KerberosLdapContextSource.super.getDirContextInstance(environment);
+				}
+				catch (NamingException ex) {
+					suppressedException[0] = ex;
+					return null;
+				}
+			}
+		});
+
+		if (suppressedException[0] != null) {
+			throw suppressedException[0];
+		}
+
+		return dirContext;
+	}
+
+	/**
+	 * The login configuration to get the serviceSubject from LoginContext
+	 * @param loginConfig the login config
+	 */
+	public void setLoginConfig(Configuration loginConfig) {
+		this.loginConfig = loginConfig;
+	}
+
+	private Subject login() throws AuthenticationException {
+		try {
+			LoginContext lc = new LoginContext(KerberosLdapContextSource.class.getSimpleName(), null, null,
+					this.loginConfig);
+
+			lc.login();
+
+			return lc.getSubject();
+		}
+		catch (LoginException ex) {
+			AuthenticationException ae = new AuthenticationException(ex.getMessage());
+			ae.initCause(ex);
+			throw ae;
+		}
+	}
+
+}

+ 135 - 0
kerberos/kerberos-client/src/test/java/org/springframework/security/kerberos/client/KerberosRestTemplateTests.java

@@ -0,0 +1,135 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.client;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import okio.Buffer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.kerberos.test.KerberosSecurityTestcase;
+import org.springframework.security.kerberos.test.MiniKdc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class KerberosRestTemplateTests extends KerberosSecurityTestcase {
+
+	private final MockWebServer server = new MockWebServer();
+
+	private static final String helloWorld = "Hello World";
+
+	private static final MediaType textContentType = new MediaType("text", "plain",
+			Collections.singletonMap("charset", "UTF-8"));
+
+	private int port;
+
+	private String baseUrl;
+
+	private KerberosRestTemplate restTemplate;
+
+	private String clientPrincipal;
+
+	private File clientKeytab;
+
+	@BeforeEach
+	void setUp() throws Exception {
+		this.server.setDispatcher(new TestDispatcher());
+		this.server.start();
+		this.port = this.server.getPort();
+		this.baseUrl = "http://localhost:" + this.port;
+
+		MiniKdc kdc = getKdc();
+		File workDir = getWorkDir();
+
+		this.clientPrincipal = "client/localhost";
+		this.clientKeytab = new File(workDir, "client.keytab");
+		kdc.createPrincipal(this.clientKeytab, this.clientPrincipal);
+
+		String serverPrincipal = "HTTP/localhost";
+		File serverKeytab = new File(workDir, "server.keytab");
+		kdc.createPrincipal(serverKeytab, serverPrincipal);
+	}
+
+	@AfterEach
+	void tearDown() throws Exception {
+		this.server.shutdown();
+	}
+
+	@Test
+	void sendsNegotiateHeader() {
+		setUpClient();
+		String s = this.restTemplate.getForObject(this.baseUrl + "/get", String.class);
+		assertThat(s).isEqualTo(helloWorld);
+	}
+
+	private void setUpClient() {
+		this.restTemplate = new KerberosRestTemplate(this.clientKeytab.getAbsolutePath(), this.clientPrincipal);
+	}
+
+	private MockResponse getRequest(RecordedRequest request, byte[] body, String contentType) {
+		if (request.getMethod().equals("OPTIONS")) {
+			return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, OPTIONS, HEAD, TRACE");
+		}
+		Buffer buf = new Buffer();
+		buf.write(body);
+		MockResponse response = new MockResponse().setHeader(HttpHeaders.CONTENT_LENGTH, body.length)
+			.setBody(buf)
+			.setResponseCode(200);
+		if (contentType != null) {
+			response = response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
+		}
+		return response;
+	}
+
+	protected class TestDispatcher extends Dispatcher {
+
+		@Override
+		public MockResponse dispatch(RecordedRequest request) {
+			try {
+				byte[] helloWorldBytes = helloWorld.getBytes(StandardCharsets.UTF_8);
+
+				if (request.getPath().equals("/get")) {
+					String header = request.getHeader(HttpHeaders.AUTHORIZATION);
+					if (header == null) {
+						return new MockResponse().setResponseCode(401)
+							.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Negotiate");
+					}
+					else if (header.startsWith("Negotiate ")) {
+						return getRequest(request, helloWorldBytes, textContentType.toString());
+					}
+				}
+				return new MockResponse().setResponseCode(404);
+			}
+			catch (Throwable ex) {
+				return new MockResponse().setResponseCode(500).setBody(ex.toString());
+			}
+
+		}
+
+	}
+
+}

+ 10 - 0
kerberos/kerberos-client/src/test/resources/log4j.properties

@@ -0,0 +1,10 @@
+log4j.rootCategory=INFO, stdout
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} - %m%n
+
+log4j.category.org.springframework.boot=INFO
+xlog4j.category.org.apache.http.wire=TRACE
+xlog4j.category.org.apache.http.headers=TRACE
+

+ 26 - 0
kerberos/kerberos-client/src/test/resources/minikdc-krb5.conf

@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+[libdefaults]
+    default_realm = {0}
+    udp_preference_limit = 1
+    forwardable = true
+
+[realms]
+    {0} = '{'
+        kdc = {1}:{2}
+    '}'

+ 86 - 0
kerberos/kerberos-client/src/test/resources/minikdc.ldiff

@@ -0,0 +1,86 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+dn: ou=users,dc=${0},dc=${1}
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+dn: uid=krbtgt,ou=users,dc=${0},dc=${1}
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: KDC Service
+sn: Service
+uid: krbtgt
+userPassword: secret
+krb5PrincipalName: krbtgt/${2}.${3}@${2}.${3}
+krb5KeyVersionNumber: 0
+
+dn: uid=ldap,ou=users,dc=${0},dc=${1}
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: LDAP
+sn: Service
+uid: ldap
+userPassword: secret
+krb5PrincipalName: ldap/${4}@${2}.${3}
+krb5KeyVersionNumber: 0
+
+dn: uid=user1,ou=users,dc=${0},dc=${1}
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: user1
+sn: Service
+uid: user1
+userPassword: secret
+krb5PrincipalName: user1@${2}.${3}
+krb5KeyVersionNumber: 0
+
+dn: uid=webtier,ou=users,dc=${0},dc=${1}
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: webtier
+sn: Service
+uid: webtier
+userPassword: secret
+krb5PrincipalName: HTTP/webtier@${2}.${3}
+krb5KeyVersionNumber: 0
+
+dn: uid=servicetier,ou=users,dc=${0},dc=${1}
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: servicetier
+sn: Service
+uid: servicetier
+userPassword: secret
+krb5PrincipalName: HTTP/servicetier@${2}.${3}
+krb5KeyVersionNumber: 0

+ 15 - 0
kerberos/kerberos-core/spring-security-kerberos-core.gradle

@@ -0,0 +1,15 @@
+plugins {
+	id 'io.spring.convention.spring-module'
+}
+
+description = 'Spring Security Kerberos Core'
+
+dependencies {
+	management platform(project(":spring-security-dependencies"))
+	api(project(':spring-security-core'))
+	testImplementation 'org.junit.jupiter:junit-jupiter'
+	testImplementation 'org.mockito:mockito-junit-jupiter'
+	testImplementation libs.org.assertj.assertj.core
+
+	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}

+ 72 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/JaasSubjectHolder.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
+
+/**
+ * <p>
+ * Holds the Subject of the currently authenticated user, since this Jaas object also has
+ * the credentials, and permits creating new credentials against other Kerberos services.
+ * </p>
+ *
+ * @author Bogdan Mustiata
+ * @see SunJaasKerberosClient
+ * @see org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider
+ */
+public class JaasSubjectHolder implements Serializable {
+
+	private static final long serialVersionUID = 8174713761131577405L;
+
+	private Subject jaasSubject;
+
+	private String username;
+
+	private Map<String, byte[]> savedTokens = new HashMap<String, byte[]>();
+
+	public JaasSubjectHolder(Subject jaasSubject) {
+		this.jaasSubject = jaasSubject;
+	}
+
+	public JaasSubjectHolder(Subject jaasSubject, String username) {
+		this.jaasSubject = jaasSubject;
+		this.username = username;
+	}
+
+	public String getUsername() {
+		return this.username;
+	}
+
+	public Subject getJaasSubject() {
+		return this.jaasSubject;
+	}
+
+	public void addToken(String targetService, byte[] outToken) {
+		this.savedTokens.put(targetService, outToken);
+	}
+
+	public byte[] getToken(String principalName) {
+		return this.savedTokens.get(principalName);
+	}
+
+}

+ 23 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthentication.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+public interface KerberosAuthentication {
+
+	JaasSubjectHolder getJaasSubjectHolder();
+
+}

+ 72 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProvider.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+/**
+ * {@link AuthenticationProvider} for kerberos.
+ *
+ * @author Mike Wiesner
+ * @author Bogdan Mustiata
+ * @since 1.0
+ */
+public class KerberosAuthenticationProvider implements AuthenticationProvider {
+
+	private KerberosClient kerberosClient;
+
+	private UserDetailsService userDetailsService;
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
+		JaasSubjectHolder subjectHolder = this.kerberosClient.login(auth.getName(), auth.getCredentials().toString());
+		UserDetails userDetails = this.userDetailsService.loadUserByUsername(subjectHolder.getUsername());
+		KerberosUsernamePasswordAuthenticationToken output = new KerberosUsernamePasswordAuthenticationToken(
+				userDetails, auth.getCredentials(), userDetails.getAuthorities(), subjectHolder);
+		output.setDetails(authentication.getDetails());
+		return output;
+
+	}
+
+	@Override
+	public boolean supports(Class<? extends Object> authentication) {
+		return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
+	}
+
+	/**
+	 * Sets the kerberos client.
+	 * @param kerberosClient the new kerberos client
+	 */
+	public void setKerberosClient(KerberosClient kerberosClient) {
+		this.kerberosClient = kerberosClient;
+	}
+
+	/**
+	 * Sets the user details service.
+	 * @param detailsService the new user details service
+	 */
+	public void setUserDetailsService(UserDetailsService detailsService) {
+		this.userDetailsService = detailsService;
+	}
+
+}

+ 29 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosClient.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+/**
+ * @author Mike Wiesner
+ * @author Bogdan Mustiata
+ * @since 1.0
+ * @version $Id$
+ */
+public interface KerberosClient {
+
+	JaasSubjectHolder login(String username, String password);
+
+}

+ 132 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosMultiTier.java

@@ -0,0 +1,132 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.Authentication;
+
+/**
+ * <p>
+ * Allows creating tickets against other service principals storing the tickets in the
+ * KerberosAuthentication's JaasSubjectHolder.
+ * </p>
+ *
+ * @author Bogdan Mustiata
+ */
+public final class KerberosMultiTier {
+
+	public static final String KERBEROS_OID_STRING = "1.2.840.113554.1.2.2";
+
+	public static final Oid KERBEROS_OID = createOid(KERBEROS_OID_STRING);
+
+	/**
+	 * Create a new ticket for the
+	 * @param authentication
+	 * @param username
+	 * @param lifetimeInSeconds
+	 * @param targetService
+	 * @return
+	 */
+	public static Authentication authenticateService(Authentication authentication, final String username,
+			final int lifetimeInSeconds, final String targetService) {
+
+		KerberosAuthentication kerberosAuthentication = (KerberosAuthentication) authentication;
+		final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();
+		Subject subject = jaasSubjectHolder.getJaasSubject();
+
+		Subject.doAs(subject, new PrivilegedAction<Object>() {
+			@Override
+			public Object run() {
+				runAuthentication(jaasSubjectHolder, username, lifetimeInSeconds, targetService);
+
+				return null;
+			}
+		});
+
+		return authentication;
+	}
+
+	public static byte[] getTokenForService(Authentication authentication, String principalName) {
+		KerberosAuthentication kerberosAuthentication = (KerberosAuthentication) authentication;
+		final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();
+
+		return jaasSubjectHolder.getToken(principalName);
+	}
+
+	private static void runAuthentication(JaasSubjectHolder jaasContext, String username, int lifetimeInSeconds,
+			String targetService) {
+		try {
+			GSSManager manager = GSSManager.getInstance();
+			GSSName clientName = manager.createName(username, GSSName.NT_USER_NAME);
+
+			GSSCredential clientCredential = manager.createCredential(clientName, lifetimeInSeconds, KERBEROS_OID,
+					GSSCredential.INITIATE_ONLY);
+
+			GSSName serverName = manager.createName(targetService, GSSName.NT_USER_NAME);
+
+			GSSContext securityContext = manager.createContext(serverName, KERBEROS_OID, clientCredential,
+					GSSContext.DEFAULT_LIFETIME);
+
+			securityContext.requestCredDeleg(true);
+			securityContext.requestInteg(false);
+			securityContext.requestAnonymity(false);
+			securityContext.requestMutualAuth(false);
+			securityContext.requestReplayDet(false);
+			securityContext.requestSequenceDet(false);
+
+			boolean established = false;
+
+			byte[] outToken = new byte[0];
+
+			while (!established) {
+				byte[] inToken = new byte[0];
+				outToken = securityContext.initSecContext(inToken, 0, inToken.length);
+
+				established = securityContext.isEstablished();
+			}
+
+			jaasContext.addToken(targetService, outToken);
+		}
+		catch (Exception ex) {
+			throw new BadCredentialsException("Kerberos authentication failed", ex);
+		}
+	}
+
+	private static Oid createOid(String oid) {
+		try {
+			return new Oid(oid);
+		}
+		catch (GSSException ex) {
+			throw new IllegalStateException("Unable to instantiate Oid: ", ex);
+		}
+	}
+
+	private KerberosMultiTier() {
+	}
+
+}

+ 122 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProvider.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsChecker;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.util.Assert;
+
+/**
+ * <p>
+ * Authentication Provider which validates Kerberos Service Tickets or SPNEGO Tokens
+ * (which includes Kerberos Service Tickets).
+ * </p>
+ *
+ * <p>
+ * It needs a <code>KerberosTicketValidator</code>, which contains the code to validate
+ * the ticket, as this code is different between SUN and IBM JRE.<br>
+ * It also needs an <code>UserDetailsService</code> to load the user properties and the
+ * <code>GrantedAuthorities</code>, as we only get back the username from Kerbeos
+ * </p>
+ *
+ * You can see an example configuration in
+ * <code>SpnegoAuthenticationProcessingFilter</code>.
+ *
+ * @author Mike Wiesner
+ * @author Jeremy Stone
+ * @since 1.0
+ * @see KerberosTicketValidator
+ * @see UserDetailsService
+ */
+public class KerberosServiceAuthenticationProvider implements AuthenticationProvider, InitializingBean {
+
+	private static final Log LOG = LogFactory.getLog(KerberosServiceAuthenticationProvider.class);
+
+	private KerberosTicketValidator ticketValidator;
+
+	private UserDetailsService userDetailsService;
+
+	private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		KerberosServiceRequestToken auth = (KerberosServiceRequestToken) authentication;
+		byte[] token = auth.getToken();
+		LOG.debug("Try to validate Kerberos Token");
+		KerberosTicketValidation ticketValidation = this.ticketValidator.validateTicket(token);
+		LOG.debug("Successfully validated " + ticketValidation.username());
+		UserDetails userDetails = this.userDetailsService.loadUserByUsername(ticketValidation.username());
+		this.userDetailsChecker.check(userDetails);
+		additionalAuthenticationChecks(userDetails, auth);
+		KerberosServiceRequestToken responseAuth = new KerberosServiceRequestToken(userDetails, ticketValidation,
+				userDetails.getAuthorities(), token);
+		responseAuth.setDetails(authentication.getDetails());
+		return responseAuth;
+	}
+
+	@Override
+	public boolean supports(Class<? extends Object> auth) {
+		return KerberosServiceRequestToken.class.isAssignableFrom(auth);
+	}
+
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		Assert.notNull(this.ticketValidator, "ticketValidator must be specified");
+		Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
+	}
+
+	/**
+	 * The <code>UserDetailsService</code> to use, for loading the user properties and the
+	 * <code>GrantedAuthorities</code>.
+	 * @param userDetailsService the new user details service
+	 */
+	public void setUserDetailsService(UserDetailsService userDetailsService) {
+		this.userDetailsService = userDetailsService;
+	}
+
+	/**
+	 * The <code>KerberosTicketValidator</code> to use, for validating the Kerberos/SPNEGO
+	 * tickets.
+	 * @param ticketValidator the new ticket validator
+	 */
+	public void setTicketValidator(KerberosTicketValidator ticketValidator) {
+		this.ticketValidator = ticketValidator;
+	}
+
+	/**
+	 * Allows subclasses to perform any additional checks of a returned
+	 * <code>UserDetails</code> for a given authentication request.
+	 * @param userDetails as retrieved from the {@link UserDetailsService}
+	 * @param authentication validated {@link KerberosServiceRequestToken}
+	 * @throws AuthenticationException AuthenticationException if the credentials could
+	 * not be validated (generally a <code>BadCredentialsException</code>, an
+	 * <code>AuthenticationServiceException</code>)
+	 */
+	protected void additionalAuthenticationChecks(UserDetails userDetails, KerberosServiceRequestToken authentication)
+			throws AuthenticationException {
+	}
+
+}

+ 233 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceRequestToken.java

@@ -0,0 +1,233 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collection;
+
+import javax.security.auth.Subject;
+
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.MessageProp;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * <p>
+ * Holds the Kerberos/SPNEGO token for requesting a kerberized service and is also the
+ * output of <code>KerberosServiceAuthenticationProvider</code>.
+ * </p>
+ * <p>
+ * Will mostly be created in <code>SpnegoAuthenticationProcessingFilter</code> and
+ * authenticated in <code>KerberosServiceAuthenticationProvider</code>.
+ * </p>
+ *
+ * This token cannot be re-authenticated, as you will get a Kerberos Reply error.
+ *
+ * @author Mike Wiesner
+ * @author Jeremy Stone
+ * @author Bogdan Mustiata
+ * @since 1.0
+ * @see KerberosServiceAuthenticationProvider
+ */
+public class KerberosServiceRequestToken extends AbstractAuthenticationToken implements KerberosAuthentication {
+
+	private static final long serialVersionUID = 395488921064775014L;
+
+	private final byte[] token;
+
+	private final Object principal;
+
+	private final transient KerberosTicketValidation ticketValidation;
+
+	private JaasSubjectHolder jaasSubjectHolder;
+
+	/**
+	 * Creates an authenticated token, normally used as an output of an authentication
+	 * provider.
+	 * @param principal the user principal (mostly of instance <code>UserDetails</code>)
+	 * @param ticketValidation result of ticket validation
+	 * @param authorities the authorities which are granted to the user
+	 * @param token the Kerberos/SPNEGO token
+	 * @see UserDetails
+	 */
+	public KerberosServiceRequestToken(Object principal, KerberosTicketValidation ticketValidation,
+			Collection<? extends GrantedAuthority> authorities, byte[] token) {
+		super(authorities);
+		this.token = token;
+		this.principal = principal;
+		this.ticketValidation = ticketValidation;
+		this.jaasSubjectHolder = new JaasSubjectHolder(ticketValidation.subject(), ticketValidation.username());
+		super.setAuthenticated(true);
+	}
+
+	/**
+	 * Creates an unauthenticated instance which should then be authenticated by
+	 * <code>KerberosServiceAuthenticationProvider</code>.
+	 * @param token Kerberos/SPNEGO token
+	 * @see KerberosServiceAuthenticationProvider
+	 */
+	public KerberosServiceRequestToken(byte[] token) {
+		super(AuthorityUtils.NO_AUTHORITIES);
+		this.token = token;
+		this.ticketValidation = null;
+		this.principal = null;
+	}
+
+	/**
+	 * equals() is based only on the Kerberos token
+	 */
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (!super.equals(obj)) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		KerberosServiceRequestToken other = (KerberosServiceRequestToken) obj;
+		if (!Arrays.equals(this.token, other.token)) {
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * Calculates hashcode based on the Kerberos token
+	 */
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Arrays.hashCode(this.token);
+		return result;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return null;
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.principal;
+	}
+
+	/**
+	 * Returns the Kerberos token
+	 * @return the token data
+	 */
+	public byte[] getToken() {
+		return this.token;
+	}
+
+	/**
+	 * Gets the ticket validation
+	 * @return the ticket validation (which will be null if the token is unauthenticated)
+	 */
+	public KerberosTicketValidation getTicketValidation() {
+		return this.ticketValidation;
+	}
+
+	/**
+	 * Determines whether an authenticated token has a response token
+	 * @return whether a response token is available
+	 */
+	public boolean hasResponseToken() {
+		return this.ticketValidation != null && this.ticketValidation.responseToken() != null;
+	}
+
+	/**
+	 * Gets the (Base64) encoded response token assuming one is available.
+	 * @return encoded response token
+	 */
+	public String getEncodedResponseToken() {
+		if (!hasResponseToken()) {
+			throw new IllegalStateException("Unauthenticated or no response token");
+		}
+		return Base64.getEncoder().encodeToString(this.ticketValidation.responseToken());
+	}
+
+	/**
+	 * Unwraps an encrypted message using the gss context
+	 * @param data the data
+	 * @param offset data offset
+	 * @param length data length
+	 * @return the decrypted message
+	 * @throws PrivilegedActionException if jaas throws and error
+	 */
+	public byte[] decrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException {
+		return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction<byte[]>() {
+			public byte[] run() throws Exception {
+				final GSSContext context = getTicketValidation().getGssContext();
+				return context.unwrap(data, offset, length, new MessageProp(true));
+			}
+		});
+	}
+
+	/**
+	 * Unwraps an encrypted message using the gss context
+	 * @param data the data
+	 * @return the decrypted message
+	 * @throws PrivilegedActionException if jaas throws and error
+	 */
+	public byte[] decrypt(final byte[] data) throws PrivilegedActionException {
+		return decrypt(data, 0, data.length);
+	}
+
+	/**
+	 * Wraps an message using the gss context
+	 * @param data the data
+	 * @param offset data offset
+	 * @param length data length
+	 * @return the encrypted message
+	 * @throws PrivilegedActionException if jaas throws and error
+	 */
+	public byte[] encrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException {
+		return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction<byte[]>() {
+			public byte[] run() throws Exception {
+				final GSSContext context = getTicketValidation().getGssContext();
+				return context.wrap(data, offset, length, new MessageProp(true));
+			}
+		});
+	}
+
+	/**
+	 * Wraps an message using the gss context
+	 * @param data the data
+	 * @return the encrypted message
+	 * @throws PrivilegedActionException if jaas throws and error
+	 */
+	public byte[] encrypt(final byte[] data) throws PrivilegedActionException {
+		return encrypt(data, 0, data.length);
+	}
+
+	@Override
+	public JaasSubjectHolder getJaasSubjectHolder() {
+		return this.jaasSubjectHolder;
+	}
+
+}

+ 92 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidation.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import java.util.HashSet;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+
+/**
+ * Result of ticket validation
+ */
+public final class KerberosTicketValidation {
+
+	private final String username;
+
+	private final Subject subject;
+
+	private final byte[] responseToken;
+
+	private final GSSContext gssContext;
+
+	private final GSSCredential delegationCredential;
+
+	public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
+			GSSContext gssContext) {
+		this(username, servicePrincipal, responseToken, gssContext, null);
+	}
+
+	public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
+			GSSContext gssContext, GSSCredential delegationCredential) {
+		final HashSet<KerberosPrincipal> princs = new HashSet<KerberosPrincipal>();
+		princs.add(new KerberosPrincipal(servicePrincipal));
+
+		this.username = username;
+		this.subject = new Subject(false, princs, new HashSet<Object>(), new HashSet<Object>());
+		this.responseToken = responseToken;
+		this.gssContext = gssContext;
+		this.delegationCredential = delegationCredential;
+	}
+
+	public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext) {
+		this(username, subject, responseToken, gssContext, null);
+	}
+
+	public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext,
+			GSSCredential delegationCredential) {
+		this.username = username;
+		this.subject = subject;
+		this.responseToken = responseToken;
+		this.gssContext = gssContext;
+		this.delegationCredential = delegationCredential;
+	}
+
+	public String username() {
+		return this.username;
+	}
+
+	public byte[] responseToken() {
+		return this.responseToken;
+	}
+
+	public GSSContext getGssContext() {
+		return this.gssContext;
+	}
+
+	public Subject subject() {
+		return this.subject;
+	}
+
+	public GSSCredential getDelegationCredential() {
+		return this.delegationCredential;
+	}
+
+}

+ 40 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidator.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import org.springframework.security.authentication.BadCredentialsException;
+
+/**
+ * Implementations of this interface are used in
+ * {@link KerberosServiceAuthenticationProvider} to validate a Kerberos/SPNEGO Ticket.
+ *
+ * @author Mike Wiesner
+ * @author Jeremy Stone
+ * @since 1.0
+ * @see KerberosServiceAuthenticationProvider
+ */
+public interface KerberosTicketValidator {
+
+	/**
+	 * Validates a Kerberos/SPNEGO ticket.
+	 * @param token Kerbeos/SPNEGO ticket
+	 * @return authenticated kerberos principal
+	 * @throws BadCredentialsException if the ticket is not valid
+	 */
+	KerberosTicketValidation validateTicket(byte[] token) throws BadCredentialsException;
+
+}

+ 69 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosUsernamePasswordAuthenticationToken.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import java.util.Collection;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+/**
+ * <p>
+ * Holds the Username/Password as well as the JAAS Subject allowing multi-tier
+ * authentications using Kerberos.
+ * </p>
+ *
+ * <p>
+ * The JAAS Subject has in its private credentials the Kerberos tickets for generating new
+ * tickets against other service principals using
+ * <code>KerberosMultiTier.authenticateService()</code>
+ * </p>
+ *
+ * @author Bogdan Mustiata
+ * @see KerberosAuthenticationProvider
+ * @see KerberosMultiTier
+ */
+public class KerberosUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken
+		implements KerberosAuthentication {
+
+	private static final long serialVersionUID = 6327699460703504153L;
+
+	private final JaasSubjectHolder jaasSubjectHolder;
+
+	/**
+	 * <p>
+	 * Creates an authentication token that holds the username and password, and the
+	 * Subject that the user will need to create new authentication tokens against other
+	 * services.
+	 * </p>
+	 * @param principal
+	 * @param credentials
+	 * @param authorities
+	 * @param subjectHolder
+	 */
+	public KerberosUsernamePasswordAuthenticationToken(Object principal, Object credentials,
+			Collection<? extends GrantedAuthority> authorities, JaasSubjectHolder subjectHolder) {
+		super(principal, credentials, authorities);
+		this.jaasSubjectHolder = subjectHolder;
+	}
+
+	@Override
+	public JaasSubjectHolder getJaasSubjectHolder() {
+		return this.jaasSubjectHolder;
+	}
+
+}

+ 78 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/GlobalSunJaasKerberosConfig.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication.sun;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * Config for global jaas.
+ *
+ * @author Mike Wiesner
+ * @since 1.0
+ */
+public class GlobalSunJaasKerberosConfig implements BeanPostProcessor, InitializingBean {
+
+	private boolean debug = false;
+
+	private String krbConfLocation;
+
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		if (this.debug) {
+			System.setProperty("sun.security.krb5.debug", "true");
+		}
+		if (this.krbConfLocation != null) {
+			System.setProperty("java.security.krb5.conf", this.krbConfLocation);
+		}
+
+	}
+
+	/**
+	 * Enable debug logs from the Sun Kerberos Implementation. Default is false.
+	 * @param debug true if debug should be enabled
+	 */
+	public void setDebug(boolean debug) {
+		this.debug = debug;
+	}
+
+	/**
+	 * Kerberos config file location can be specified here.
+	 * @param krbConfLocation the path to krb config file
+	 */
+	public void setKrbConfLocation(String krbConfLocation) {
+		this.krbConfLocation = krbConfLocation;
+	}
+
+	// The following methods are not used here. This Bean implements only
+	// BeanPostProcessor to ensure that it
+	// is created before any other bean is created, because the system properties needed
+	// to be set very early
+	// in the startup-phase, but after the BeanFactoryPostProcessing.
+
+	@Override
+	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+		return bean;
+	}
+
+	@Override
+	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+		return bean;
+	}
+
+}

+ 47 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/JaasUtil.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication.sun;
+
+import java.security.Principal;
+import java.util.HashSet;
+
+import javax.security.auth.Subject;
+
+/**
+ * JAAS utility functions.
+ *
+ * @author Bogdan Mustiata
+ */
+public final class JaasUtil {
+
+	/**
+	 * Copy the principal and the credentials into a new Subject.
+	 * @param subject
+	 * @return
+	 */
+	public static Subject copySubject(Subject subject) {
+		Subject subjectCopy = new Subject(false, new HashSet<Principal>(subject.getPrincipals()),
+				new HashSet<Object>(subject.getPublicCredentials()),
+				new HashSet<Object>(subject.getPrivateCredentials()));
+
+		return subjectCopy;
+	}
+
+	private JaasUtil() {
+	}
+
+}

+ 153 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosClient.java

@@ -0,0 +1,153 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication.sun;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.kerberos.authentication.JaasSubjectHolder;
+import org.springframework.security.kerberos.authentication.KerberosClient;
+
+/**
+ * Implementation of {@link KerberosClient} which uses the SUN JAAS login module, which is
+ * included in the SUN JRE, it will not work with an IBM JRE. The whole configuration is
+ * done in this class, no additional JAAS configuration is needed.
+ *
+ * @author Mike Wiesner
+ * @author Bogdan Mustiata
+ * @since 1.0
+ */
+public class SunJaasKerberosClient implements KerberosClient {
+
+	private boolean debug = false;
+
+	private boolean multiTier = false;
+
+	private static final Log LOG = LogFactory.getLog(SunJaasKerberosClient.class);
+
+	@Override
+	public JaasSubjectHolder login(String username, String password) {
+		LOG.debug("Trying to authenticate " + username + " with Kerberos");
+		JaasSubjectHolder result;
+
+		try {
+			LoginContext loginContext = new LoginContext("", null,
+					new KerberosClientCallbackHandler(username, password), new LoginConfig(this.debug));
+			loginContext.login();
+
+			Subject jaasSubject = loginContext.getSubject();
+
+			if (LOG.isDebugEnabled()) {
+				LOG.debug("Kerberos authenticated user: " + jaasSubject);
+			}
+
+			String validatedUsername = jaasSubject.getPrincipals().iterator().next().toString();
+			Subject subjectCopy = JaasUtil.copySubject(jaasSubject);
+			result = new JaasSubjectHolder(subjectCopy, validatedUsername);
+
+			if (!this.multiTier) {
+				loginContext.logout();
+			}
+		}
+		catch (LoginException ex) {
+			throw new BadCredentialsException("Kerberos authentication failed", ex);
+		}
+
+		return result;
+	}
+
+	public void setDebug(boolean debug) {
+		this.debug = debug;
+	}
+
+	public void setMultiTier(boolean multiTier) {
+		this.multiTier = multiTier;
+	}
+
+	private static final class LoginConfig extends Configuration {
+
+		private boolean debug;
+
+		private LoginConfig(boolean debug) {
+			super();
+			this.debug = debug;
+		}
+
+		@Override
+		public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+			HashMap<String, String> options = new HashMap<String, String>();
+			options.put("storeKey", "true");
+			if (this.debug) {
+				options.put("debug", "true");
+			}
+
+			return new AppConfigurationEntry[] {
+					new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
+							AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
+		}
+
+	}
+
+	static final class KerberosClientCallbackHandler implements CallbackHandler {
+
+		private String username;
+
+		private String password;
+
+		private KerberosClientCallbackHandler(String username, String password) {
+			this.username = username;
+			this.password = password;
+		}
+
+		@Override
+		public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+			for (Callback callback : callbacks) {
+				if (callback instanceof NameCallback) {
+					NameCallback ncb = (NameCallback) callback;
+					ncb.setName(this.username);
+				}
+				else if (callback instanceof PasswordCallback) {
+					PasswordCallback pwcb = (PasswordCallback) callback;
+					pwcb.setPassword(this.password.toCharArray());
+				}
+				else {
+					throw new UnsupportedCallbackException(callback,
+							"We got a " + callback.getClass().getCanonicalName()
+									+ ", but only NameCallback and PasswordCallback is supported");
+				}
+			}
+
+		}
+
+	}
+
+}

+ 332 - 0
kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidator.java

@@ -0,0 +1,332 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication.sun;
+
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+
+import com.sun.security.jgss.GSSUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.kerberos.authentication.JaasSubjectHolder;
+import org.springframework.security.kerberos.authentication.KerberosTicketValidation;
+import org.springframework.security.kerberos.authentication.KerberosTicketValidator;
+import org.springframework.util.Assert;
+
+/**
+ * Implementation of {@link KerberosTicketValidator} which uses the SUN JAAS login module,
+ * which is included in the SUN JRE, it will not work with an IBM JRE. The whole
+ * configuration is done in this class, no additional JAAS configuration is needed.
+ *
+ * @author Mike Wiesner
+ * @author Jeremy Stone
+ * @author Bogdan Mustiata
+ * @since 1.0
+ */
+public class SunJaasKerberosTicketValidator implements KerberosTicketValidator, InitializingBean {
+
+	private String servicePrincipal;
+
+	private String realmName;
+
+	private Resource keyTabLocation;
+
+	private Subject serviceSubject;
+
+	private boolean holdOnToGSSContext;
+
+	private boolean debug = false;
+
+	private boolean multiTier = false;
+
+	private boolean refreshKrb5Config = false;
+
+	private static final Log LOG = LogFactory.getLog(SunJaasKerberosTicketValidator.class);
+
+	@Override
+	public KerberosTicketValidation validateTicket(byte[] token) {
+		try {
+			if (!this.multiTier) {
+				return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
+			}
+
+			Subject subjectCopy = JaasUtil.copySubject(this.serviceSubject);
+			JaasSubjectHolder subjectHolder = new JaasSubjectHolder(subjectCopy);
+
+			return Subject.doAs(subjectHolder.getJaasSubject(), new KerberosMultitierValidateAction(token));
+
+		}
+		catch (PrivilegedActionException ex) {
+			throw new BadCredentialsException("Kerberos validation not successful", ex);
+		}
+	}
+
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		Assert.notNull(this.servicePrincipal, "servicePrincipal must be specified");
+		Assert.notNull(this.keyTabLocation, "keyTab must be specified");
+		if (this.keyTabLocation instanceof ClassPathResource) {
+			this.LOG.warn(
+					"Your keytab is in the classpath. This file needs special protection and shouldn't be in the classpath. JAAS may also not be able to load this file from classpath.");
+		}
+		String keyTabLocationAsString = this.keyTabLocation.getURL().toExternalForm();
+		// We need to remove the file prefix (if there is one), as it is not supported in
+		// Java 7 anymore.
+		// As Java 6 accepts it with and without the prefix, we don't need to check for
+		// Java 7
+		if (keyTabLocationAsString.startsWith("file:")) {
+			keyTabLocationAsString = keyTabLocationAsString.substring(5);
+		}
+		LoginConfig loginConfig = new LoginConfig(keyTabLocationAsString, this.servicePrincipal, this.realmName,
+				this.multiTier, this.debug, this.refreshKrb5Config);
+		Set<Principal> princ = new HashSet<Principal>(1);
+		princ.add(new KerberosPrincipal(this.servicePrincipal));
+		Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>());
+		LoginContext lc = new LoginContext("", sub, null, loginConfig);
+		lc.login();
+		this.serviceSubject = lc.getSubject();
+	}
+
+	/**
+	 * The service principal of the application. For web apps this is
+	 * <code>HTTP/full-qualified-domain-name@DOMAIN</code>. The keytab must contain the
+	 * key for this principal.
+	 * @param servicePrincipal service principal to use
+	 * @see #setKeyTabLocation(Resource)
+	 */
+	public void setServicePrincipal(String servicePrincipal) {
+		this.servicePrincipal = servicePrincipal;
+	}
+
+	/**
+	 * The realm name of the application. For web apps this is <code>DOMAIN</code>
+	 * @param realmName
+	 */
+	public void setRealmName(String realmName) {
+		this.realmName = realmName;
+	}
+
+	/**
+	 * @param multiTier
+	 */
+	public void setMultiTier(boolean multiTier) {
+		this.multiTier = multiTier;
+	}
+
+	/**
+	 * <p>
+	 * The location of the keytab. You can use the normale Spring Resource prefixes like
+	 * <code>file:</code> or <code>classpath:</code>, but as the file is later on read by
+	 * JAAS, we cannot guarantee that <code>classpath</code> works in every environment,
+	 * esp. not in Java EE application servers. You should use <code>file:</code> there.
+	 *
+	 * This file also needs special protection, which is another reason to not include it
+	 * in the classpath but rather use <code>file:/etc/http.keytab</code> for example.
+	 * @param keyTabLocation The location where the keytab resides
+	 */
+	public void setKeyTabLocation(Resource keyTabLocation) {
+		this.keyTabLocation = keyTabLocation;
+	}
+
+	/**
+	 * Enables the debug mode of the JAAS Kerberos login module.
+	 * @param debug default is false
+	 */
+	public void setDebug(boolean debug) {
+		this.debug = debug;
+	}
+
+	/**
+	 * Determines whether to hold on to the {@link GSSContext GSS security context} or
+	 * otherwise {@link GSSContext#dispose() dispose} of it immediately (the default
+	 * behaviour).
+	 * <p>
+	 * Holding on to the GSS context allows decrypt and encrypt operations for subsequent
+	 * interactions with the principal.
+	 * @param holdOnToGSSContext true if should hold on to context
+	 */
+	public void setHoldOnToGSSContext(boolean holdOnToGSSContext) {
+		this.holdOnToGSSContext = holdOnToGSSContext;
+	}
+
+	/**
+	 * Enables configuration to be refreshed before the login method is called.
+	 * @param refreshKrb5Config Set this to true, if you want the configuration to be
+	 * refreshed before the login method is called.
+	 */
+	public void setRefreshKrb5Config(boolean refreshKrb5Config) {
+		this.refreshKrb5Config = refreshKrb5Config;
+	}
+
+	/**
+	 * This class is needed, because the validation must run with previously generated
+	 * JAAS subject which belongs to the service principal and was loaded out of the
+	 * keytab during startup.
+	 */
+	private final class KerberosMultitierValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
+
+		byte[] kerberosTicket;
+
+		private KerberosMultitierValidateAction(byte[] kerberosTicket) {
+			this.kerberosTicket = kerberosTicket;
+		}
+
+		@Override
+		public KerberosTicketValidation run() throws Exception {
+			byte[] responseToken = new byte[0];
+			GSSManager manager = GSSManager.getInstance();
+
+			GSSContext context = manager.createContext((GSSCredential) null);
+
+			while (!context.isEstablished()) {
+				context.acceptSecContext(this.kerberosTicket, 0, this.kerberosTicket.length);
+			}
+
+			Subject subject = GSSUtil.createSubject(context.getSrcName(), context.getDelegCred());
+
+			KerberosTicketValidation result = new KerberosTicketValidation(context.getSrcName().toString(), subject,
+					responseToken, context);
+
+			if (!SunJaasKerberosTicketValidator.this.holdOnToGSSContext) {
+				context.dispose();
+			}
+
+			return result;
+		}
+
+	}
+
+	/**
+	 * This class is needed, because the validation must run with previously generated
+	 * JAAS subject which belongs to the service principal and was loaded out of the
+	 * keytab during startup.
+	 */
+	private final class KerberosValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
+
+		byte[] kerberosTicket;
+
+		private KerberosValidateAction(byte[] kerberosTicket) {
+			this.kerberosTicket = kerberosTicket;
+		}
+
+		@Override
+		public KerberosTicketValidation run() throws Exception {
+			byte[] responseToken = new byte[0];
+			GSSName gssName = null;
+			GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
+			while (!context.isEstablished()) {
+				responseToken = context.acceptSecContext(this.kerberosTicket, 0, this.kerberosTicket.length);
+				gssName = context.getSrcName();
+				if (gssName == null) {
+					throw new BadCredentialsException("GSSContext name of the context initiator is null");
+				}
+			}
+
+			GSSCredential delegationCredential = null;
+			if (context.getCredDelegState()) {
+				delegationCredential = context.getDelegCred();
+			}
+
+			if (!SunJaasKerberosTicketValidator.this.holdOnToGSSContext) {
+				context.dispose();
+			}
+			return new KerberosTicketValidation(gssName.toString(),
+					SunJaasKerberosTicketValidator.this.servicePrincipal, responseToken, context, delegationCredential);
+		}
+
+	}
+
+	/**
+	 * Normally you need a JAAS config file in order to use the JAAS Kerberos Login
+	 * Module, with this class it is not needed and you can have different configurations
+	 * in one JVM.
+	 */
+	private static final class LoginConfig extends Configuration {
+
+		private String keyTabLocation;
+
+		private String servicePrincipalName;
+
+		private String realmName;
+
+		private boolean multiTier;
+
+		private boolean debug;
+
+		private boolean refreshKrb5Config;
+
+		private LoginConfig(String keyTabLocation, String servicePrincipalName, String realmName, boolean multiTier,
+				boolean debug, boolean refreshKrb5Config) {
+			this.keyTabLocation = keyTabLocation;
+			this.servicePrincipalName = servicePrincipalName;
+			this.realmName = realmName;
+			this.multiTier = multiTier;
+			this.debug = debug;
+			this.refreshKrb5Config = refreshKrb5Config;
+		}
+
+		@Override
+		public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+			HashMap<String, String> options = new HashMap<String, String>();
+			options.put("useKeyTab", "true");
+			options.put("keyTab", this.keyTabLocation);
+			options.put("principal", this.servicePrincipalName);
+			options.put("storeKey", "true");
+			options.put("doNotPrompt", "true");
+			if (this.debug) {
+				options.put("debug", "true");
+			}
+
+			if (this.realmName != null) {
+				options.put("realm", this.realmName);
+			}
+
+			if (this.refreshKrb5Config) {
+				options.put("refreshKrb5Config", "true");
+			}
+
+			if (!this.multiTier) {
+				options.put("isInitiator", "false");
+			}
+
+			return new AppConfigurationEntry[] {
+					new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
+							AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
+		}
+
+	}
+
+}

+ 92 - 0
kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProviderTests.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Test class for {@link KerberosAuthenticationProvider}
+ *
+ * @author Mike Wiesner
+ * @since 1.0
+ */
+public class KerberosAuthenticationProviderTests {
+
+	private KerberosAuthenticationProvider provider;
+
+	private KerberosClient kerberosClient;
+
+	private UserDetailsService userDetailsService;
+
+	private static final String TEST_USER = "Testuser@SPRINGSOURCE.ORG";
+
+	private static final String TEST_PASSWORD = "password";
+
+	private static final UsernamePasswordAuthenticationToken INPUT_TOKEN = new UsernamePasswordAuthenticationToken(
+			TEST_USER, TEST_PASSWORD);
+
+	private static final List<GrantedAuthority> AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
+
+	private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true, true,
+			AUTHORITY_LIST);
+
+	private static final JaasSubjectHolder JAAS_SUBJECT_HOLDER = new JaasSubjectHolder(null, TEST_USER);
+
+	@BeforeEach
+	public void before() {
+		// mocking
+		this.kerberosClient = mock(KerberosClient.class);
+		this.userDetailsService = mock(UserDetailsService.class);
+		this.provider = new KerberosAuthenticationProvider();
+		this.provider.setKerberosClient(this.kerberosClient);
+		this.provider.setUserDetailsService(this.userDetailsService);
+	}
+
+	@Test
+	public void testLoginOk() throws Exception {
+		given(this.userDetailsService.loadUserByUsername(TEST_USER)).willReturn(USER_DETAILS);
+		given(this.kerberosClient.login(TEST_USER, TEST_PASSWORD)).willReturn(JAAS_SUBJECT_HOLDER);
+
+		Authentication authenticate = this.provider.authenticate(INPUT_TOKEN);
+
+		verify(this.kerberosClient).login(TEST_USER, TEST_PASSWORD);
+
+		assertThat(authenticate).isNotNull();
+		assertThat(authenticate.getName()).isEqualTo(TEST_USER);
+		assertThat(authenticate.getPrincipal()).isEqualTo(USER_DETAILS);
+		assertThat(authenticate.getCredentials()).isEqualTo(TEST_PASSWORD);
+		assertThat(authenticate.getAuthorities()).isEqualTo(AUTHORITY_LIST);
+
+	}
+
+}

+ 173 - 0
kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProviderTests.java

@@ -0,0 +1,173 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.AccountExpiredException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Test class for {@link KerberosServiceAuthenticationProvider}
+ *
+ * @author Mike Wiesner
+ * @author Jeremy Stone
+ * @since 1.0
+ */
+public class KerberosServiceAuthenticationProviderTests {
+
+	private KerberosServiceAuthenticationProvider provider;
+
+	private KerberosTicketValidator ticketValidator;
+
+	private UserDetailsService userDetailsService;
+
+	// data
+	private static final byte[] TEST_TOKEN = "TestToken".getBytes();
+
+	private static final byte[] RESPONSE_TOKEN = "ResponseToken".getBytes();
+
+	private static final String TEST_USER = "Testuser@SPRINGSOURCE.ORG";
+
+	private static final KerberosTicketValidation TICKET_VALIDATION = new KerberosTicketValidation(TEST_USER,
+			"XXX@test.com", RESPONSE_TOKEN, null);
+
+	private static final List<GrantedAuthority> AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
+
+	private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true, true,
+			AUTHORITY_LIST);
+
+	private static final KerberosServiceRequestToken INPUT_TOKEN = new KerberosServiceRequestToken(TEST_TOKEN);
+
+	@BeforeEach
+	public void before() {
+		System.setProperty("java.security.krb5.conf", "test.com");
+		System.setProperty("java.security.krb5.kdc", "kdc.test.com");
+		// mocking
+		this.ticketValidator = mock(KerberosTicketValidator.class);
+		this.userDetailsService = mock(UserDetailsService.class);
+		this.provider = new KerberosServiceAuthenticationProvider();
+		this.provider.setTicketValidator(this.ticketValidator);
+		this.provider.setUserDetailsService(this.userDetailsService);
+	}
+
+	@AfterEach
+	public void after() {
+		System.clearProperty("java.security.krb5.conf");
+		System.clearProperty("java.security.krb5.kdc");
+	}
+
+	@Test
+	public void testEverythingWorks() throws Exception {
+		Authentication output = callProviderAndReturnUser(USER_DETAILS, INPUT_TOKEN);
+		assertThat(output).isNotNull();
+		assertThat(output.getName()).isEqualTo(TEST_USER);
+		assertThat(output.getAuthorities()).isEqualTo(AUTHORITY_LIST);
+		assertThat(output.getPrincipal()).isEqualTo(USER_DETAILS);
+	}
+
+	@Test
+	public void testAuthenticationDetailsPropagation() throws Exception {
+		KerberosServiceRequestToken requestToken = new KerberosServiceRequestToken(TEST_TOKEN);
+		requestToken.setDetails("TestDetails");
+		Authentication output = callProviderAndReturnUser(USER_DETAILS, requestToken);
+		assertThat(output).isNotNull();
+		assertThat(output.getDetails()).isEqualTo(requestToken.getDetails());
+	}
+
+	@Test
+	public void testUserIsDisabled() throws Exception {
+		assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> {
+			User disabledUser = new User(TEST_USER, "empty", false, true, true, true, AUTHORITY_LIST);
+			callProviderAndReturnUser(disabledUser, INPUT_TOKEN);
+		});
+	}
+
+	@Test
+	public void testUserAccountIsExpired() throws Exception {
+		assertThatExceptionOfType(AccountExpiredException.class).isThrownBy(() -> {
+			User expiredUser = new User(TEST_USER, "empty", true, false, true, true, AUTHORITY_LIST);
+			callProviderAndReturnUser(expiredUser, INPUT_TOKEN);
+		}).isInstanceOf(AccountExpiredException.class);
+	}
+
+	@Test
+	public void testUserCredentialsExpired() throws Exception {
+		assertThatExceptionOfType(CredentialsExpiredException.class).isThrownBy(() -> {
+			User credExpiredUser = new User(TEST_USER, "empty", true, true, false, true, AUTHORITY_LIST);
+			callProviderAndReturnUser(credExpiredUser, INPUT_TOKEN);
+		});
+	}
+
+	@Test
+	public void testUserAccountLockedCredentialsExpired() throws Exception {
+		assertThatExceptionOfType(LockedException.class).isThrownBy(() -> {
+			User lockedUser = new User(TEST_USER, "empty", true, true, true, false, AUTHORITY_LIST);
+			callProviderAndReturnUser(lockedUser, INPUT_TOKEN);
+		});
+	}
+
+	@Test
+	public void testUsernameNotFound() throws Exception {
+		// stubbing
+		given(this.ticketValidator.validateTicket(TEST_TOKEN)).willReturn(TICKET_VALIDATION);
+		given(this.userDetailsService.loadUserByUsername(TEST_USER)).willThrow(new UsernameNotFoundException(""));
+
+		// testing
+		assertThatExceptionOfType(UsernameNotFoundException.class)
+			.isThrownBy(() -> this.provider.authenticate(INPUT_TOKEN));
+	}
+
+	@Test
+	public void testTicketValidationWrong() throws Exception {
+		// stubbing
+		given(this.ticketValidator.validateTicket(TEST_TOKEN)).willThrow(new BadCredentialsException(""));
+
+		// testing
+		assertThatExceptionOfType(BadCredentialsException.class)
+			.isThrownBy(() -> this.provider.authenticate(INPUT_TOKEN));
+	}
+
+	private Authentication callProviderAndReturnUser(UserDetails userDetails, Authentication inputToken) {
+		// stubbing
+		given(this.ticketValidator.validateTicket(TEST_TOKEN)).willReturn(TICKET_VALIDATION);
+		given(this.userDetailsService.loadUserByUsername(TEST_USER)).willReturn(userDetails);
+
+		// testing
+		return this.provider.authenticate(inputToken);
+	}
+
+}

+ 68 - 0
kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosTicketValidationTests.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication;
+
+import javax.security.auth.Subject;
+
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class KerberosTicketValidationTests {
+
+	private String username = "username";
+
+	private Subject subject = new Subject();
+
+	private byte[] responseToken = "token".getBytes();
+
+	private GSSContext gssContext = mock(GSSContext.class);
+
+	private GSSCredential delegationCredential = mock(GSSCredential.class);
+
+	@Test
+	public void createResultOfTicketValidationWithSubject() {
+
+		KerberosTicketValidation ticketValidation = new KerberosTicketValidation(this.username, this.subject,
+				this.responseToken, this.gssContext);
+
+		assertThat(ticketValidation.username()).isEqualTo(this.username);
+		assertThat(ticketValidation.responseToken()).isEqualTo(this.responseToken);
+		assertThat(ticketValidation.getGssContext()).isEqualTo(this.gssContext);
+
+		assertThat(ticketValidation.getDelegationCredential()).withFailMessage("With no credential delegation")
+			.isNull();
+	}
+
+	@Test
+	public void createResultOfTicketValidationWithSubjectAndDelegation() {
+
+		KerberosTicketValidation ticketValidation = new KerberosTicketValidation(this.username, this.subject,
+				this.responseToken, this.gssContext, this.delegationCredential);
+
+		assertThat(ticketValidation.username()).isEqualTo(this.username);
+		assertThat(ticketValidation.responseToken()).isEqualTo(this.responseToken);
+		assertThat(ticketValidation.getGssContext()).isEqualTo(this.gssContext);
+
+		assertThat(ticketValidation.getDelegationCredential()).withFailMessage("With credential delegation")
+			.isEqualTo(this.delegationCredential);
+	}
+
+}

+ 91 - 0
kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidatorTests.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.authentication.sun;
+
+import java.util.Base64;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.BadCredentialsException;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+public class SunJaasKerberosTicketValidatorTests {
+
+	// copy of token taken from a test where windows host
+	// is trying to authenticate with spnego. nothing sensitive here
+	private static String header = "YIIGXAYGKwYBBQUCoIIGUDCCBkygMDAuBgkqhkiC9xIBAgIGCSqGSIb3EgEC"
+			+ "AgYKKwYBBAGCNwICHgYKKwYBBAGCNwICCqKCBhYEggYSYIIGDgYJKoZIhvcS"
+			+ "AQICAQBuggX9MIIF+aADAgEFoQMCAQ6iBwMFACAAAACjggSFYYIEgTCCBH2g"
+			+ "AwIBBaENGwtFWEFNUExFLk9SR6IiMCCgAwIBAqEZMBcbBEhUVFAbD25lby5l"
+			+ "eGFtcGxlLm9yZ6OCBEEwggQ9oAMCARehAwIBA6KCBC8EggQrD8vaEz0V5W5n"
+			+ "PZINBBxp1yCVZOn4kpHzfNtqj9F3L/6MzrTo9bP2l0UhxCQIKo+ixUMJgQAs"
+			+ "Xd82tF4JEsSt90pyv8f751pH3UeqCOhssTcXhJpTKQmYlAro+t3klpT6/c/r"
+			+ "4KX+wqM++19IjWE2CJpyloo/5Wi9Kwk83bjO6UfCTreqkd+eIPM16rf8p/wH"
+			+ "KYj+ssla4y+IvwvZvAW8TXuth8opiqeLvt5H0GWkwuJhrZu6cHlSWZAMtRQg"
+			+ "TSZCS/0LCiZVCyNNCpvvXbyp8p5T6ImKPfMO5l8VJKgdrmCOlAQYFwTpG0MD"
+			+ "1e9LUvk/Fh7OoeglJAygTRgbvIGDAuexw7o6MHbj+XhXvEtC6kUEwHuG5C/1"
+			+ "5Q327FRLfMeL8YcdU6YZ06wNmUmDPGqy+WHlEaFM7G38u/oKKS4cKIZKi8PL"
+			+ "hpVPvjU+uIOJVuIP882IxCW7rcqaRCleYCp7YAQbjussrCS0DSRKPEy60bv0"
+			+ "MIkh71lCY5/KwQloEDMqav12+1wtWTnmLAkfglGjgb1Q7fb79h58nnTBJAwI"
+			+ "e6Bv72XYdgcU1orDQVlylAk9trxDP42yOGuG5IozJTIn+9zPOvM5CGgTCzZv"
+			+ "4wInGa1Stuz11WwaIenwGbpCXWSP4uoe9TLpKVzJUmLd8dpZ0YjpuFNBGnHz"
+			+ "1LG0Q9aUni7nl7seKVc2AnuBqS+mlS+/In0LaEW4k0GctgMqfVyP2mmb7ur+"
+			+ "wl4YjAVRFhPMSSy4AYftRYoIUGad97VcZx107pD0v/gE1Eu4iqTomqJBOaWJ"
+			+ "gqnjmf6A8P9IHbeVx/zbnKYp8nC+M57jpFcy9GKVh3DIXkbSBHQ+feamGBJn"
+			+ "AxTpeix/DN5u91azJaB9RlfIvQYGLGaxupCXpjVfhTSJHvoA6sOUObgK3/hQ"
+			+ "7Gj81FR+C8AfrHzOPPD2S14pkL7n2WC6jOTHrghxm7/iXcreDHos/1OuPFk0"
+			+ "9wbrCWgF9tHAuXQJW/zxjYg9CUboJ51+ZposfmABTKoUKeFY4zgVyuEwE2YO"
+			+ "hn7OLsfbXalmF5IPAlNibAIIFVos1u+14oFOYivIXEEgpvZMhvFOuGaqrHHR"
+			+ "xRBQ/z8nogMVGyCukFH/tg5N8IX9X+VQ1U43rf4IYaCJ0no5skmStf7fmcUJ"
+			+ "+3KXhKfP4TKrSIDdo313GW/6rIM2wo4RPdjQ1LlX+EAb8X73W0OZLumtvhm9"
+			+ "1jL2pWFL/mTGEGkPd7Od29h7JYcvwdDCjkIzIlrbzFJyyTU3ATaMyrvDZKys"
+			+ "ZSJ2m3v7Y0E/Cw+/T8SG3HeSjJ2e/dsjJRpv+6RxXzdNWKKCUN3UFEH0QfAk"
+			+ "6s8avEF767U87Df7BBCuecxIJAUL+kBBsYuDCw8FP0AOxOIjh9EX/EopeJpi"
+			+ "e1ekNGvUK+mhj3WgjCExEe60y4FoENKkggFZMIIBVaADAgEXooIBTASCAUgR"
+			+ "/FTo9JsQB4yInDswmvHiOyJYGdA9jv72rjvJfdHejaU6L8QHj0DPMdGWxAXI"
+			+ "aqLrANjOOSGb9HEdt9QUd/zvi8fBEEZgWIX0nUUrvN9wsKEB1jxmlAx87mf7"
+			+ "2Kyo9z7mdlFBG49mq/jjFFLtiVJxHfea4B4VGRUodNRLWUY7H05ruJZQbeUF"
+			+ "UgYMsiMC59oi82OR3re8gpypecrtD0g88CwCrReDpoLb7VGVCc4z00ld7ugz"
+			+ "EbGsZvh0SLMKnxAAm1nYlqQTu/VKC8zi9N0c7ikJegGwBKOgbebPm+ckKDra"
+			+ "fbVsm0pcmnXv5WvwjJPFjJWsL+7NzUfsedJxgHTCzdztZyNxu6iQf8cpAabp"
+			+ "PB1vJdIMjc8benP9/+EUhX1LkwvV/rOO3ocwjtdLY1rcmNXSbhnf8jDcVjOe" + "eL2PHBfvkne/FgxC";
+
+	// @Rule
+	// public ExpectedException thrown = ExpectedException.none();
+
+	// @Test
+	// public void testJdkMsKrb5OIDRegressionTweak() throws Exception {
+	// thrown.expect(BadCredentialsException.class);
+	// thrown.expectMessage(not(containsString("GSSContext name of the context initiator
+	// is null")));
+	// thrown.expectMessage(containsString("Kerberos validation not successful"));
+	// SunJaasKerberosTicketValidator validator = new SunJaasKerberosTicketValidator();
+	// byte[] kerberosTicket = Base64.decode(header.getBytes());
+	// validator.validateTicket(kerberosTicket);
+	// }
+
+	@Test
+	public void testJdkMsKrb5OIDRegressionTweak() {
+		assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> {
+			SunJaasKerberosTicketValidator validator = new SunJaasKerberosTicketValidator();
+			byte[] kerberosTicket = Base64.getDecoder().decode(header.getBytes());
+			validator.validateTicket(kerberosTicket);
+		}).withMessage("Kerberos validation not successful");
+	}
+
+}

+ 16 - 0
kerberos/kerberos-test/spring-security-kerberos-test.gradle

@@ -0,0 +1,16 @@
+plugins {
+	id 'io.spring.convention.spring-module'
+}
+
+description = 'Spring Security Kerberos Test'
+
+dependencies {
+	management platform(project(":spring-security-dependencies"))
+	api libs.org.apache.kerby.simplekdc
+	api 'org.junit.jupiter:junit-jupiter'
+	testImplementation 'org.springframework:spring-test'
+	testImplementation 'org.mockito:mockito-junit-jupiter'
+	testImplementation libs.org.assertj.assertj.core
+
+	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}

+ 88 - 0
kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/KerberosSecurityTestcase.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.test;
+
+import java.io.File;
+import java.util.Properties;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * KerberosSecurityTestcase provides a base class for using MiniKdc with other testcases.
+ * KerberosSecurityTestcase starts the MiniKdc (@Before) before running tests, and stop
+ * the MiniKdc (@After) after the testcases, using default settings (working dir and kdc
+ * configurations).
+ * <p>
+ * Users can directly inherit this class and implement their own test functions using the
+ * default settings, or override functions getTestDir() and createMiniKdcConf() to provide
+ * new settings.
+ *
+ */
+public class KerberosSecurityTestcase {
+
+	private MiniKdc kdc;
+
+	private File workDir;
+
+	private Properties conf;
+
+	@BeforeEach
+	public void startMiniKdc() throws Exception {
+		createTestDir();
+		createMiniKdcConf();
+
+		this.kdc = new MiniKdc(this.conf, this.workDir);
+		this.kdc.start();
+	}
+
+	/**
+	 * Create a working directory, it should be the build directory. Under this directory
+	 * an ApacheDS working directory will be created, this directory will be deleted when
+	 * the MiniKdc stops.
+	 */
+	public void createTestDir() {
+		this.workDir = new File(System.getProperty("test.dir", "target"));
+	}
+
+	/**
+	 * Create a Kdc configuration
+	 */
+	public void createMiniKdcConf() {
+		this.conf = MiniKdc.createConf();
+	}
+
+	@AfterEach
+	public void stopMiniKdc() {
+		if (this.kdc != null) {
+			this.kdc.stop();
+		}
+	}
+
+	public MiniKdc getKdc() {
+		return this.kdc;
+	}
+
+	public File getWorkDir() {
+		return this.workDir;
+	}
+
+	public Properties getConf() {
+		return this.conf;
+	}
+
+}

+ 429 - 0
kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/MiniKdc.java

@@ -0,0 +1,429 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.kerby.kerberos.kerb.KrbException;
+import org.apache.kerby.kerberos.kerb.server.KdcConfigKey;
+import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
+import org.apache.kerby.util.IOUtil;
+import org.apache.kerby.util.NetworkUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Mini KDC based on Apache Directory Server that can be embedded in testcases or used
+ * from command line as a standalone KDC.
+ * <p>
+ * <b>From within testcases:</b>
+ * <p>
+ * MiniKdc sets one System property when started and un-set when stopped:
+ * <ul>
+ * <li>sun.security.krb5.debug: set to the debug value provided in the configuration</li>
+ * </ul>
+ * Because of this, multiple MiniKdc instances cannot be started in parallel. For example,
+ * running testcases in parallel that start a KDC each. To accomplish this a single
+ * MiniKdc should be used for all testcases running in parallel.
+ * <p>
+ * MiniKdc default configuration values are:
+ * <ul>
+ * <li>org.name=EXAMPLE (used to create the REALM)</li>
+ * <li>org.domain=COM (used to create the REALM)</li>
+ * <li>kdc.bind.address=localhost</li>
+ * <li>kdc.port=0 (ephemeral port)</li>
+ * <li>instance=DefaultKrbServer</li>
+ * <li>max.ticket.lifetime=86400000 (1 day)</li>
+ * <li>max.renewable.lifetime=604800000 (7 days)</li>
+ * <li>transport=TCP</li>
+ * <li>debug=false</li>
+ * </ul>
+ * The generated krb5.conf forces TCP connections.
+ *
+ * @author Original Hadoop MiniKdc Authors
+ * @author Janne Valkealahti
+ * @author Bogdan Mustiata
+ */
+public class MiniKdc {
+
+	public static final String JAVA_SECURITY_KRB5_CONF = "java.security.krb5.conf";
+
+	public static final String SUN_SECURITY_KRB5_DEBUG = "sun.security.krb5.debug";
+
+	public static void main(String[] args) throws Exception {
+		if (args.length < 4) {
+			System.out.println("Arguments: <WORKDIR> <MINIKDCPROPERTIES> " + "<KEYTABFILE> [<PRINCIPALS>]+");
+			System.exit(1);
+		}
+		File workDir = new File(args[0]);
+		if (!workDir.exists()) {
+			throw new RuntimeException("Specified work directory does not exists: " + workDir.getAbsolutePath());
+		}
+		Properties conf = createConf();
+		File file = new File(args[1]);
+		if (!file.exists()) {
+			throw new RuntimeException("Specified configuration does not exists: " + file.getAbsolutePath());
+		}
+		Properties userConf = new Properties();
+		InputStreamReader r = null;
+		try {
+			r = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
+			userConf.load(r);
+		}
+		finally {
+			if (r != null) {
+				r.close();
+			}
+		}
+		for (Map.Entry<?, ?> entry : userConf.entrySet()) {
+			conf.put(entry.getKey(), entry.getValue());
+		}
+		final MiniKdc miniKdc = new MiniKdc(conf, workDir);
+		miniKdc.start();
+		File krb5conf = new File(workDir, "krb5.conf");
+		if (miniKdc.getKrb5conf().renameTo(krb5conf)) {
+			File keytabFile = new File(args[2]).getAbsoluteFile();
+			String[] principals = new String[args.length - 3];
+			System.arraycopy(args, 3, principals, 0, args.length - 3);
+			miniKdc.createPrincipal(keytabFile, principals);
+			System.out.println();
+			System.out.println("Standalone MiniKdc Running");
+			System.out.println("---------------------------------------------------");
+			System.out.println("  Realm           : " + miniKdc.getRealm());
+			System.out.println("  Running at      : " + miniKdc.getHost() + ":" + miniKdc.getPort());
+			System.out.println("  krb5conf        : " + krb5conf);
+			System.out.println();
+			System.out.println("  created keytab  : " + keytabFile);
+			System.out.println("  with principals : " + Arrays.asList(principals));
+			System.out.println();
+			System.out.println(" Do <CTRL-C> or kill <PID> to stop it");
+			System.out.println("---------------------------------------------------");
+			System.out.println();
+			Runtime.getRuntime().addShutdownHook(new Thread() {
+				@Override
+				public void run() {
+					miniKdc.stop();
+				}
+			});
+		}
+		else {
+			throw new RuntimeException("Cannot rename KDC's krb5conf to " + krb5conf.getAbsolutePath());
+		}
+	}
+
+	private static final Logger LOG = LoggerFactory.getLogger(MiniKdc.class);
+
+	public static final String ORG_NAME = "org.name";
+
+	public static final String ORG_DOMAIN = "org.domain";
+
+	public static final String KDC_BIND_ADDRESS = "kdc.bind.address";
+
+	public static final String KDC_PORT = "kdc.port";
+
+	public static final String INSTANCE = "instance";
+
+	public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime";
+
+	public static final String MIN_TICKET_LIFETIME = "min.ticket.lifetime";
+
+	public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime";
+
+	public static final String TRANSPORT = "transport";
+
+	public static final String DEBUG = "debug";
+
+	private static final Set<String> PROPERTIES = new HashSet<String>();
+
+	private static final Properties DEFAULT_CONFIG = new Properties();
+
+	static {
+		PROPERTIES.add(ORG_NAME);
+		PROPERTIES.add(ORG_DOMAIN);
+		PROPERTIES.add(KDC_BIND_ADDRESS);
+		PROPERTIES.add(KDC_BIND_ADDRESS);
+		PROPERTIES.add(KDC_PORT);
+		PROPERTIES.add(INSTANCE);
+		PROPERTIES.add(TRANSPORT);
+		PROPERTIES.add(MAX_TICKET_LIFETIME);
+		PROPERTIES.add(MAX_RENEWABLE_LIFETIME);
+
+		DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost");
+		DEFAULT_CONFIG.setProperty(KDC_PORT, "0");
+		DEFAULT_CONFIG.setProperty(INSTANCE, "DefaultKrbServer");
+		DEFAULT_CONFIG.setProperty(ORG_NAME, "EXAMPLE");
+		DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM");
+		DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP");
+		DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000");
+		DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000");
+		DEFAULT_CONFIG.setProperty(DEBUG, "false");
+	}
+
+	/**
+	 * Convenience method that returns MiniKdc default configuration.
+	 * <p>
+	 * The returned configuration is a copy, it can be customized before using it to
+	 * create a MiniKdc.
+	 * @return a MiniKdc default configuration.
+	 */
+	public static Properties createConf() {
+		return (Properties) DEFAULT_CONFIG.clone();
+	}
+
+	private Properties conf;
+
+	private SimpleKdcServer simpleKdc;
+
+	private int port;
+
+	private String realm;
+
+	private File workDir;
+
+	private File krb5conf;
+
+	private String transport;
+
+	private boolean krb5Debug;
+
+	public void setTransport(String transport) {
+		this.transport = transport;
+	}
+
+	/**
+	 * Creates a MiniKdc.
+	 * @param conf MiniKdc configuration.
+	 * @param workDir working directory, it should be the build directory. Under this
+	 * directory an ApacheDS working directory will be created, this directory will be
+	 * deleted when the MiniKdc stops.
+	 * @throws Exception thrown if the MiniKdc could not be created.
+	 */
+	public MiniKdc(Properties conf, File workDir) throws Exception {
+		if (!conf.keySet().containsAll(PROPERTIES)) {
+			Set<String> missingProperties = new HashSet<String>(PROPERTIES);
+			missingProperties.removeAll(conf.keySet());
+			throw new IllegalArgumentException("Missing configuration properties: " + missingProperties);
+		}
+		this.workDir = new File(workDir, Long.toString(System.currentTimeMillis()));
+		if (!this.workDir.exists() && !this.workDir.mkdirs()) {
+			throw new RuntimeException("Cannot create directory " + this.workDir);
+		}
+		LOG.info("Configuration:");
+		LOG.info("---------------------------------------------------------------");
+		for (Map.Entry<?, ?> entry : conf.entrySet()) {
+			LOG.info("  {}: {}", entry.getKey(), entry.getValue());
+		}
+		LOG.info("---------------------------------------------------------------");
+		this.conf = conf;
+		this.port = Integer.parseInt(conf.getProperty(KDC_PORT));
+		String orgName = conf.getProperty(ORG_NAME);
+		String orgDomain = conf.getProperty(ORG_DOMAIN);
+		this.realm = orgName.toUpperCase(Locale.ENGLISH) + "." + orgDomain.toUpperCase(Locale.ENGLISH);
+	}
+
+	/**
+	 * Returns the port of the MiniKdc.
+	 * @return the port of the MiniKdc.
+	 */
+	public int getPort() {
+		return this.port;
+	}
+
+	/**
+	 * Returns the host of the MiniKdc.
+	 * @return the host of the MiniKdc.
+	 */
+	public String getHost() {
+		return this.conf.getProperty(KDC_BIND_ADDRESS);
+	}
+
+	/**
+	 * Returns the realm of the MiniKdc.
+	 * @return the realm of the MiniKdc.
+	 */
+	public String getRealm() {
+		return this.realm;
+	}
+
+	public File getKrb5conf() {
+		this.krb5conf = new File(System.getProperty(JAVA_SECURITY_KRB5_CONF));
+		return this.krb5conf;
+	}
+
+	/**
+	 * Starts the MiniKdc.
+	 * @throws Exception thrown if the MiniKdc could not be started.
+	 */
+	public synchronized void start() throws Exception {
+		if (this.simpleKdc != null) {
+			throw new RuntimeException("Already started");
+		}
+		this.simpleKdc = new SimpleKdcServer();
+		prepareKdcServer();
+		this.simpleKdc.init();
+		resetDefaultRealm();
+		this.simpleKdc.start();
+		LOG.info("MiniKdc started.");
+	}
+
+	private void resetDefaultRealm() throws IOException {
+		InputStream templateResource = new FileInputStream(getKrb5conf().getAbsolutePath());
+		String content = IOUtil.readInput(templateResource);
+		content = content.replaceAll("default_realm = .*\n", "default_realm = " + getRealm() + "\n");
+		IOUtil.writeFile(content, getKrb5conf());
+	}
+
+	private void prepareKdcServer() throws Exception {
+		// transport
+		this.simpleKdc.setWorkDir(this.workDir);
+		this.simpleKdc.setKdcHost(getHost());
+		this.simpleKdc.setKdcRealm(this.realm);
+		if (this.transport == null) {
+			this.transport = this.conf.getProperty(TRANSPORT);
+		}
+		if (this.port == 0) {
+			this.port = NetworkUtil.getServerPort();
+		}
+		if (this.transport != null) {
+			if (this.transport.trim().equals("TCP")) {
+				this.simpleKdc.setKdcTcpPort(this.port);
+				this.simpleKdc.setAllowUdp(false);
+			}
+			else if (this.transport.trim().equals("UDP")) {
+				this.simpleKdc.setKdcUdpPort(this.port);
+				this.simpleKdc.setAllowTcp(false);
+			}
+			else {
+				throw new IllegalArgumentException("Invalid transport: " + this.transport);
+			}
+		}
+		else {
+			throw new IllegalArgumentException("Need to set transport!");
+		}
+		this.simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, this.conf.getProperty(INSTANCE));
+		if (this.conf.getProperty(DEBUG) != null) {
+			this.krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, this.conf.getProperty(DEBUG));
+		}
+		if (this.conf.getProperty(MIN_TICKET_LIFETIME) != null) {
+			this.simpleKdc.getKdcConfig()
+				.setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME,
+						Long.parseLong(this.conf.getProperty(MIN_TICKET_LIFETIME)));
+		}
+		if (this.conf.getProperty(MAX_TICKET_LIFETIME) != null) {
+			this.simpleKdc.getKdcConfig()
+				.setLong(KdcConfigKey.MAXIMUM_TICKET_LIFETIME,
+						Long.parseLong(this.conf.getProperty(MiniKdc.MAX_TICKET_LIFETIME)));
+		}
+	}
+
+	/**
+	 * Stops the MiniKdc
+	 */
+	public synchronized void stop() {
+		if (this.simpleKdc != null) {
+			try {
+				this.simpleKdc.stop();
+			}
+			catch (KrbException ex) {
+				ex.printStackTrace();
+			}
+			finally {
+				if (this.conf.getProperty(DEBUG) != null) {
+					System.setProperty(SUN_SECURITY_KRB5_DEBUG, Boolean.toString(this.krb5Debug));
+				}
+			}
+		}
+		delete(this.workDir);
+		try {
+			// Will be fixed in next Kerby version.
+			Thread.sleep(1000);
+		}
+		catch (InterruptedException ex) {
+			ex.printStackTrace();
+		}
+		LOG.info("MiniKdc stopped.");
+	}
+
+	private void delete(File f) {
+		if (f.isFile()) {
+			if (!f.delete()) {
+				LOG.warn("WARNING: cannot delete file " + f.getAbsolutePath());
+			}
+		}
+		else {
+			File[] fileList = f.listFiles();
+			if (fileList != null) {
+				for (File c : fileList) {
+					delete(c);
+				}
+			}
+			if (!f.delete()) {
+				LOG.warn("WARNING: cannot delete directory " + f.getAbsolutePath());
+			}
+		}
+	}
+
+	/**
+	 * Creates a principal in the KDC with the specified user and password.
+	 * @param principal principal name, do not include the domain.
+	 * @param password password.
+	 * @throws Exception thrown if the principal could not be created.
+	 */
+	public synchronized void createPrincipal(String principal, String password) throws Exception {
+		this.simpleKdc.createPrincipal(principal, password);
+	}
+
+	/**
+	 * Creates multiple principals in the KDC and adds them to a keytab file.
+	 * @param keytabFile keytab file to add the created principals.
+	 * @param principals principals to add to the KDC, do not include the domain.
+	 * @throws Exception thrown if the principals or the keytab file could not be created.
+	 */
+	public synchronized void createPrincipal(File keytabFile, String... principals) throws Exception {
+		this.simpleKdc.createPrincipals(principals);
+		if (keytabFile.exists() && !keytabFile.delete()) {
+			LOG.error("Failed to delete keytab file: " + keytabFile);
+		}
+		for (String principal : principals) {
+			this.simpleKdc.getKadmin().exportKeytab(keytabFile, principal);
+		}
+	}
+
+	/**
+	 * Set the System property; return the old value for caching.
+	 * @param sysprop property
+	 * @param debug true or false
+	 * @return the previous value
+	 */
+	private boolean getAndSet(String sysprop, String debug) {
+		boolean old = Boolean.getBoolean(sysprop);
+		System.setProperty(sysprop, debug);
+		return old;
+	}
+
+}

+ 192 - 0
kerberos/kerberos-test/src/test/java/org/springframework/security/kerberos/test/TestMiniKdc.java

@@ -0,0 +1,192 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.test;
+
+import java.io.File;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+
+import org.apache.kerby.kerberos.kerb.keytab.Keytab;
+import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestMiniKdc extends KerberosSecurityTestcase {
+
+	private static final boolean IBM_JAVA = shouldUseIbmPackages();
+
+	// duplicated to avoid cycles in the build
+	private static boolean shouldUseIbmPackages() {
+		final List<String> ibmTechnologyEditionSecurityModules = Arrays.asList(
+				"com.ibm.security.auth.module.JAASLoginModule", "com.ibm.security.auth.module.Win64LoginModule",
+				"com.ibm.security.auth.module.NTLoginModule", "com.ibm.security.auth.module.AIX64LoginModule",
+				"com.ibm.security.auth.module.LinuxLoginModule", "com.ibm.security.auth.module.Krb5LoginModule");
+
+		if (System.getProperty("java.vendor").contains("IBM")) {
+			return ibmTechnologyEditionSecurityModules.stream().anyMatch((module) -> isSystemClassAvailable(module));
+		}
+
+		return false;
+	}
+
+	@Test
+	public void testKerberosLogin() throws Exception {
+		MiniKdc kdc = getKdc();
+		File workDir = getWorkDir();
+		LoginContext loginContext = null;
+		try {
+			String principal = "foo";
+			File keytab = new File(workDir, "foo.keytab");
+			kdc.createPrincipal(keytab, principal);
+
+			Set<Principal> principals = new HashSet<Principal>();
+			principals.add(new KerberosPrincipal(principal));
+
+			// client login
+			Subject subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());
+			loginContext = new LoginContext("", subject, null,
+					KerberosConfiguration.createClientConfig(principal, keytab));
+			loginContext.login();
+			subject = loginContext.getSubject();
+			assertThat(subject.getPrincipals().size()).isEqualTo(1);
+			assertThat(subject.getPrincipals().iterator().next().getClass()).isEqualTo(KerberosPrincipal.class);
+			assertThat(subject.getPrincipals().iterator().next().getName()).isEqualTo(principal + "@" + kdc.getRealm());
+			loginContext.logout();
+
+			// server login
+			subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());
+			loginContext = new LoginContext("", subject, null,
+					KerberosConfiguration.createServerConfig(principal, keytab));
+			loginContext.login();
+			subject = loginContext.getSubject();
+			assertThat(subject.getPrincipals().size()).isEqualTo(1);
+			assertThat(subject.getPrincipals().iterator().next().getClass()).isEqualTo(KerberosPrincipal.class);
+			assertThat(subject.getPrincipals().iterator().next().getName()).isEqualTo(principal + "@" + kdc.getRealm());
+			loginContext.logout();
+
+		}
+		finally {
+			if (loginContext != null && loginContext.getSubject() != null
+					&& !loginContext.getSubject().getPrivateCredentials().isEmpty()) {
+				loginContext.logout();
+			}
+		}
+	}
+
+	private static boolean isSystemClassAvailable(String className) {
+		try {
+			Class.forName(className);
+			return true;
+		}
+		catch (Exception ignored) {
+			return false;
+		}
+	}
+
+	@Test
+	public void testMiniKdcStart() {
+		MiniKdc kdc = getKdc();
+		assertThat(kdc.getPort()).isNotEqualTo(0);
+	}
+
+	@Test
+	public void testKeytabGen() throws Exception {
+		MiniKdc kdc = getKdc();
+		File workDir = getWorkDir();
+
+		kdc.createPrincipal(new File(workDir, "keytab"), "foo/bar", "bar/foo");
+		List<PrincipalName> principalNameList = Keytab.loadKeytab(new File(workDir, "keytab")).getPrincipals();
+
+		Set<String> principals = new HashSet<String>();
+		for (PrincipalName principalName : principalNameList) {
+			principals.add(principalName.getName());
+		}
+
+		assertThat(principals).containsExactlyInAnyOrder("foo/bar@" + kdc.getRealm(), "bar/foo@" + kdc.getRealm());
+
+	}
+
+	private static final class KerberosConfiguration extends Configuration {
+
+		private String principal;
+
+		private String keytab;
+
+		private boolean isInitiator;
+
+		private KerberosConfiguration(String principal, File keytab, boolean client) {
+			this.principal = principal;
+			this.keytab = keytab.getAbsolutePath();
+			this.isInitiator = client;
+		}
+
+		private static Configuration createClientConfig(String principal, File keytab) {
+			return new KerberosConfiguration(principal, keytab, true);
+		}
+
+		private static Configuration createServerConfig(String principal, File keytab) {
+			return new KerberosConfiguration(principal, keytab, false);
+		}
+
+		private static String getKrb5LoginModuleName() {
+			return System.getProperty("java.vendor").contains("IBM") ? "com.ibm.security.auth.module.Krb5LoginModule"
+					: "com.sun.security.auth.module.Krb5LoginModule";
+		}
+
+		@Override
+		public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+			Map<String, String> options = new HashMap<String, String>();
+			options.put("principal", this.principal);
+			options.put("refreshKrb5Config", "true");
+			if (IBM_JAVA) {
+				options.put("useKeytab", this.keytab);
+				options.put("credsType", "both");
+			}
+			else {
+				options.put("keyTab", this.keytab);
+				options.put("useKeyTab", "true");
+				options.put("storeKey", "true");
+				options.put("doNotPrompt", "true");
+				options.put("useTicketCache", "true");
+				options.put("renewTGT", "true");
+				options.put("isInitiator", Boolean.toString(this.isInitiator));
+			}
+			String ticketCache = System.getenv("KRB5CCNAME");
+			if (ticketCache != null) {
+				options.put("ticketCache", ticketCache);
+			}
+			options.put("debug", "true");
+
+			return new AppConfigurationEntry[] { new AppConfigurationEntry(getKrb5LoginModuleName(),
+					AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) };
+		}
+
+	}
+
+}

+ 10 - 0
kerberos/kerberos-test/src/test/resources/log4j.properties

@@ -0,0 +1,10 @@
+log4j.rootCategory=INFO, stdout
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} - %m%n
+
+log4j.category.org.springframework.boot=INFO
+xlog4j.category.org.apache.http.wire=TRACE
+xlog4j.category.org.apache.http.headers=TRACE
+

+ 25 - 0
kerberos/kerberos-test/src/test/resources/minikdc-krb5.conf

@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+[libdefaults]
+    default_realm = {0}
+    udp_preference_limit = 1
+
+[realms]
+    {0} = '{'
+        kdc = {1}:{2}
+    '}'

+ 47 - 0
kerberos/kerberos-test/src/test/resources/minikdc.ldiff

@@ -0,0 +1,47 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+dn: ou=users,dc=${0},dc=${1}
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+dn: uid=krbtgt,ou=users,dc=${0},dc=${1}
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: KDC Service
+sn: Service
+uid: krbtgt
+userPassword: secret
+krb5PrincipalName: krbtgt/${2}.${3}@${2}.${3}
+krb5KeyVersionNumber: 0
+
+dn: uid=ldap,ou=users,dc=${0},dc=${1}
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: LDAP
+sn: Service
+uid: ldap
+userPassword: secret
+krb5PrincipalName: ldap/${4}@${2}.${3}
+krb5KeyVersionNumber: 0

+ 19 - 0
kerberos/kerberos-web/spring-security-kerberos-web.gradle

@@ -0,0 +1,19 @@
+plugins {
+	id 'io.spring.convention.spring-module'
+}
+
+description = 'Spring Security Kerberos Web'
+
+dependencies {
+	management platform(project(":spring-security-dependencies"))
+	implementation project(':spring-security-kerberos-core')
+	api(project(':spring-security-web'))
+	api(libs.jakarta.servlet.jakarta.servlet.api)
+	testImplementation 'org.springframework:spring-test'
+	testImplementation project(':spring-security-config')
+	testImplementation 'org.junit.jupiter:junit-jupiter'
+	testImplementation 'org.mockito:mockito-junit-jupiter'
+	testImplementation libs.org.assertj.assertj.core
+
+	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}

+ 71 - 0
kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.web.authentication;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+
+/**
+ * Adds a WWW-Authenticate (or other) header to the response following successful
+ * authentication.
+ *
+ * @author Jeremy Stone
+ */
+public class ResponseHeaderSettingKerberosAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+	private static final String NEGOTIATE_PREFIX = "Negotiate ";
+
+	private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+
+	private String headerName = WWW_AUTHENTICATE;
+
+	private String headerPrefix = NEGOTIATE_PREFIX;
+
+	/**
+	 * Sets the name of the header to set. By default this is 'WWW-Authenticate'.
+	 * @param headerName the www authenticate header name
+	 */
+	public void setHeaderName(String headerName) {
+		this.headerName = headerName;
+	}
+
+	/**
+	 * Sets the value of the prefix for the encoded response token value. By default this
+	 * is 'Negotiate '.
+	 * @param headerPrefix the negotiate prefix
+	 */
+	public void setHeaderPrefix(String headerPrefix) {
+		this.headerPrefix = headerPrefix;
+	}
+
+	@Override
+	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
+			Authentication authentication) throws IOException, ServletException {
+		KerberosServiceRequestToken auth = (KerberosServiceRequestToken) authentication;
+		if (auth.hasResponseToken()) {
+			response.addHeader(this.headerName, this.headerPrefix + auth.getEncodedResponseToken());
+		}
+	}
+
+}

+ 320 - 0
kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoAuthenticationProcessingFilter.java

@@ -0,0 +1,320 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.web.authentication;
+
+import java.io.IOException;
+import java.util.Base64;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
+import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * Parses the SPNEGO authentication Header, which was generated by the browser and creates
+ * a {@link KerberosServiceRequestToken} out if it. It will then call the
+ * {@link AuthenticationManager}.
+ *
+ * <p>
+ * A typical Spring Security configuration might look like this:
+ * </p>
+ *
+ * <pre>
+ * &lt;beans xmlns=&quot;https://www.springframework.org/schema/beans&quot;
+ * xmlns:xsi=&quot;https://www.w3.org/2001/XMLSchema-instance&quot; xmlns:sec=&quot;https://www.springframework.org/schema/security&quot;
+ * xsi:schemaLocation=&quot;https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
+ * 	https://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security-3.0.xsd&quot;&gt;
+ *
+ * &lt;sec:http entry-point-ref=&quot;spnegoEntryPoint&quot;&gt;
+ * 	&lt;sec:intercept-url pattern=&quot;/secure/**&quot; access=&quot;IS_AUTHENTICATED_FULLY&quot; /&gt;
+ * 	&lt;sec:custom-filter ref=&quot;spnegoAuthenticationProcessingFilter&quot; position=&quot;BASIC_AUTH_FILTER&quot; /&gt;
+ * &lt;/sec:http&gt;
+ *
+ * &lt;bean id=&quot;spnegoEntryPoint&quot; class=&quot;org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint&quot; /&gt;
+ *
+ * &lt;bean id=&quot;spnegoAuthenticationProcessingFilter&quot;
+ * 	class=&quot;org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter&quot;&gt;
+ * 	&lt;property name=&quot;authenticationManager&quot; ref=&quot;authenticationManager&quot; /&gt;
+ * &lt;/bean&gt;
+ *
+ * &lt;sec:authentication-manager alias=&quot;authenticationManager&quot;&gt;
+ * 	&lt;sec:authentication-provider ref=&quot;kerberosServiceAuthenticationProvider&quot; /&gt;
+ * &lt;/sec:authentication-manager&gt;
+ *
+ * &lt;bean id=&quot;kerberosServiceAuthenticationProvider&quot;
+ * 	class=&quot;org.springframework.security.kerberos.authenitcation.KerberosServiceAuthenticationProvider&quot;&gt;
+ * 	&lt;property name=&quot;ticketValidator&quot;&gt;
+ * 		&lt;bean class=&quot;org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator&quot;&gt;
+ * 			&lt;property name=&quot;servicePrincipal&quot; value=&quot;HTTP/web.springsource.com&quot; /&gt;
+ * 			&lt;property name=&quot;keyTabLocation&quot; value=&quot;classpath:http-java.keytab&quot; /&gt;
+ * 		&lt;/bean&gt;
+ * 	&lt;/property&gt;
+ * 	&lt;property name=&quot;userDetailsService&quot; ref=&quot;inMemoryUserDetailsService&quot; /&gt;
+ * &lt;/bean&gt;
+ *
+ * &lt;bean id=&quot;inMemoryUserDetailsService&quot;
+ * 	class=&quot;org.springframework.security.core.userdetails.memory.InMemoryDaoImpl&quot;&gt;
+ * 	&lt;property name=&quot;userProperties&quot;&gt;
+ * 		&lt;value&gt;
+ * 			mike@SECPOD.DE=notUsed,ROLE_ADMIN
+ * 		&lt;/value&gt;
+ * 	&lt;/property&gt;
+ * &lt;/bean&gt;
+ * &lt;/beans&gt;
+ * </pre>
+ *
+ * <p>
+ * If you get a "GSSException: Channel binding mismatch (Mechanism level:ChannelBinding
+ * not provided!) have a look at this
+ * <a href="https://bugs.sun.com/view_bug.do?bug_id=6851973">bug</a>.
+ * </p>
+ * <p>
+ * A workaround unti this is fixed in the JVM is to change
+ * </p>
+ * HKEY_LOCAL_MACHINE\System \CurrentControlSet\Control\LSA\SuppressExtendedProtection to
+ * 0x02
+ *
+ * @author Mike Wiesner
+ * @author Jeremy Stone
+ * @author Denis Angilella
+ * @since 1.0
+ * @see KerberosServiceAuthenticationProvider
+ * @see SpnegoEntryPoint
+ */
+public class SpnegoAuthenticationProcessingFilter extends OncePerRequestFilter {
+
+	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
+		.getContextHolderStrategy();
+
+	private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
+
+	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
+
+	private AuthenticationManager authenticationManager;
+
+	private AuthenticationSuccessHandler successHandler;
+
+	private AuthenticationFailureHandler failureHandler;
+
+	private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
+
+	private boolean skipIfAlreadyAuthenticated = true;
+
+	private boolean stopFilterChainOnSuccessfulAuthentication = false;
+
+	/**
+	 * Authentication header prefix sent by IE/Windows when the domain controller fails to
+	 * issue a Kerberos ticket for the URL.
+	 *
+	 * "TlRMTVNTUA" is the base64 encoding of "NTLMSSP". This will be followed by the
+	 * actual token.
+	 **/
+	private static final String NTLMSSP_PREFIX = "Negotiate TlRMTVNTUA";
+
+	@Override
+	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+			throws ServletException, IOException {
+
+		if (this.skipIfAlreadyAuthenticated) {
+			Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
+
+			if (existingAuth != null && existingAuth.isAuthenticated()
+					&& !(existingAuth instanceof AnonymousAuthenticationToken)) {
+				chain.doFilter(request, response);
+				return;
+			}
+		}
+
+		String header = request.getHeader("Authorization");
+
+		if (header != null && ((header.startsWith("Negotiate ") && !header.startsWith(NTLMSSP_PREFIX))
+				|| header.startsWith("Kerberos "))) {
+			if (this.logger.isDebugEnabled()) {
+				this.logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header);
+			}
+			byte[] base64Token = header.substring(header.indexOf(" ") + 1).getBytes("UTF-8");
+			byte[] kerberosTicket = Base64.getDecoder().decode(base64Token);
+			KerberosServiceRequestToken authenticationRequest = new KerberosServiceRequestToken(kerberosTicket);
+			authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
+			Authentication authentication;
+			try {
+				authentication = this.authenticationManager.authenticate(authenticationRequest);
+			}
+			catch (AuthenticationException ex) {
+				// That shouldn't happen, as it is most likely a wrong
+				// configuration on the server side
+				this.logger.warn("Negotiate Header was invalid: " + header, ex);
+				this.securityContextHolderStrategy.clearContext();
+				if (this.failureHandler != null) {
+					this.failureHandler.onAuthenticationFailure(request, response, ex);
+				}
+				else {
+					response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+					response.flushBuffer();
+				}
+				return;
+			}
+			this.sessionStrategy.onAuthentication(authentication, request, response);
+
+			SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
+			context.setAuthentication(authentication);
+			this.securityContextHolderStrategy.setContext(context);
+			this.securityContextRepository.saveContext(context, request, response);
+			if (this.successHandler != null) {
+				this.successHandler.onAuthenticationSuccess(request, response, authentication);
+			}
+			if (this.stopFilterChainOnSuccessfulAuthentication) {
+				return;
+			}
+		}
+
+		chain.doFilter(request, response);
+
+	}
+
+	@Override
+	public void afterPropertiesSet() throws ServletException {
+		super.afterPropertiesSet();
+		Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
+	}
+
+	/**
+	 * The authentication manager for validating the ticket.
+	 * @param authenticationManager the authentication manager
+	 */
+	public void setAuthenticationManager(AuthenticationManager authenticationManager) {
+		this.authenticationManager = authenticationManager;
+	}
+
+	/**
+	 * <p>
+	 * This handler is called after a successful authentication. One can add additional
+	 * authentication behavior by setting this.
+	 * </p>
+	 * <p>
+	 * Default is null, which means nothing additional happens
+	 * </p>
+	 * @param successHandler the authentication success handler
+	 */
+	public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
+		this.successHandler = successHandler;
+	}
+
+	/**
+	 * <p>
+	 * This handler is called after a failure authentication. In most cases you only get
+	 * Kerberos/SPNEGO failures with a wrong server or network configurations and not
+	 * during runtime. If the client encounters an error, he will just stop the
+	 * communication with server and therefore this handler will not be called in this
+	 * case.
+	 * </p>
+	 * <p>
+	 * Default is null, which means that the Filter returns the HTTP 500 code
+	 * </p>
+	 * @param failureHandler the authentication failure handler
+	 */
+	public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
+		this.failureHandler = failureHandler;
+	}
+
+	/**
+	 * Should Kerberos authentication be skipped if a user is already authenticated for
+	 * this request (e.g. in the HTTP session).
+	 * @param skipIfAlreadyAuthenticated default is true
+	 */
+	public void setSkipIfAlreadyAuthenticated(boolean skipIfAlreadyAuthenticated) {
+		this.skipIfAlreadyAuthenticated = skipIfAlreadyAuthenticated;
+	}
+
+	/**
+	 * The session handling strategy which will be invoked immediately after an
+	 * authentication request is successfully processed by the
+	 * <tt>AuthenticationManager</tt>. Used, for example, to handle changing of the
+	 * session identifier to prevent session fixation attacks.
+	 * @param sessionStrategy the implementation to use. If not set a null implementation
+	 * is used.
+	 */
+	public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
+		this.sessionStrategy = sessionStrategy;
+	}
+
+	/**
+	 * Sets the authentication details source.
+	 * @param authenticationDetailsSource the authentication details source
+	 */
+	public void setAuthenticationDetailsSource(
+			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
+		Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
+		this.authenticationDetailsSource = authenticationDetailsSource;
+	}
+
+	/**
+	 * If set to {@code false} (the default) and authentication is successful, the request
+	 * will be processed by the next filter in the chain. If {@code true} and
+	 * authentication is successful, the filter chain will stop here.
+	 * @param shouldStop set to {@code true} to prevent the next filter in the chain from
+	 * processing the request after a successful authentication.
+	 * @since 1.0.2
+	 */
+	public void setStopFilterChainOnSuccessfulAuthentication(boolean shouldStop) {
+		this.stopFilterChainOnSuccessfulAuthentication = shouldStop;
+	}
+
+	/**
+	 * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
+	 * authentication success. The default action is not to save the
+	 * {@link SecurityContext}.
+	 * @param securityContextRepository the {@link SecurityContextRepository} to use.
+	 * Cannot be null.
+	 */
+	public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
+		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
+		this.securityContextRepository = securityContextRepository;
+	}
+
+	/**
+	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
+	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
+	 * @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
+	 * use. Cannot be null.
+	 */
+	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
+		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
+		this.securityContextHolderStrategy = securityContextHolderStrategy;
+	}
+
+}

+ 142 - 0
kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoEntryPoint.java

@@ -0,0 +1,142 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.web.authentication;
+
+import java.io.IOException;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Sends back a request for a Negotiate Authentication to the browser.
+ *
+ * <p>
+ * With optional configured <code>forwardUrl</code> it is possible to use form login as
+ * fallback authentication.
+ * </p>
+ *
+ * <p>
+ * This approach enables security configuration to use SPNEGO in combination with login
+ * form as fallback for clients that do not support this kind of authentication. Set
+ * Response Code 401 - unauthorized and forward to login page. A useful scenario might be
+ * an environment where windows domain is present but it is required to access the
+ * application also from non domain client devices. One could use a combination with form
+ * based LDAP login.
+ * </p>
+ *
+ * <p>
+ * See <code>spnego-with-form-login.xml</code> in spring-security-kerberos-sample for
+ * details
+ * </p>
+ *
+ * @author Mike Wiesner
+ * @author Andre Schaefer, Namics AG
+ * @since 1.0
+ * @see SpnegoAuthenticationProcessingFilter
+ */
+public class SpnegoEntryPoint implements AuthenticationEntryPoint {
+
+	private static final Log LOG = LogFactory.getLog(SpnegoEntryPoint.class);
+
+	private final String forwardUrl;
+
+	private final HttpMethod forwardMethod;
+
+	private final boolean forward;
+
+	/**
+	 * Instantiates a new spnego entry point. Using this constructor the EntryPoint will
+	 * Sends back a request for a Negotiate Authentication to the browser without
+	 * providing a fallback mechanism for login, Use constructor with forwardUrl to
+	 * provide form based login.
+	 */
+	public SpnegoEntryPoint() {
+		this(null);
+	}
+
+	/**
+	 * Instantiates a new spnego entry point. This constructor enables security
+	 * configuration to use SPNEGO in combination with a fallback page (login form, custom
+	 * 401 page ...). The forward method will be the same as the original request.
+	 * @param forwardUrl URL where the login page can be found. Should be relative to the
+	 * web-app context path (include a leading {@code /}) and can't be absolute URL.
+	 */
+	public SpnegoEntryPoint(String forwardUrl) {
+		this(forwardUrl, null);
+	}
+
+	/**
+	 * Instantiates a new spnego entry point. This constructor enables security
+	 * configuration to use SPNEGO in combination a fallback page (login form, custom 401
+	 * page ...). The forward URL will be accessed via provided HTTP method.
+	 * @param forwardUrl URL where the login page can be found. Should be relative to the
+	 * web-app context path (include a leading {@code /}) and can't be absolute URL.
+	 * @param forwardMethod HTTP method to use when accessing the forward URL
+	 */
+	public SpnegoEntryPoint(String forwardUrl, HttpMethod forwardMethod) {
+		if (StringUtils.hasText(forwardUrl)) {
+			Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), "Forward url specified must be a valid forward URL");
+			Assert.isTrue(!UrlUtils.isAbsoluteUrl(forwardUrl), "Forward url specified must not be absolute");
+
+			this.forwardUrl = forwardUrl;
+			this.forwardMethod = forwardMethod;
+			this.forward = true;
+		}
+		else {
+			this.forwardUrl = null;
+			this.forwardMethod = null;
+			this.forward = false;
+		}
+	}
+
+	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
+			throws IOException, ServletException {
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("Add header WWW-Authenticate:Negotiate to " + request.getRequestURL() + ", forward: "
+					+ (this.forward ? this.forwardUrl : "no"));
+		}
+		response.addHeader("WWW-Authenticate", "Negotiate");
+		response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+
+		if (this.forward) {
+			RequestDispatcher dispatcher = request.getRequestDispatcher(this.forwardUrl);
+			HttpServletRequest fwdRequest = (this.forwardMethod != null) ? new HttpServletRequestWrapper(request) {
+				@Override
+				public String getMethod() {
+					return SpnegoEntryPoint.this.forwardMethod.name();
+				}
+			} : request;
+			dispatcher.forward(fwdRequest, response);
+		}
+		else {
+			response.flushBuffer();
+		}
+	}
+
+}

+ 44 - 0
kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfig.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.docs;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
+
+//tag::snippetA[]
+@Configuration
+public class AuthProviderConfig {
+
+	@Bean
+	public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
+		KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
+		SunJaasKerberosClient client = new SunJaasKerberosClient();
+		client.setDebug(true);
+		provider.setKerberosClient(client);
+		provider.setUserDetailsService(dummyUserDetailsService());
+		return provider;
+	}
+
+	@Bean
+	public DummyUserDetailsService dummyUserDetailsService() {
+		return new DummyUserDetailsService();
+	}
+
+}
+// end::snippetA[]

+ 33 - 0
kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfigTests.java

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.docs;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(locations = { "AuthProviderConfig.xml" })
+public class AuthProviderConfigTests {
+
+	@Test
+	public void configLoads() {
+	}
+
+}

+ 34 - 0
kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/DummyUserDetailsService.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.docs;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+//tag::snippetA[]
+public class DummyUserDetailsService implements UserDetailsService {
+
+	@Override
+	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+		return new User(username, "notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER"));
+	}
+
+}
+// end::snippetA[]

+ 80 - 0
kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/SpnegoConfig.java

@@ -0,0 +1,80 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.docs;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
+import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
+import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
+
+//tag::snippetA[]
+@Configuration
+public class SpnegoConfig {
+
+	@Bean
+	public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
+		KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
+		SunJaasKerberosClient client = new SunJaasKerberosClient();
+		client.setDebug(true);
+		provider.setKerberosClient(client);
+		provider.setUserDetailsService(dummyUserDetailsService());
+		return provider;
+	}
+
+	@Bean
+	public SpnegoEntryPoint spnegoEntryPoint() {
+		return new SpnegoEntryPoint("/login");
+	}
+
+	@Bean
+	public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
+			AuthenticationManager authenticationManager) {
+		SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
+		filter.setAuthenticationManager(authenticationManager);
+		return filter;
+	}
+
+	@Bean
+	public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
+		KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
+		provider.setTicketValidator(sunJaasKerberosTicketValidator());
+		provider.setUserDetailsService(dummyUserDetailsService());
+		return provider;
+	}
+
+	@Bean
+	public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
+		SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
+		ticketValidator.setServicePrincipal("HTTP/servicehost.example.org@EXAMPLE.ORG");
+		ticketValidator.setKeyTabLocation(new FileSystemResource("/tmp/service.keytab"));
+		ticketValidator.setDebug(true);
+		return ticketValidator;
+	}
+
+	@Bean
+	public DummyUserDetailsService dummyUserDetailsService() {
+		return new DummyUserDetailsService();
+	}
+
+}
+// end::snippetA[]

+ 298 - 0
kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoAuthenticationProcessingFilterTests.java

@@ -0,0 +1,298 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.web;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
+import org.springframework.security.kerberos.authentication.KerberosTicketValidation;
+import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.security.web.context.SecurityContextRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Test class for {@link SpnegoAuthenticationProcessingFilter}
+ *
+ * @author Mike Wiesner
+ * @author Jeremy Stone
+ * @since 1.0
+ */
+public class SpnegoAuthenticationProcessingFilterTests {
+
+	private SpnegoAuthenticationProcessingFilter filter;
+
+	private AuthenticationManager authenticationManager;
+
+	private HttpServletRequest request;
+
+	private HttpServletResponse response;
+
+	private FilterChain chain;
+
+	private AuthenticationSuccessHandler successHandler;
+
+	private AuthenticationFailureHandler failureHandler;
+
+	private WebAuthenticationDetailsSource detailsSource;
+
+	// data
+	private static final byte[] TEST_TOKEN = "TestToken".getBytes();
+
+	private static final String TEST_TOKEN_BASE64 = "VGVzdFRva2Vu";
+
+	private static KerberosTicketValidation UNUSED_TICKET_VALIDATION = mock(KerberosTicketValidation.class);
+
+	private static final Authentication AUTHENTICATION = new KerberosServiceRequestToken("test",
+			UNUSED_TICKET_VALIDATION, AuthorityUtils.createAuthorityList("ROLE_ADMIN"), TEST_TOKEN);
+
+	private static final String HEADER = "Authorization";
+
+	private static final String TOKEN_PREFIX_NEG = "Negotiate ";
+
+	private static final String TOKEN_PREFIX_KERB = "Kerberos ";
+
+	private static final String TOKEN_NTLM = "Negotiate TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==";
+
+	private static final BadCredentialsException BCE = new BadCredentialsException("");
+
+	@BeforeEach
+	public void before() throws Exception {
+		// mocking
+		this.authenticationManager = mock(AuthenticationManager.class);
+		this.detailsSource = new WebAuthenticationDetailsSource();
+		this.filter = new SpnegoAuthenticationProcessingFilter();
+		this.filter.setAuthenticationManager(this.authenticationManager);
+		this.request = mock(HttpServletRequest.class);
+		this.response = mock(HttpServletResponse.class);
+		this.chain = mock(FilterChain.class);
+		this.filter.afterPropertiesSet();
+	}
+
+	@Test
+	public void testEverythingWorks() throws Exception {
+		everythingWorks(TOKEN_PREFIX_NEG);
+	}
+
+	@Test
+	public void testEverythingWorks_Kerberos() throws Exception {
+		everythingWorks(TOKEN_PREFIX_KERB);
+	}
+
+	@Test
+	public void testEverythingWorksWithHandlers() throws Exception {
+		everythingWorksWithHandlers(TOKEN_PREFIX_NEG);
+	}
+
+	@Test
+	public void testEverythingWorksWithHandlers_Kerberos() throws Exception {
+		everythingWorksWithHandlers(TOKEN_PREFIX_KERB);
+	}
+
+	private void everythingWorksWithHandlers(String tokenPrefix) throws Exception {
+		createHandler();
+		everythingWorks(tokenPrefix);
+		everythingWorksVerifyHandlers();
+	}
+
+	private void everythingWorksVerifyHandlers() throws Exception {
+		verify(this.successHandler).onAuthenticationSuccess(this.request, this.response, AUTHENTICATION);
+		verify(this.failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class),
+				any(HttpServletResponse.class), any(AuthenticationException.class));
+	}
+
+	private void everythingWorks(String tokenPrefix) throws IOException, ServletException {
+		// stubbing
+		SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
+		this.filter.setSecurityContextRepository(securityContextRepository);
+		everythingWorksStub(tokenPrefix);
+
+		// testing
+		this.filter.doFilter(this.request, this.response, this.chain);
+		verify(this.chain).doFilter(this.request, this.response);
+		verify(securityContextRepository).saveContext(SecurityContextHolder.getContext(), this.request, this.response);
+		assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(AUTHENTICATION);
+	}
+
+	@Test
+	public void testNoHeader() throws Exception {
+		this.filter.doFilter(this.request, this.response, this.chain);
+		// If the header is not present, the filter is not allowed to call
+		// authenticate()
+		verify(this.authenticationManager, never()).authenticate(any(Authentication.class));
+		// chain should go on
+		verify(this.chain).doFilter(this.request, this.response);
+		assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(null);
+	}
+
+	@Test
+	public void testNTLMSSPHeader() throws Exception {
+		given(this.request.getHeader(HEADER)).willReturn(TOKEN_NTLM);
+
+		this.filter.doFilter(this.request, this.response, this.chain);
+		// If the header is not present, the filter is not allowed to call
+		// authenticate()
+		verify(this.authenticationManager, never()).authenticate(any(Authentication.class));
+		// chain should go on
+		verify(this.chain).doFilter(this.request, this.response);
+		assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(null);
+	}
+
+	@Test
+	public void testAuthenticationFails() throws Exception {
+		authenticationFails();
+		verify(this.response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+	}
+
+	@Test
+	public void testAuthenticationFailsWithHandlers() throws Exception {
+		createHandler();
+		authenticationFails();
+		verify(this.failureHandler).onAuthenticationFailure(this.request, this.response, BCE);
+		verify(this.successHandler, never()).onAuthenticationSuccess(any(HttpServletRequest.class),
+				any(HttpServletResponse.class), any(Authentication.class));
+		verify(this.response, never()).setStatus(anyInt());
+	}
+
+	@Test
+	public void testAlreadyAuthenticated() throws Exception {
+		try {
+			Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike",
+					AuthorityUtils.createAuthorityList("ROLE_TEST"));
+			SecurityContextHolder.getContext().setAuthentication(existingAuth);
+			given(this.request.getHeader(HEADER)).willReturn(TOKEN_PREFIX_NEG + TEST_TOKEN_BASE64);
+			this.filter.doFilter(this.request, this.response, this.chain);
+			verify(this.authenticationManager, never()).authenticate(any(Authentication.class));
+		}
+		finally {
+			SecurityContextHolder.clearContext();
+		}
+	}
+
+	@Test
+	public void testAlreadyAuthenticatedWithNotAuthenticatedToken() throws Exception {
+		try {
+			// this token is not authenticated yet!
+			Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike");
+			SecurityContextHolder.getContext().setAuthentication(existingAuth);
+			everythingWorks(TOKEN_PREFIX_NEG);
+		}
+		finally {
+			SecurityContextHolder.clearContext();
+		}
+	}
+
+	@Test
+	public void testAlreadyAuthenticatedWithAnonymousToken() throws Exception {
+		try {
+			Authentication existingAuth = new AnonymousAuthenticationToken("test", "mike",
+					AuthorityUtils.createAuthorityList("ROLE_TEST"));
+			SecurityContextHolder.getContext().setAuthentication(existingAuth);
+			everythingWorks(TOKEN_PREFIX_NEG);
+		}
+		finally {
+			SecurityContextHolder.clearContext();
+		}
+	}
+
+	@Test
+	public void testAlreadyAuthenticatedNotActive() throws Exception {
+		try {
+			Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike",
+					AuthorityUtils.createAuthorityList("ROLE_TEST"));
+			SecurityContextHolder.getContext().setAuthentication(existingAuth);
+			this.filter.setSkipIfAlreadyAuthenticated(false);
+			everythingWorks(TOKEN_PREFIX_NEG);
+		}
+		finally {
+			SecurityContextHolder.clearContext();
+		}
+	}
+
+	@Test
+	public void testEverythingWorksWithHandlers_stopFilterChain() throws Exception {
+		this.filter.setStopFilterChainOnSuccessfulAuthentication(true);
+
+		createHandler();
+		everythingWorksStub(TOKEN_PREFIX_NEG);
+
+		// testing
+		this.filter.doFilter(this.request, this.response, this.chain);
+		verify(this.chain, never()).doFilter(this.request, this.response);
+		assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(AUTHENTICATION);
+		everythingWorksVerifyHandlers();
+	}
+
+	private void everythingWorksStub(String tokenPrefix) throws IOException, ServletException {
+		given(this.request.getHeader(HEADER)).willReturn(tokenPrefix + TEST_TOKEN_BASE64);
+		KerberosServiceRequestToken requestToken = new KerberosServiceRequestToken(TEST_TOKEN);
+		requestToken.setDetails(this.detailsSource.buildDetails(this.request));
+		given(this.authenticationManager.authenticate(requestToken)).willReturn(AUTHENTICATION);
+	}
+
+	private void authenticationFails() throws IOException, ServletException {
+		// stubbing
+		given(this.request.getHeader(HEADER)).willReturn(TOKEN_PREFIX_NEG + TEST_TOKEN_BASE64);
+		given(this.authenticationManager.authenticate(any(Authentication.class))).willThrow(BCE);
+
+		// testing
+		this.filter.doFilter(this.request, this.response, this.chain);
+		// chain should stop here and it should send back a 500
+		// future version should call some error handler
+		verify(this.chain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class));
+	}
+
+	private void createHandler() {
+		this.successHandler = mock(AuthenticationSuccessHandler.class);
+		this.failureHandler = mock(AuthenticationFailureHandler.class);
+		this.filter.setSuccessHandler(this.successHandler);
+		this.filter.setFailureHandler(this.failureHandler);
+	}
+
+	@AfterEach
+	public void after() {
+		SecurityContextHolder.clearContext();
+	}
+
+}

+ 121 - 0
kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoEntryPointTests.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.kerberos.web;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Test class for {@link SpnegoEntryPoint}
+ *
+ * @author Mike Wiesner
+ * @author Janne Valkealahti
+ * @author Andre Schaefer, Namics AG
+ * @since 1.0
+ */
+public class SpnegoEntryPointTests {
+
+	private SpnegoEntryPoint entryPoint = new SpnegoEntryPoint();
+
+	@Test
+	public void testEntryPointOk() throws Exception {
+		HttpServletRequest request = mock(HttpServletRequest.class);
+		HttpServletResponse response = mock(HttpServletResponse.class);
+
+		this.entryPoint.commence(request, response, null);
+
+		verify(response).addHeader("WWW-Authenticate", "Negotiate");
+		verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+	}
+
+	@Test
+	public void testEntryPointOkWithDispatcher() throws Exception {
+		SpnegoEntryPoint entryPoint = new SpnegoEntryPoint();
+		HttpServletResponse response = mock(HttpServletResponse.class);
+		HttpServletRequest request = mock(HttpServletRequest.class);
+		RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
+		given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
+		entryPoint.commence(request, response, null);
+		verify(response).addHeader("WWW-Authenticate", "Negotiate");
+		verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+	}
+
+	@Test
+	public void testEntryPointForwardOk() throws Exception {
+		String forwardUrl = "/login";
+		SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl);
+		HttpServletResponse response = mock(HttpServletResponse.class);
+		HttpServletRequest request = mock(HttpServletRequest.class);
+		RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
+		given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
+		entryPoint.commence(request, response, null);
+		verify(response).addHeader("WWW-Authenticate", "Negotiate");
+		verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+		verify(request).getRequestDispatcher(forwardUrl);
+		verify(requestDispatcher).forward(request, response);
+	}
+
+	@Test
+	public void testForwardUsesDefaultHttpMethod() throws Exception {
+		ArgumentCaptor<HttpServletRequest> servletRequestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class);
+		String forwardUrl = "/login";
+		SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl);
+		HttpServletResponse response = mock(HttpServletResponse.class);
+		HttpServletRequest request = mock(HttpServletRequest.class);
+		given(request.getMethod()).willReturn(RequestMethod.POST.name());
+		RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
+		given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
+		entryPoint.commence(request, response, null);
+		verify(requestDispatcher).forward(servletRequestCaptor.capture(), eq(response));
+		assertThat(servletRequestCaptor.getValue().getMethod()).isEqualTo(HttpMethod.POST.name());
+	}
+
+	@Test
+	public void testForwardUsesCustomHttpMethod() throws Exception {
+		ArgumentCaptor<HttpServletRequest> servletRequestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class);
+		String forwardUrl = "/login";
+		SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl, HttpMethod.DELETE);
+		HttpServletResponse response = mock(HttpServletResponse.class);
+		HttpServletRequest request = mock(HttpServletRequest.class);
+		RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
+		given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
+		entryPoint.commence(request, response, null);
+		verify(requestDispatcher).forward(servletRequestCaptor.capture(), eq(response));
+		assertThat(servletRequestCaptor.getValue().getMethod()).isEqualTo(HttpMethod.DELETE.name());
+	}
+
+	@Test
+	public void testEntryPointForwardAbsolute() throws Exception {
+		assertThatIllegalArgumentException().isThrownBy(() -> new SpnegoEntryPoint("http://test/login"));
+	}
+
+}

+ 47 - 0
kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/AuthProviderConfig.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- tag::snippetA[] -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns:sec="http://www.springframework.org/schema/security"
+  xmlns:context="http://www.springframework.org/schema/context"
+  xsi:schemaLocation="
+    http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.2.xsd
+    http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.2.xsd
+    http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
+
+  <sec:http entry-point-ref="spnegoEntryPoint" use-expressions="true">
+    <sec:intercept-url pattern="/" access="permitAll" />
+    <sec:intercept-url pattern="/home" access="permitAll" />
+    <sec:intercept-url pattern="/**" access="authenticated"/>
+  </sec:http>
+
+  <sec:authentication-manager alias="authenticationManager">
+    <sec:authentication-provider ref="kerberosAuthenticationProvider"/>
+  </sec:authentication-manager>
+
+  <bean id="kerberosAuthenticationProvider"
+    class="org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider">
+    <property name="kerberosClient">
+      <bean class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient">
+        <property name="debug" value="true"/>
+      </bean>
+    </property>
+    <property name="userDetailsService" ref="dummyUserDetailsService"/>
+  </bean>
+
+  <bean
+    class="org.springframework.security.kerberos.authentication.sun.GlobalSunJaasKerberosConfig">
+    <property name="debug" value="true" />
+    <property name="krbConfLocation" value="/path/to/krb5.ini"/>
+  </bean>
+
+  <bean id="dummyUserDetailsService"
+    class="org.springframework.security.kerberos.docs.DummyUserDetailsService" />
+
+  <bean id="spnegoEntryPoint"
+    class="org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint" >
+    <constructor-arg value="/login" />
+  </bean>
+
+</beans>
+<!-- end::snippetA[] -->

+ 63 - 0
kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/SpnegoConfig.xml

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- tag::snippetA[] -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns:sec="http://www.springframework.org/schema/security"
+  xmlns:context="http://www.springframework.org/schema/context"
+  xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
+    http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-4.1.xsd
+    http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.1.xsd">
+
+  <sec:http entry-point-ref="spnegoEntryPoint" use-expressions="true" >
+    <sec:intercept-url pattern="/" access="permitAll" />
+    <sec:intercept-url pattern="/home" access="permitAll" />
+    <sec:intercept-url pattern="/login" access="permitAll" />
+    <sec:intercept-url pattern="/**" access="authenticated"/>
+    <sec:form-login login-page="/login" />
+    <sec:custom-filter ref="spnegoAuthenticationProcessingFilter"
+      before="BASIC_AUTH_FILTER" />
+  </sec:http>
+
+  <sec:authentication-manager alias="authenticationManager">
+    <sec:authentication-provider ref="kerberosAuthenticationProvider" />
+    <sec:authentication-provider ref="kerberosServiceAuthenticationProvider" />
+  </sec:authentication-manager>
+
+  <bean id="kerberosAuthenticationProvider"
+    class="org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider">
+    <property name="userDetailsService" ref="dummyUserDetailsService"/>
+    <property name="kerberosClient">
+      <bean class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient">
+        <property name="debug" value="true"/>
+      </bean>
+    </property>
+  </bean>
+
+  <bean id="spnegoEntryPoint"
+    class="org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint" >
+    <constructor-arg value="/login" />
+  </bean>
+
+  <bean id="spnegoAuthenticationProcessingFilter"
+    class="org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter">
+    <property name="authenticationManager" ref="authenticationManager" />
+  </bean>
+
+  <bean id="kerberosServiceAuthenticationProvider"
+    class="org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider">
+    <property name="ticketValidator">
+      <bean
+        class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator">
+        <property name="servicePrincipal" value="${app.service-principal}" />
+        <property name="keyTabLocation" value="${app.keytab-location}" />
+        <property name="debug" value="true" />
+      </bean>
+    </property>
+    <property name="userDetailsService" ref="dummyUserDetailsService" />
+  </bean>
+
+  <bean id="dummyUserDetailsService"
+    class="org.springframework.security.kerberos.docs.DummyUserDetailsService" />
+
+</beans>
+<!-- end::snippetA[] -->

+ 12 - 0
kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/appproperties.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
+		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.1.xsd
+		http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-4.1.xsd">
+
+	<context:property-placeholder location="app.properties"/>
+
+</beans>