From f5fb127c8ce00d3a956967db44b7d0be1b9e7222 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:23:19 -0500 Subject: [PATCH] Add Spring Security Kerberos Move the Spring Security Kerberos Extension into Spring Security Closes gh-17879 --- .../kerberos/drawio-kerb-cc1.png | Bin 0 -> 19416 bytes .../kerberos/drawio-kerb-cc2.png | Bin 0 -> 24648 bytes .../kerberos/drawio-kerb-cc3.png | Bin 0 -> 24692 bytes .../kerberos/drawio-kerb-cc4.png | Bin 0 -> 22500 bytes .../servlet/authentication/kerberos/ff1.png | Bin 0 -> 35400 bytes .../servlet/authentication/kerberos/ff2.png | Bin 0 -> 36285 bytes .../servlet/authentication/kerberos/ff3.png | Bin 0 -> 32884 bytes .../servlet/authentication/kerberos/ie1.png | Bin 0 -> 15576 bytes .../servlet/authentication/kerberos/ie2.png | Bin 0 -> 19243 bytes .../examples/kerberos/AuthProviderConfig.java | 118 +++++ .../kerberos/AuthProviderConfigTest.java | 27 + .../kerberos/DummyUserDetailsService.java | 35 ++ .../KerberosLdapContextSourceConfig.java | 67 +++ .../kerberos/KerberosRestTemplateConfig.java | 38 ++ .../ROOT/examples/kerberos/SpnegoConfig.java | 151 ++++++ docs/modules/ROOT/nav.adoc | 5 + .../authentication/kerberos/appendix.adoc | 473 ++++++++++++++++++ .../authentication/kerberos/index.adoc | 3 + .../authentication/kerberos/introduction.adoc | 5 + .../authentication/kerberos/samples.adoc | 225 +++++++++ .../servlet/authentication/kerberos/ssk.adoc | 85 ++++ docs/modules/ROOT/pages/whats-new.adoc | 4 + .../spring-security-kerberos-client.gradle | 23 + .../kerberos/client/KerberosRestTemplate.java | 355 +++++++++++++ .../client/config/SunJaasKrb5LoginConfig.java | 122 +++++ .../ldap/KerberosLdapContextSource.java | 156 ++++++ .../client/KerberosRestTemplateTests.java | 135 +++++ .../src/test/resources/log4j.properties | 10 + .../src/test/resources/minikdc-krb5.conf | 26 + .../src/test/resources/minikdc.ldiff | 86 ++++ .../spring-security-kerberos-core.gradle | 15 + .../authentication/JaasSubjectHolder.java | 72 +++ .../KerberosAuthentication.java | 23 + .../KerberosAuthenticationProvider.java | 72 +++ .../authentication/KerberosClient.java | 29 ++ .../authentication/KerberosMultiTier.java | 132 +++++ ...KerberosServiceAuthenticationProvider.java | 122 +++++ .../KerberosServiceRequestToken.java | 233 +++++++++ .../KerberosTicketValidation.java | 92 ++++ .../KerberosTicketValidator.java | 40 ++ ...osUsernamePasswordAuthenticationToken.java | 69 +++ .../sun/GlobalSunJaasKerberosConfig.java | 78 +++ .../kerberos/authentication/sun/JaasUtil.java | 47 ++ .../sun/SunJaasKerberosClient.java | 153 ++++++ .../sun/SunJaasKerberosTicketValidator.java | 332 ++++++++++++ .../KerberosAuthenticationProviderTests.java | 92 ++++ ...rosServiceAuthenticationProviderTests.java | 173 +++++++ .../KerberosTicketValidationTests.java | 68 +++ .../SunJaasKerberosTicketValidatorTests.java | 91 ++++ .../spring-security-kerberos-test.gradle | 16 + .../test/KerberosSecurityTestcase.java | 88 ++++ .../security/kerberos/test/MiniKdc.java | 429 ++++++++++++++++ .../security/kerberos/test/TestMiniKdc.java | 192 +++++++ .../src/test/resources/log4j.properties | 10 + .../src/test/resources/minikdc-krb5.conf | 25 + .../src/test/resources/minikdc.ldiff | 47 ++ .../spring-security-kerberos-web.gradle | 19 + ...gKerberosAuthenticationSuccessHandler.java | 71 +++ .../SpnegoAuthenticationProcessingFilter.java | 320 ++++++++++++ .../web/authentication/SpnegoEntryPoint.java | 142 ++++++ .../kerberos/docs/AuthProviderConfig.java | 44 ++ .../docs/AuthProviderConfigTests.java | 33 ++ .../docs/DummyUserDetailsService.java | 34 ++ .../security/kerberos/docs/SpnegoConfig.java | 80 +++ ...goAuthenticationProcessingFilterTests.java | 298 +++++++++++ .../kerberos/web/SpnegoEntryPointTests.java | 121 +++++ .../kerberos/docs/AuthProviderConfig.xml | 47 ++ .../security/kerberos/docs/SpnegoConfig.xml | 63 +++ .../security/kerberos/docs/appproperties.xml | 12 + 69 files changed, 6173 insertions(+) create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie2.png create mode 100644 docs/modules/ROOT/examples/kerberos/AuthProviderConfig.java create mode 100644 docs/modules/ROOT/examples/kerberos/AuthProviderConfigTest.java create mode 100644 docs/modules/ROOT/examples/kerberos/DummyUserDetailsService.java create mode 100644 docs/modules/ROOT/examples/kerberos/KerberosLdapContextSourceConfig.java create mode 100644 docs/modules/ROOT/examples/kerberos/KerberosRestTemplateConfig.java create mode 100644 docs/modules/ROOT/examples/kerberos/SpnegoConfig.java create mode 100644 docs/modules/ROOT/pages/servlet/authentication/kerberos/appendix.adoc create mode 100644 docs/modules/ROOT/pages/servlet/authentication/kerberos/index.adoc create mode 100644 docs/modules/ROOT/pages/servlet/authentication/kerberos/introduction.adoc create mode 100644 docs/modules/ROOT/pages/servlet/authentication/kerberos/samples.adoc create mode 100644 docs/modules/ROOT/pages/servlet/authentication/kerberos/ssk.adoc create mode 100644 kerberos/kerberos-client/spring-security-kerberos-client.gradle create mode 100644 kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/KerberosRestTemplate.java create mode 100644 kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/config/SunJaasKrb5LoginConfig.java create mode 100644 kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/ldap/KerberosLdapContextSource.java create mode 100644 kerberos/kerberos-client/src/test/java/org/springframework/security/kerberos/client/KerberosRestTemplateTests.java create mode 100644 kerberos/kerberos-client/src/test/resources/log4j.properties create mode 100644 kerberos/kerberos-client/src/test/resources/minikdc-krb5.conf create mode 100644 kerberos/kerberos-client/src/test/resources/minikdc.ldiff create mode 100644 kerberos/kerberos-core/spring-security-kerberos-core.gradle create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/JaasSubjectHolder.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthentication.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProvider.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosClient.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosMultiTier.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProvider.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceRequestToken.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidation.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidator.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosUsernamePasswordAuthenticationToken.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/GlobalSunJaasKerberosConfig.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/JaasUtil.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosClient.java create mode 100644 kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidator.java create mode 100644 kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProviderTests.java create mode 100644 kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProviderTests.java create mode 100644 kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosTicketValidationTests.java create mode 100644 kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidatorTests.java create mode 100644 kerberos/kerberos-test/spring-security-kerberos-test.gradle create mode 100644 kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/KerberosSecurityTestcase.java create mode 100644 kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/MiniKdc.java create mode 100644 kerberos/kerberos-test/src/test/java/org/springframework/security/kerberos/test/TestMiniKdc.java create mode 100644 kerberos/kerberos-test/src/test/resources/log4j.properties create mode 100644 kerberos/kerberos-test/src/test/resources/minikdc-krb5.conf create mode 100644 kerberos/kerberos-test/src/test/resources/minikdc.ldiff create mode 100644 kerberos/kerberos-web/spring-security-kerberos-web.gradle create mode 100644 kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java create mode 100644 kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoAuthenticationProcessingFilter.java create mode 100644 kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoEntryPoint.java create mode 100644 kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfig.java create mode 100644 kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfigTests.java create mode 100644 kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/DummyUserDetailsService.java create mode 100644 kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/SpnegoConfig.java create mode 100644 kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoAuthenticationProcessingFilterTests.java create mode 100644 kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoEntryPointTests.java create mode 100644 kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/AuthProviderConfig.xml create mode 100644 kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/SpnegoConfig.xml create mode 100644 kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/appproperties.xml diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png new file mode 100644 index 0000000000000000000000000000000000000000..ff95c9b73fd6c433f1ede4de3d5b55d8c1d2eb0d GIT binary patch literal 19416 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png new file mode 100644 index 0000000000000000000000000000000000000000..8eb235a0e9a2e8bf5f4d88ece3e7dde28e0b9441 GIT binary patch literal 24648 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png new file mode 100644 index 0000000000000000000000000000000000000000..5be17ffc9b6e09271dc9129fd89dfeecf3054a81 GIT binary patch literal 24692 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png new file mode 100644 index 0000000000000000000000000000000000000000..850845f89ee6d36f27b5fc1255c898b530af53a6 GIT binary patch literal 22500 zcmdSBWmH>T^fwsXTA;X7ptuz)4uvAc-QC??La^e};!caZyGwB^t_ey8yr7&-B^_tl z`-=ODGyFn96cg9bMP>X+4K;z2IKT3GnXCWhTEak)<8?bLh_5cunV*Oqb&QCoE`j$U zNjX#*=e=+{l9(>xIf>Dr@R&i2{6pFA1&%C*W#hZ7k2Qhj{aTE74$ zuCpxi(dd|RN`q>gbH+4Z9smQT4~_5wd`cqNe4nHHCadJ6htrcawOd?RC(@Jm@Xxzi zEDSdn>6>Q>c_%NUyom=_eP%@1-LKFo^_v_{=UfmTeoG==z6cb`l8U-qu(vN%o&5E6 zPg1bqy1|?rKEhoq=dH#>Rga}IzX(e#Zgc3Es_OJQWsRJD2z?y76tNZ67}uWI0cL`Z zoBo?o1F-8$;>uI%r^*f+YR;Fxg`119{^7KZNbFX^B!us7h6^}C{22ou7zKpB1~h(M=Q5G-y@s(w{n_&tc7={N+s8s4xljR9+DjwICx}t;ct)nk!ql=*uKjaU-i+~x zb|c?zW?YGy-%6SYuw!9}*Y4>{bk<-y6zs)hTmFWatG?Om-kY$x)2l=GI5C;?$*}nR zM5v~y&$?{V+vnzhjgr0JTTlrCT@@}BZrF;(7TdTa949NcwT0aB2Iw&d{qA}!W|TZO zsJ^M==i-u}&A;EK^r;x}zU_E!UeU#%6l7*D9~*2pzdZfIr;?rPIr~JnJ;8q`#@e{> zn=Sj=CN(zIq$9id^wQt2`twexo0k%0A$3w->HbNB3eykV$wsg2mBgrGXaba7JqVkd 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3f250ad4b80da2682452ea1495ef198fed8e68 GIT binary patch literal 36285 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 QPiFuEPgg&ebxsLQ0DH~HqW}N^ literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png new file mode 100644 index 0000000000000000000000000000000000000000..696cd9fca7b2528147035e05f01f319b52492184 GIT binary patch literal 32884 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png new file mode 100644 index 0000000000000000000000000000000000000000..1fddc2ed874baf62079505c44bf92213eab25990 GIT binary patch literal 15576 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%0ADUd1 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 da8{ 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[] diff --git a/docs/modules/ROOT/examples/kerberos/AuthProviderConfigTest.java b/docs/modules/ROOT/examples/kerberos/AuthProviderConfigTest.java new file mode 100644 index 0000000000..26befa3b6f --- /dev/null +++ b/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() {} +} diff --git a/docs/modules/ROOT/examples/kerberos/DummyUserDetailsService.java b/docs/modules/ROOT/examples/kerberos/DummyUserDetailsService.java new file mode 100644 index 0000000000..e92ff90702 --- /dev/null +++ b/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[] diff --git a/docs/modules/ROOT/examples/kerberos/KerberosLdapContextSourceConfig.java b/docs/modules/ROOT/examples/kerberos/KerberosLdapContextSourceConfig.java new file mode 100644 index 0000000000..2dd97f9980 --- /dev/null +++ b/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[] + +} diff --git a/docs/modules/ROOT/examples/kerberos/KerberosRestTemplateConfig.java b/docs/modules/ROOT/examples/kerberos/KerberosRestTemplateConfig.java new file mode 100644 index 0000000000..ddff55fbf7 --- /dev/null +++ b/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[] + +} diff --git a/docs/modules/ROOT/examples/kerberos/SpnegoConfig.java b/docs/modules/ROOT/examples/kerberos/SpnegoConfig.java new file mode 100644 index 0000000000..4ac1f87f55 --- /dev/null +++ b/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[] diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index f5b1816e3d..3d454a048a 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -62,6 +62,11 @@ *** xref:servlet/authentication/runas.adoc[Run-As] *** xref:servlet/authentication/logout.adoc[Logout] *** xref:servlet/authentication/events.adoc[Authentication Events] +** xref:servlet/authentication/kerberos/index.adoc[Kerberos] +*** xref:servlet/authentication/kerberos/introduction.adoc[Introduction] +*** xref:servlet/authentication/kerberos/ssk.adoc[Reference] +*** xref:servlet/authentication/kerberos/samples.adoc[Samples] +*** xref:servlet/authentication/kerberos/appendix.adoc[Appendices] ** xref:servlet/authorization/index.adoc[Authorization] *** xref:servlet/authorization/architecture.adoc[Authorization Architecture] *** xref:servlet/authorization/authorize-http-requests.adoc[Authorize HTTP Requests] diff --git a/docs/modules/ROOT/pages/servlet/authentication/kerberos/appendix.adoc b/docs/modules/ROOT/pages/servlet/authentication/kerberos/appendix.adoc new file mode 100644 index 0000000000..42d32c69f7 --- /dev/null +++ b/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. + diff --git a/docs/modules/ROOT/pages/servlet/authentication/kerberos/index.adoc b/docs/modules/ROOT/pages/servlet/authentication/kerberos/index.adoc new file mode 100644 index 0000000000..c07b0dff36 --- /dev/null +++ b/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. diff --git a/docs/modules/ROOT/pages/servlet/authentication/kerberos/introduction.adoc b/docs/modules/ROOT/pages/servlet/authentication/kerberos/introduction.adoc new file mode 100644 index 0000000000..668f61d95b --- /dev/null +++ b/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}. diff --git a/docs/modules/ROOT/pages/servlet/authentication/kerberos/samples.adoc b/docs/modules/ROOT/pages/servlet/authentication/kerberos/samples.adoc new file mode 100644 index 0000000000..910658b907 --- /dev/null +++ b/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. +==== + +<> sample for Windows environment + +<> sample using server side authenticator + +<> sample using ticket validation +with spnego and form + +<> 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 +<>. + +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] +---- + + + Spring Security Kerberos Example + + +

Hello user1@EXAMPLE.ORG!

+ + +---- + +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] +---- + + + Spring Security Kerberos Example + + +

Hello user2@EXAMPLE.ORG!

+ + +---- + diff --git a/docs/modules/ROOT/pages/servlet/authentication/kerberos/ssk.adoc b/docs/modules/ROOT/pages/servlet/authentication/kerberos/ssk.adoc new file mode 100644 index 0000000000..67340f3d4a --- /dev/null +++ b/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. + +<> describes the authentication provider support. + +<> describes the spnego negotiate support. + +<> 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. +==== diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 48c78165b7..be7c6daddf 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -9,6 +9,10 @@ Below are the highlights of the release, or you can view https://github.com/spri Being a major release, there are a number of deprecated APIs that are removed in Spring Security 7. Each section that follows will indicate the more notable removals as well as the new features in that module +== Modules + +* The https://github.com/spring-projects/spring-security-kerberos[Spring Security Kerberos Extension] is now part of Spring Security. See the xref:servlet/authentication/kerberos/index.adoc[Kerberos] section of the reference for details. + == Core * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize` diff --git a/kerberos/kerberos-client/spring-security-kerberos-client.gradle b/kerberos/kerberos-client/spring-security-kerberos-client.gradle new file mode 100644 index 0000000000..76260ed289 --- /dev/null +++ b/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' +} + diff --git a/kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/KerberosRestTemplate.java b/kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/KerberosRestTemplate.java new file mode 100644 index 0000000000..23bfe1b834 --- /dev/null +++ b/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. + * + *

+ * Generally this template can be configured in few different ways. + *

    + *
  • Leave keyTabLocation and userPrincipal empty if you want to use cached ticket
  • + *
  • Use keyTabLocation and userPrincipal if you want to use keytab file
  • + *
  • Use userPrincipal and password if you want to use user/password
  • + *
  • Use loginOptions if you want to customise Krb5LoginModule options
  • + *
  • Use a customised httpClient
  • + *
+ * + * @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 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 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 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 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 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 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 authSchemeRegistry = RegistryBuilder.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 princ = new HashSet(1); + if (this.userPrincipal != null) { + princ.add(new KerberosPrincipal(this.userPrincipal)); + } + Subject sub = new Subject(false, princ, new HashSet(), new HashSet()); + CallbackHandler callbackHandler = new CallbackHandlerImpl(this.userPrincipal, this.password); + LoginContext lc = new LoginContext("", sub, callbackHandler, loginConfig); + return lc; + } + + @Override + protected final T doExecute(final URI url, final String uriTemplate, final HttpMethod method, + final RequestCallback requestCallback, final ResponseExtractor responseExtractor) + throws RestClientException { + + try { + LoginContext lc = buildLoginContext(); + lc.login(); + Subject serviceSubject = lc.getSubject(); + return Subject.doAs(serviceSubject, new PrivilegedAction() { + + @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 doExecuteSubject(URI url, String uriTemplate, HttpMethod method, RequestCallback requestCallback, + ResponseExtractor 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 loginOptions; + + private ClientLoginConfig(String keyTabLocation, String userPrincipal, String password, + Map loginOptions) { + super(); + this.keyTabLocation = keyTabLocation; + this.userPrincipal = userPrincipal; + this.password = password; + this.loginOptions = loginOptions; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + + Map options = new HashMap(); + + // 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"); + } + } + } + + } + +} diff --git a/kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/config/SunJaasKrb5LoginConfig.java b/kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/config/SunJaasKrb5LoginConfig.java new file mode 100644 index 0000000000..92b3daca03 --- /dev/null +++ b/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 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), }; + } + +} diff --git a/kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/ldap/KerberosLdapContextSource.java b/kerberos/kerberos-client/src/main/java/org/springframework/security/kerberos/client/ldap/KerberosLdapContextSource.java new file mode 100644 index 0000000000..1ec332a442 --- /dev/null +++ b/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: + * + *
+ *  <bean id="authorizationContextSource" class="org.springframework.security.kerberos.ldap.KerberosLdapContextSource">
+ *      <constructor-arg value="${authentication.ldap.ldapUrl}" />
+ *      <property name="referral" value="ignore" />
+ *
+ *       <property name="loginConfig">
+ *           <bean class="org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig">
+ *               <property name="servicePrincipal" value="${authentication.ldap.servicePrincipal}" />
+ *               <property name="useTicketCache" value="true" />
+ *               <property name="isInitiator" value="true" />
+ *               <property name="debug" value="false" />
+ *           </bean>
+ *       </property>
+ *   </bean>
+ *
+ *   <sec:ldap-user-service id="ldapUserService" server-ref="authorizationContextSource" user-search-filter="(| (userPrincipalName={0}) (sAMAccountName={0}))"
+ *       group-search-filter="(member={0})" group-role-attribute="cn" role-prefix="none" />
+ * 
+ * + * @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 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; + } + } + +} diff --git a/kerberos/kerberos-client/src/test/java/org/springframework/security/kerberos/client/KerberosRestTemplateTests.java b/kerberos/kerberos-client/src/test/java/org/springframework/security/kerberos/client/KerberosRestTemplateTests.java new file mode 100644 index 0000000000..852c9b6083 --- /dev/null +++ b/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()); + } + + } + + } + +} diff --git a/kerberos/kerberos-client/src/test/resources/log4j.properties b/kerberos/kerberos-client/src/test/resources/log4j.properties new file mode 100644 index 0000000000..42ac2a2825 --- /dev/null +++ b/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 + diff --git a/kerberos/kerberos-client/src/test/resources/minikdc-krb5.conf b/kerberos/kerberos-client/src/test/resources/minikdc-krb5.conf new file mode 100644 index 0000000000..004cc3db51 --- /dev/null +++ b/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} + '}' \ No newline at end of file diff --git a/kerberos/kerberos-client/src/test/resources/minikdc.ldiff b/kerberos/kerberos-client/src/test/resources/minikdc.ldiff new file mode 100644 index 0000000000..4ff14d76e1 --- /dev/null +++ b/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 diff --git a/kerberos/kerberos-core/spring-security-kerberos-core.gradle b/kerberos/kerberos-core/spring-security-kerberos-core.gradle new file mode 100644 index 0000000000..a83deb55bb --- /dev/null +++ b/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' +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/JaasSubjectHolder.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/JaasSubjectHolder.java new file mode 100644 index 0000000000..173b6b0944 --- /dev/null +++ b/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; + +/** + *

+ * 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. + *

+ * + * @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 savedTokens = new HashMap(); + + 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); + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthentication.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthentication.java new file mode 100644 index 0000000000..68e4897073 --- /dev/null +++ b/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(); + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProvider.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProvider.java new file mode 100644 index 0000000000..35b907286e --- /dev/null +++ b/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 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; + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosClient.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosClient.java new file mode 100644 index 0000000000..ad8272cf84 --- /dev/null +++ b/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); + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosMultiTier.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosMultiTier.java new file mode 100644 index 0000000000..8f3ba89d47 --- /dev/null +++ b/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; + +/** + *

+ * Allows creating tickets against other service principals storing the tickets in the + * KerberosAuthentication's JaasSubjectHolder. + *

+ * + * @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() { + @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() { + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProvider.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProvider.java new file mode 100644 index 0000000000..229f23ba65 --- /dev/null +++ b/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; + +/** + *

+ * Authentication Provider which validates Kerberos Service Tickets or SPNEGO Tokens + * (which includes Kerberos Service Tickets). + *

+ * + *

+ * It needs a KerberosTicketValidator, which contains the code to validate + * the ticket, as this code is different between SUN and IBM JRE.
+ * It also needs an UserDetailsService to load the user properties and the + * GrantedAuthorities, as we only get back the username from Kerbeos + *

+ * + * You can see an example configuration in + * SpnegoAuthenticationProcessingFilter. + * + * @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 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 UserDetailsService to use, for loading the user properties and the + * GrantedAuthorities. + * @param userDetailsService the new user details service + */ + public void setUserDetailsService(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + /** + * The KerberosTicketValidator 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 + * UserDetails 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 BadCredentialsException, an + * AuthenticationServiceException) + */ + protected void additionalAuthenticationChecks(UserDetails userDetails, KerberosServiceRequestToken authentication) + throws AuthenticationException { + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceRequestToken.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosServiceRequestToken.java new file mode 100644 index 0000000000..b890e23736 --- /dev/null +++ b/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; + +/** + *

+ * Holds the Kerberos/SPNEGO token for requesting a kerberized service and is also the + * output of KerberosServiceAuthenticationProvider. + *

+ *

+ * Will mostly be created in SpnegoAuthenticationProcessingFilter and + * authenticated in KerberosServiceAuthenticationProvider. + *

+ * + * 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 UserDetails) + * @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 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 + * KerberosServiceAuthenticationProvider. + * @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() { + 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() { + 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; + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidation.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidation.java new file mode 100644 index 0000000000..63b650e65a --- /dev/null +++ b/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 princs = new HashSet(); + princs.add(new KerberosPrincipal(servicePrincipal)); + + this.username = username; + this.subject = new Subject(false, princs, new HashSet(), new HashSet()); + 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; + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidator.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosTicketValidator.java new file mode 100644 index 0000000000..d9a9f6ad68 --- /dev/null +++ b/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; + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosUsernamePasswordAuthenticationToken.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/KerberosUsernamePasswordAuthenticationToken.java new file mode 100644 index 0000000000..d8b102cd2f --- /dev/null +++ b/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; + +/** + *

+ * Holds the Username/Password as well as the JAAS Subject allowing multi-tier + * authentications using Kerberos. + *

+ * + *

+ * The JAAS Subject has in its private credentials the Kerberos tickets for generating new + * tickets against other service principals using + * KerberosMultiTier.authenticateService() + *

+ * + * @author Bogdan Mustiata + * @see KerberosAuthenticationProvider + * @see KerberosMultiTier + */ +public class KerberosUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken + implements KerberosAuthentication { + + private static final long serialVersionUID = 6327699460703504153L; + + private final JaasSubjectHolder jaasSubjectHolder; + + /** + *

+ * 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. + *

+ * @param principal + * @param credentials + * @param authorities + * @param subjectHolder + */ + public KerberosUsernamePasswordAuthenticationToken(Object principal, Object credentials, + Collection authorities, JaasSubjectHolder subjectHolder) { + super(principal, credentials, authorities); + this.jaasSubjectHolder = subjectHolder; + } + + @Override + public JaasSubjectHolder getJaasSubjectHolder() { + return this.jaasSubjectHolder; + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/GlobalSunJaasKerberosConfig.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/GlobalSunJaasKerberosConfig.java new file mode 100644 index 0000000000..4a3f0543c2 --- /dev/null +++ b/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; + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/JaasUtil.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/JaasUtil.java new file mode 100644 index 0000000000..d859e2db78 --- /dev/null +++ b/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(subject.getPrincipals()), + new HashSet(subject.getPublicCredentials()), + new HashSet(subject.getPrivateCredentials())); + + return subjectCopy; + } + + private JaasUtil() { + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosClient.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosClient.java new file mode 100644 index 0000000000..3a0de5456b --- /dev/null +++ b/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 options = new HashMap(); + 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"); + } + } + + } + + } + +} diff --git a/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidator.java b/kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidator.java new file mode 100644 index 0000000000..ada0506639 --- /dev/null +++ b/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 princ = new HashSet(1); + princ.add(new KerberosPrincipal(this.servicePrincipal)); + Subject sub = new Subject(false, princ, new HashSet(), new HashSet()); + LoginContext lc = new LoginContext("", sub, null, loginConfig); + lc.login(); + this.serviceSubject = lc.getSubject(); + } + + /** + * The service principal of the application. For web apps this is + * HTTP/full-qualified-domain-name@DOMAIN. 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 DOMAIN + * @param realmName + */ + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + /** + * @param multiTier + */ + public void setMultiTier(boolean multiTier) { + this.multiTier = multiTier; + } + + /** + *

+ * The location of the keytab. You can use the normale Spring Resource prefixes like + * file: or classpath:, but as the file is later on read by + * JAAS, we cannot guarantee that classpath works in every environment, + * esp. not in Java EE application servers. You should use file: there. + * + * This file also needs special protection, which is another reason to not include it + * in the classpath but rather use file:/etc/http.keytab 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). + *

+ * 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 { + + 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 { + + 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 options = new HashMap(); + 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), }; + } + + } + +} diff --git a/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProviderTests.java b/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosAuthenticationProviderTests.java new file mode 100644 index 0000000000..23e03c240f --- /dev/null +++ b/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 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); + + } + +} diff --git a/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProviderTests.java b/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosServiceAuthenticationProviderTests.java new file mode 100644 index 0000000000..00ad6d56f7 --- /dev/null +++ b/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 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); + } + +} diff --git a/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosTicketValidationTests.java b/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/KerberosTicketValidationTests.java new file mode 100644 index 0000000000..265dae591d --- /dev/null +++ b/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); + } + +} diff --git a/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidatorTests.java b/kerberos/kerberos-core/src/test/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidatorTests.java new file mode 100644 index 0000000000..6893df4834 --- /dev/null +++ b/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 = "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" + "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"); + } + +} diff --git a/kerberos/kerberos-test/spring-security-kerberos-test.gradle b/kerberos/kerberos-test/spring-security-kerberos-test.gradle new file mode 100644 index 0000000000..d623fb8029 --- /dev/null +++ b/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' +} diff --git a/kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/KerberosSecurityTestcase.java b/kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/KerberosSecurityTestcase.java new file mode 100644 index 0000000000..ffa9160278 --- /dev/null +++ b/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). + *

+ * 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; + } + +} diff --git a/kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/MiniKdc.java b/kerberos/kerberos-test/src/main/java/org/springframework/security/kerberos/test/MiniKdc.java new file mode 100644 index 0000000000..924abd3b21 --- /dev/null +++ b/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. + *

+ * From within testcases: + *

+ * MiniKdc sets one System property when started and un-set when stopped: + *

    + *
  • sun.security.krb5.debug: set to the debug value provided in the configuration
  • + *
+ * 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. + *

+ * MiniKdc default configuration values are: + *

    + *
  • org.name=EXAMPLE (used to create the REALM)
  • + *
  • org.domain=COM (used to create the REALM)
  • + *
  • kdc.bind.address=localhost
  • + *
  • kdc.port=0 (ephemeral port)
  • + *
  • instance=DefaultKrbServer
  • + *
  • max.ticket.lifetime=86400000 (1 day)
  • + *
  • max.renewable.lifetime=604800000 (7 days)
  • + *
  • transport=TCP
  • + *
  • debug=false
  • + *
+ * 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: " + " []+"); + 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 or kill 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 PROPERTIES = new HashSet(); + + 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. + *

+ * 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 missingProperties = new HashSet(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; + } + +} diff --git a/kerberos/kerberos-test/src/test/java/org/springframework/security/kerberos/test/TestMiniKdc.java b/kerberos/kerberos-test/src/test/java/org/springframework/security/kerberos/test/TestMiniKdc.java new file mode 100644 index 0000000000..b6e8d19d60 --- /dev/null +++ b/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 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 principals = new HashSet(); + principals.add(new KerberosPrincipal(principal)); + + // client login + Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); + 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(), new HashSet()); + 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 principalNameList = Keytab.loadKeytab(new File(workDir, "keytab")).getPrincipals(); + + Set principals = new HashSet(); + 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 options = new HashMap(); + 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) }; + } + + } + +} diff --git a/kerberos/kerberos-test/src/test/resources/log4j.properties b/kerberos/kerberos-test/src/test/resources/log4j.properties new file mode 100644 index 0000000000..42ac2a2825 --- /dev/null +++ b/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 + diff --git a/kerberos/kerberos-test/src/test/resources/minikdc-krb5.conf b/kerberos/kerberos-test/src/test/resources/minikdc-krb5.conf new file mode 100644 index 0000000000..849d7046b2 --- /dev/null +++ b/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} + '}' \ No newline at end of file diff --git a/kerberos/kerberos-test/src/test/resources/minikdc.ldiff b/kerberos/kerberos-test/src/test/resources/minikdc.ldiff new file mode 100644 index 0000000000..ad66661ad1 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/kerberos/kerberos-web/spring-security-kerberos-web.gradle b/kerberos/kerberos-web/spring-security-kerberos-web.gradle new file mode 100644 index 0000000000..a049532d79 --- /dev/null +++ b/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' +} diff --git a/kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java b/kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java new file mode 100644 index 0000000000..637fcd1e42 --- /dev/null +++ b/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()); + } + } + +} diff --git a/kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoAuthenticationProcessingFilter.java b/kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoAuthenticationProcessingFilter.java new file mode 100644 index 0000000000..018c0722ad --- /dev/null +++ b/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}. + * + *

+ * A typical Spring Security configuration might look like this: + *

+ * + *
+ * <beans xmlns="https://www.springframework.org/schema/beans"
+ * xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:sec="https://www.springframework.org/schema/security"
+ * xsi:schemaLocation="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">
+ *
+ * <sec:http entry-point-ref="spnegoEntryPoint">
+ * 	<sec:intercept-url pattern="/secure/**" access="IS_AUTHENTICATED_FULLY" />
+ * 	<sec:custom-filter ref="spnegoAuthenticationProcessingFilter" position="BASIC_AUTH_FILTER" />
+ * </sec:http>
+ *
+ * <bean id="spnegoEntryPoint" class="org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint" />
+ *
+ * <bean id="spnegoAuthenticationProcessingFilter"
+ * 	class="org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter">
+ * 	<property name="authenticationManager" ref="authenticationManager" />
+ * </bean>
+ *
+ * <sec:authentication-manager alias="authenticationManager">
+ * 	<sec:authentication-provider ref="kerberosServiceAuthenticationProvider" />
+ * </sec:authentication-manager>
+ *
+ * <bean id="kerberosServiceAuthenticationProvider"
+ * 	class="org.springframework.security.kerberos.authenitcation.KerberosServiceAuthenticationProvider">
+ * 	<property name="ticketValidator">
+ * 		<bean class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator">
+ * 			<property name="servicePrincipal" value="HTTP/web.springsource.com" />
+ * 			<property name="keyTabLocation" value="classpath:http-java.keytab" />
+ * 		</bean>
+ * 	</property>
+ * 	<property name="userDetailsService" ref="inMemoryUserDetailsService" />
+ * </bean>
+ *
+ * <bean id="inMemoryUserDetailsService"
+ * 	class="org.springframework.security.core.userdetails.memory.InMemoryDaoImpl">
+ * 	<property name="userProperties">
+ * 		<value>
+ * 			mike@SECPOD.DE=notUsed,ROLE_ADMIN
+ * 		</value>
+ * 	</property>
+ * </bean>
+ * </beans>
+ * 
+ * + *

+ * If you get a "GSSException: Channel binding mismatch (Mechanism level:ChannelBinding + * not provided!) have a look at this + * bug. + *

+ *

+ * A workaround unti this is fixed in the JVM is to change + *

+ * 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 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; + } + + /** + *

+ * This handler is called after a successful authentication. One can add additional + * authentication behavior by setting this. + *

+ *

+ * Default is null, which means nothing additional happens + *

+ * @param successHandler the authentication success handler + */ + public void setSuccessHandler(AuthenticationSuccessHandler successHandler) { + this.successHandler = successHandler; + } + + /** + *

+ * 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. + *

+ *

+ * Default is null, which means that the Filter returns the HTTP 500 code + *

+ * @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 + * AuthenticationManager. 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 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; + } + +} diff --git a/kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoEntryPoint.java b/kerberos/kerberos-web/src/main/java/org/springframework/security/kerberos/web/authentication/SpnegoEntryPoint.java new file mode 100644 index 0000000000..939f4ca275 --- /dev/null +++ b/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. + * + *

+ * With optional configured forwardUrl it is possible to use form login as + * fallback authentication. + *

+ * + *

+ * 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. + *

+ * + *

+ * See spnego-with-form-login.xml in spring-security-kerberos-sample for + * details + *

+ * + * @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(); + } + } + +} diff --git a/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfig.java b/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfig.java new file mode 100644 index 0000000000..84c05cdc74 --- /dev/null +++ b/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[] diff --git a/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfigTests.java b/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/AuthProviderConfigTests.java new file mode 100644 index 0000000000..805717b581 --- /dev/null +++ b/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() { + } + +} diff --git a/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/DummyUserDetailsService.java b/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/DummyUserDetailsService.java new file mode 100644 index 0000000000..d386f5a67f --- /dev/null +++ b/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[] diff --git a/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/SpnegoConfig.java b/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/docs/SpnegoConfig.java new file mode 100644 index 0000000000..2516aaf047 --- /dev/null +++ b/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[] diff --git a/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoAuthenticationProcessingFilterTests.java b/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoAuthenticationProcessingFilterTests.java new file mode 100644 index 0000000000..5c28969c11 --- /dev/null +++ b/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(); + } + +} diff --git a/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoEntryPointTests.java b/kerberos/kerberos-web/src/test/java/org/springframework/security/kerberos/web/SpnegoEntryPointTests.java new file mode 100644 index 0000000000..f866386d20 --- /dev/null +++ b/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 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 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")); + } + +} diff --git a/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/AuthProviderConfig.xml b/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/AuthProviderConfig.xml new file mode 100644 index 0000000000..661b39e98f --- /dev/null +++ b/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/AuthProviderConfig.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/SpnegoConfig.xml b/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/SpnegoConfig.xml new file mode 100644 index 0000000000..1a277417ca --- /dev/null +++ b/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/SpnegoConfig.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/appproperties.xml b/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/appproperties.xml new file mode 100644 index 0000000000..9afe751557 --- /dev/null +++ b/kerberos/kerberos-web/src/test/resources/org/springframework/security/kerberos/docs/appproperties.xml @@ -0,0 +1,12 @@ + + + + + +