浏览代码

Add Spring Security Kerberos

Move the Spring Security Kerberos Extension into Spring Security

Closes gh-17879
Rob Winch 1 周之前
父节点
当前提交
f5fb127c8c
共有 69 个文件被更改,包括 6173 次插入0 次删除
  1. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png
  2. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png
  3. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png
  4. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png
  5. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png
  6. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png
  7. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png
  8. 二进制
      docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png
  9. 二进制
      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

二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png


二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png


二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png


二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png


二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png


二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png


二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png


二进制
docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png


二进制
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/runas.adoc[Run-As]
 *** xref:servlet/authentication/logout.adoc[Logout]
 *** xref:servlet/authentication/logout.adoc[Logout]
 *** xref:servlet/authentication/events.adoc[Authentication Events]
 *** 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/index.adoc[Authorization]
 *** xref:servlet/authorization/architecture.adoc[Authorization Architecture]
 *** xref:servlet/authorization/architecture.adoc[Authorization Architecture]
 *** xref:servlet/authorization/authorize-http-requests.adoc[Authorize HTTP Requests]
 *** 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.
 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
 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
 == Core
 
 
 * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
 * 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>