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 0000000000..ff95c9b73f Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc1.png differ 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 0000000000..8eb235a0e9 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc2.png differ 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 0000000000..5be17ffc9b Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc3.png differ 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 0000000000..850845f89e Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/drawio-kerb-cc4.png differ diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png new file mode 100644 index 0000000000..97ba5e77a7 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff1.png differ 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 0000000000..bf3f250ad4 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff2.png differ 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 0000000000..696cd9fca7 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ff3.png differ 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 0000000000..1fddc2ed87 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie1.png differ diff --git a/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie2.png b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie2.png new file mode 100644 index 0000000000..e47df1e395 Binary files /dev/null and b/docs/modules/ROOT/assets/images/servlet/authentication/kerberos/ie2.png differ diff --git a/docs/modules/ROOT/examples/kerberos/AuthProviderConfig.java b/docs/modules/ROOT/examples/kerberos/AuthProviderConfig.java new file mode 100644 index 0000000000..2b166a542c --- /dev/null +++ b/docs/modules/ROOT/examples/kerberos/AuthProviderConfig.java @@ -0,0 +1,118 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.kerberos.docs; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileSystemResource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider; +import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider; +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient; +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator; +import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter; +import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +//tag::snippetA[] +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + + @Value("${app.service-principal}") + private String servicePrincipal; + + @Value("${app.keytab-location}") + private String keytabLocation; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + KerberosAuthenticationProvider kerberosAuthenticationProvider = kerberosAuthenticationProvider(); + KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider(); + ProviderManager providerManager = new ProviderManager(kerberosAuthenticationProvider, + kerberosServiceAuthenticationProvider); + + http + .authorizeHttpRequests((authz) -> authz + .requestMatchers("/", "/home").permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling() + .authenticationEntryPoint(spnegoEntryPoint()) + .and() + .formLogin() + .loginPage("/login").permitAll() + .and() + .logout() + .permitAll() + .and() + .authenticationProvider(kerberosAuthenticationProvider()) + .authenticationProvider(kerberosServiceAuthenticationProvider()) + .addFilterBefore(spnegoAuthenticationProcessingFilter(providerManager), + BasicAuthenticationFilter.class); + return http.build(); + } + + @Bean + public KerberosAuthenticationProvider kerberosAuthenticationProvider() { + KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider(); + SunJaasKerberosClient client = new SunJaasKerberosClient(); + client.setDebug(true); + provider.setKerberosClient(client); + provider.setUserDetailsService(dummyUserDetailsService()); + return provider; + } + + @Bean + public SpnegoEntryPoint spnegoEntryPoint() { + return new SpnegoEntryPoint("/login"); + } + + public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter( + AuthenticationManager authenticationManager) { + SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter(); + filter.setAuthenticationManager(authenticationManager); + return filter; + } + + @Bean + public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() { + KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider(); + provider.setTicketValidator(sunJaasKerberosTicketValidator()); + provider.setUserDetailsService(dummyUserDetailsService()); + return provider; + } + + @Bean + public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() { + SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator(); + ticketValidator.setServicePrincipal(servicePrincipal); + ticketValidator.setKeyTabLocation(new FileSystemResource(keytabLocation)); + ticketValidator.setDebug(true); + return ticketValidator; + } + + @Bean + public DummyUserDetailsService dummyUserDetailsService() { + return new DummyUserDetailsService(); + } +} +//end::snippetA[] 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 = "YIIGXAYGKwYBBQUCoIIGUDCCBkygMDAuBgkqhkiC9xIBAgIGCSqGSIb3EgEC" + + "AgYKKwYBBAGCNwICHgYKKwYBBAGCNwICCqKCBhYEggYSYIIGDgYJKoZIhvcS" + + "AQICAQBuggX9MIIF+aADAgEFoQMCAQ6iBwMFACAAAACjggSFYYIEgTCCBH2g" + + "AwIBBaENGwtFWEFNUExFLk9SR6IiMCCgAwIBAqEZMBcbBEhUVFAbD25lby5l" + + "eGFtcGxlLm9yZ6OCBEEwggQ9oAMCARehAwIBA6KCBC8EggQrD8vaEz0V5W5n" + + "PZINBBxp1yCVZOn4kpHzfNtqj9F3L/6MzrTo9bP2l0UhxCQIKo+ixUMJgQAs" + + "Xd82tF4JEsSt90pyv8f751pH3UeqCOhssTcXhJpTKQmYlAro+t3klpT6/c/r" + + "4KX+wqM++19IjWE2CJpyloo/5Wi9Kwk83bjO6UfCTreqkd+eIPM16rf8p/wH" + + "KYj+ssla4y+IvwvZvAW8TXuth8opiqeLvt5H0GWkwuJhrZu6cHlSWZAMtRQg" + + "TSZCS/0LCiZVCyNNCpvvXbyp8p5T6ImKPfMO5l8VJKgdrmCOlAQYFwTpG0MD" + + "1e9LUvk/Fh7OoeglJAygTRgbvIGDAuexw7o6MHbj+XhXvEtC6kUEwHuG5C/1" + + "5Q327FRLfMeL8YcdU6YZ06wNmUmDPGqy+WHlEaFM7G38u/oKKS4cKIZKi8PL" + + "hpVPvjU+uIOJVuIP882IxCW7rcqaRCleYCp7YAQbjussrCS0DSRKPEy60bv0" + + "MIkh71lCY5/KwQloEDMqav12+1wtWTnmLAkfglGjgb1Q7fb79h58nnTBJAwI" + + "e6Bv72XYdgcU1orDQVlylAk9trxDP42yOGuG5IozJTIn+9zPOvM5CGgTCzZv" + + "4wInGa1Stuz11WwaIenwGbpCXWSP4uoe9TLpKVzJUmLd8dpZ0YjpuFNBGnHz" + + "1LG0Q9aUni7nl7seKVc2AnuBqS+mlS+/In0LaEW4k0GctgMqfVyP2mmb7ur+" + + "wl4YjAVRFhPMSSy4AYftRYoIUGad97VcZx107pD0v/gE1Eu4iqTomqJBOaWJ" + + "gqnjmf6A8P9IHbeVx/zbnKYp8nC+M57jpFcy9GKVh3DIXkbSBHQ+feamGBJn" + + "AxTpeix/DN5u91azJaB9RlfIvQYGLGaxupCXpjVfhTSJHvoA6sOUObgK3/hQ" + + "7Gj81FR+C8AfrHzOPPD2S14pkL7n2WC6jOTHrghxm7/iXcreDHos/1OuPFk0" + + "9wbrCWgF9tHAuXQJW/zxjYg9CUboJ51+ZposfmABTKoUKeFY4zgVyuEwE2YO" + + "hn7OLsfbXalmF5IPAlNibAIIFVos1u+14oFOYivIXEEgpvZMhvFOuGaqrHHR" + + "xRBQ/z8nogMVGyCukFH/tg5N8IX9X+VQ1U43rf4IYaCJ0no5skmStf7fmcUJ" + + "+3KXhKfP4TKrSIDdo313GW/6rIM2wo4RPdjQ1LlX+EAb8X73W0OZLumtvhm9" + + "1jL2pWFL/mTGEGkPd7Od29h7JYcvwdDCjkIzIlrbzFJyyTU3ATaMyrvDZKys" + + "ZSJ2m3v7Y0E/Cw+/T8SG3HeSjJ2e/dsjJRpv+6RxXzdNWKKCUN3UFEH0QfAk" + + "6s8avEF767U87Df7BBCuecxIJAUL+kBBsYuDCw8FP0AOxOIjh9EX/EopeJpi" + + "e1ekNGvUK+mhj3WgjCExEe60y4FoENKkggFZMIIBVaADAgEXooIBTASCAUgR" + + "/FTo9JsQB4yInDswmvHiOyJYGdA9jv72rjvJfdHejaU6L8QHj0DPMdGWxAXI" + + "aqLrANjOOSGb9HEdt9QUd/zvi8fBEEZgWIX0nUUrvN9wsKEB1jxmlAx87mf7" + + "2Kyo9z7mdlFBG49mq/jjFFLtiVJxHfea4B4VGRUodNRLWUY7H05ruJZQbeUF" + + "UgYMsiMC59oi82OR3re8gpypecrtD0g88CwCrReDpoLb7VGVCc4z00ld7ugz" + + "EbGsZvh0SLMKnxAAm1nYlqQTu/VKC8zi9N0c7ikJegGwBKOgbebPm+ckKDra" + + "fbVsm0pcmnXv5WvwjJPFjJWsL+7NzUfsedJxgHTCzdztZyNxu6iQf8cpAabp" + + "PB1vJdIMjc8benP9/+EUhX1LkwvV/rOO3ocwjtdLY1rcmNXSbhnf8jDcVjOe" + "eL2PHBfvkne/FgxC"; + + // @Rule + // public ExpectedException thrown = ExpectedException.none(); + + // @Test + // public void testJdkMsKrb5OIDRegressionTweak() throws Exception { + // thrown.expect(BadCredentialsException.class); + // thrown.expectMessage(not(containsString("GSSContext name of the context initiator + // is null"))); + // thrown.expectMessage(containsString("Kerberos validation not successful")); + // SunJaasKerberosTicketValidator validator = new SunJaasKerberosTicketValidator(); + // byte[] kerberosTicket = Base64.decode(header.getBytes()); + // validator.validateTicket(kerberosTicket); + // } + + @Test + public void testJdkMsKrb5OIDRegressionTweak() { + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> { + SunJaasKerberosTicketValidator validator = new SunJaasKerberosTicketValidator(); + byte[] kerberosTicket = Base64.getDecoder().decode(header.getBytes()); + validator.validateTicket(kerberosTicket); + }).withMessage("Kerberos validation not successful"); + } + +} 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 @@ + + + + + +